text: import Vue from '../../vue'
  import { NAME_CALENDAR } from '../../constants/components'
  import {
    CALENDAR_GREGORY,
    CALENDAR_LONG,
    CALENDAR_NARROW,
    CALENDAR_SHORT,
    DATE_FORMAT_2_DIGIT,
    DATE_FORMAT_NUMERIC
  } from '../../constants/date'
  import {
    CODE_DOWN,
    CODE_END,
    CODE_ENTER,
    CODE_HOME,
    CODE_LEFT,
    CODE_PAGEDOWN,
    CODE_PAGEUP,
    CODE_RIGHT,
    CODE_SPACE,
    CODE_UP
  } from '../../constants/key-codes'
  import identity from '../../utils/identity'
  import looseEqual from '../../utils/loose-equal'
  import { arrayIncludes, concat } from '../../utils/array'
  import { makePropsConfigurable } from '../../utils/config'
  import {
    createDate,
    createDateFormatter,
    constrainDate,
    datesEqual,
    firstDateOfMonth,
    formatYMD,
    lastDateOfMonth,
    oneMonthAgo,
    oneMonthAhead,
    oneYearAgo,
    oneYearAhead,
    oneDecadeAgo,
    oneDecadeAhead,
    parseYMD,
    resolveLocale
  } from '../../utils/date'
  import { attemptBlur, attemptFocus, requestAF } from '../../utils/dom'
  import { stopEvent } from '../../utils/events'
  import { isArray, isPlainObject, isString } from '../../utils/inspect'
  import { isLocaleRTL } from '../../utils/locale'
  import { mathMax } from '../../utils/math'
  import { toInteger } from '../../utils/number'
  import { toString } from '../../utils/string'
  import attrsMixin from '../../mixins/attrs'
  import idMixin from '../../mixins/id'
  import normalizeSlotMixin from '../../mixins/normalize-slot'
  import {
    BIconChevronLeft,
    BIconChevronDoubleLeft,
    BIconChevronBarLeft,
    BIconCircleFill
  } from '../../icons/icons'
  
  // --- Props ---
  
  export const props = makePropsConfigurable(
    {
      value: {
        type: [String, Date]
        // default: null
      },
      valueAsDate: {
        // Always return the `v-model` value as a date object
        type: Boolean,
        default: false
      },
      initialDate: {
        // This specifies the calendar year/month/day that will be shown when
        // first opening the datepicker if no v-model value is provided
        // Default is the current date (or `min`/`max`)
        type: [String, Date]
        // default: null
      },
      disabled: {
        type: Boolean,
        default: false
      },
      readonly: {
        type: Boolean,
        default: false
      },
      min: {
        type: [String, Date]
        // default: null
      },
      max: {
        type: [String, Date]
        // default: null
      },
      dateDisabledFn: {
        type: Function
        // default: null
      },
      startWeekday: {
        // `0` (Sunday), `1` (Monday), ... `6` (Saturday)
        // Day of week to start calendar on
        type: [Number, String],
        default: 0
      },
      locale: {
        // Locale(s) to use
        // Default is to use page/browser default setting
        type: [String, Array]
        // default: null
      },
      direction: {
        // 'ltr', 'rtl', or `null` (for auto detect)
        type: String
        // default: null
      },
      selectedVariant: {
        // Variant color to use for the selected date
        type: String,
        default: 'primary'
      },
      todayVariant: {
        // Variant color to use for today's date (defaults to `selectedVariant`)
        type: String
        // default: null
      },
      navButtonVariant: {
        // Variant color to use for the navigation buttons
        type: String,
        default: 'secondary'
      },
      noHighlightToday: {
        // Disable highlighting today's date
        type: Boolean,
        default: false
      },
      dateInfoFn: {
        // Function to set a class of (classes) on the date cell
        // if passed a string or an array
        // TODO:
        //   If the function returns an object, look for class prop for classes,
        //   and other props for handling events/details/descriptions
        type: Function
        // default: null
      },
      width: {
        // Has no effect if prop `block` is set
        type: String,
        default: '270px'
      },
      block: {
        // Makes calendar the full width of its parent container
        type: Boolean,
        default: false
      },
      hideHeader: {
        // When true makes the selected date header `sr-only`
        type: Boolean,
        default: false
      },
      showDecadeNav: {
        // When `true` enables the decade navigation buttons
        type: Boolean,
        default: false
      },
      hidden: {
        // When `true`, renders a comment node, but keeps the component instance active
        // Mainly for <b-form-date>, so that we can get the component's value and locale
        // But we might just use separate date formatters, using the resolved locale
        // (adjusted for the gregorian calendar)
        type: Boolean,
        default: false
      },
      ariaControls: {
        type: String
        // default: null
      },
      noKeyNav: {
        type: Boolean,
        default: false
      },
      roleDescription: {
        type: String
        // default: null
      },
      // Labels for buttons and keyboard shortcuts
      labelPrevDecade: {
        type: String,
        default: 'Previous decade'
      },
      labelPrevYear: {
        type: String,
        default: 'Previous year'
      },
      labelPrevMonth: {
        type: String,
        default: 'Previous month'
      },
      labelCurrentMonth: {
        type: String,
        default: 'Current month'
      },
      labelNextMonth: {
        type: String,
        default: 'Next month'
      },
      labelNextYear: {
        type: String,
        default: 'Next year'
      },
      labelNextDecade: {
        type: String,
        default: 'Next decade'
      },
      labelToday: {
        type: String,
        default: 'Today'
      },
      labelSelected: {
        type: String,
        default: 'Selected date'
      },
      labelNoDateSelected: {
        type: String,
        default: 'No date selected'
      },
      labelCalendar: {
        type: String,
        default: 'Calendar'
      },
      labelNav: {
        type: String,
        default: 'Calendar navigation'
      },
      labelHelp: {
        type: String,
        default: 'Use cursor keys to navigate calendar dates'
      },
      dateFormatOptions: {
        // `Intl.DateTimeFormat` object
        // Note: This value is *not* to be placed in the global config
        type: Object,
        default: () => ({
          year: DATE_FORMAT_NUMERIC,
          month: CALENDAR_LONG,
          day: DATE_FORMAT_NUMERIC,
          weekday: CALENDAR_LONG
        })
      },
      weekdayHeaderFormat: {
        // Format of the weekday names at the top of the calendar
        // Note: This value is *not* to be placed in the global config
        type: String,
        // `short` is typically a 3 letter abbreviation,
        // `narrow` is typically a single letter
        // `long` is the full week day name
        // Although some locales may override this (i.e `ar`, etc.)
        default: CALENDAR_SHORT,
        validator(value) {
          return arrayIncludes([CALENDAR_LONG, CALENDAR_SHORT, CALENDAR_NARROW], value)
        }
      }
    },
    NAME_CALENDAR
  )
  
  // --- Main component ---
  // @vue/component
  export const BCalendar = Vue.extend({
    name: NAME_CALENDAR,
    // Mixin order is important!
    mixins: [attrsMixin, idMixin, normalizeSlotMixin],
    model: {
      // Even though this is the default that Vue assumes, we need
      // to add it for the docs to reflect that this is the model
      // And also for some validation libraries to work
      prop: 'value',
      event: 'input'
    },
    props,
    data() {
      const selected = formatYMD(this.value) || ''
      return {
        // Selected date
        selectedYMD: selected,
        // Date in calendar grid that has `tabindex` of `0`
        activeYMD:
          selected ||
          formatYMD(constrainDate(this.initialDate || this.getToday()), this.min, this.max),
        // Will be true if the calendar grid has/contains focus
        gridHasFocus: false,
        // Flag to enable the `aria-live` region(s) after mount
        // to prevent screen reader "outbursts" when mounting
        isLive: false
      }
    },
    computed: {
      valueId() {
        return this.safeId()
      },
      widgetId() {
        return this.safeId('_calendar-wrapper_')
      },
      navId() {
        return this.safeId('_calendar-nav_')
      },
      gridId() {
        return this.safeId('_calendar-grid_')
      },
      gridCaptionId() {
        return this.safeId('_calendar-grid-caption_')
      },
      gridHelpId() {
        return this.safeId('_calendar-grid-help_')
      },
      activeId() {
        return this.activeYMD ? this.safeId(`_cell-${this.activeYMD}_`) : null
      },
      // TODO: Use computed props to convert `YYYY-MM-DD` to `Date` object
      selectedDate() {
        // Selected as a `Date` object
        return parseYMD(this.selectedYMD)
      },
      activeDate() {
        // Active as a `Date` object
        return parseYMD(this.activeYMD)
      },
      computedMin() {
        return parseYMD(this.min)
      },
      computedMax() {
        return parseYMD(this.max)
      },
      computedWeekStarts() {
        // `startWeekday` is a prop (constrained to `0` through `6`)
        return mathMax(toInteger(this.startWeekday, 0), 0) % 7
      },
      computedLocale() {
        // Returns the resolved locale used by the calendar
        return resolveLocale(concat(this.locale).filter(identity), CALENDAR_GREGORY)
      },
      computedDateDisabledFn() {
        const { dateDisabledFn } = this
        return dateDisabledFn.name !== 'default' ? dateDisabledFn : () => false
      },
      // TODO: Change `dateInfoFn` to handle events and notes as well as classes
      computedDateInfoFn() {
        const { dateInfoFn } = this
        return dateInfoFn.name !== 'default' ? dateInfoFn : () => ({})
      },
      calendarLocale() {
        // This locale enforces the gregorian calendar (for use in formatter functions)
        // Needed because IE 11 resolves `ar-IR` as islamic-civil calendar
        // and IE 11 (and some other browsers) do not support the `calendar` option
        // And we currently only support the gregorian calendar
        const fmt = new Intl.DateTimeFormat(this.computedLocale, { calendar: CALENDAR_GREGORY })
        const calendar = fmt.resolvedOptions().calendar
        let locale = fmt.resolvedOptions().locale
        /* istanbul ignore if: mainly for IE 11 and a few other browsers, hard to test in JSDOM */
        if (calendar !== CALENDAR_GREGORY) {
          // Ensure the locale requests the gregorian calendar
          // Mainly for IE 11, and currently we can't handle non-gregorian calendars
          // TODO: Should we always return this value?
          locale = locale.replace(/-u-.+$/i, '').concat('-u-ca-gregory')
        }
        return locale
      },
      calendarYear() {
        return this.activeDate.getFullYear()
      },
      calendarMonth() {
        return this.activeDate.getMonth()
      },
      calendarFirstDay() {
        // We set the time for this date to 12pm to work around
        // date formatting issues in Firefox and Safari
        // See: https://github.com/bootstrap-vue/bootstrap-vue/issues/5818
        return createDate(this.calendarYear, this.calendarMonth, 1, 12)
      },
      calendarDaysInMonth() {
        // We create a new date as to not mutate the original
        const date = createDate(this.calendarFirstDay)
        date.setMonth(date.getMonth() + 1, 0)
        return date.getDate()
      },
      computedVariant() {
        return `btn-${this.selectedVariant || 'primary'}`
      },
      computedTodayVariant() {
        return `btn-outline-${this.todayVariant || this.selectedVariant || 'primary'}`
      },
      computedNavButtonVariant() {
        return `btn-outline-${this.navButtonVariant || 'primary'}`
      },
      isRTL() {
        // `true` if the language requested is RTL
        const dir = toString(this.direction).toLowerCase()
        if (dir === 'rtl') {
          /* istanbul ignore next */
          return true
        } else if (dir === 'ltr') {
          /* istanbul ignore next */
          return false
        }
        return isLocaleRTL(this.computedLocale)
      },
      context() {
        const { selectedYMD, activeYMD } = this
        const selectedDate = parseYMD(selectedYMD)
        const activeDate = parseYMD(activeYMD)
        return {
          // The current value of the `v-model`
          selectedYMD,
          selectedDate,
          selectedFormatted: selectedDate
            ? this.formatDateString(selectedDate)
            : this.labelNoDateSelected,
          // Which date cell is considered active due to navigation
          activeYMD,
          activeDate,
          activeFormatted: activeDate ? this.formatDateString(activeDate) : '',
          // `true` if the date is disabled (when using keyboard navigation)
          disabled: this.dateDisabled(activeDate),
          // Locales used in formatting dates
          locale: this.computedLocale,
          calendarLocale: this.calendarLocale,
          rtl: this.isRTL
        }
      },
      // Computed props that return a function reference
      dateOutOfRange() {
        // Check whether a date is within the min/max range
        // Returns a new function ref if the pops change
        // We do this as we need to trigger the calendar computed prop
        // to update when these props update
        const { computedMin: min, computedMax: max } = this
        return date => {
          // Handle both `YYYY-MM-DD` and `Date` objects
          date = parseYMD(date)
          return (min && date < min) || (max && date > max)
        }
      },
      dateDisabled() {
        // Returns a function for validating if a date is within range
        // We grab this variables first to ensure a new function ref
        // is generated when the props value changes
        // We do this as we need to trigger the calendar computed prop
        // to update when these props update
        const rangeFn = this.dateOutOfRange
        // Return the function ref
        return date => {
          // Handle both `YYYY-MM-DD` and `Date` objects
          date = parseYMD(date)
          const ymd = formatYMD(date)
          return !!(rangeFn(date) || this.computedDateDisabledFn(ymd, date))
        }
      },
      // Computed props that return date formatter functions
      formatDateString() {
        // Returns a date formatter function
        return createDateFormatter(this.calendarLocale, {
          // Ensure we have year, month, day shown for screen readers/ARIA
          // If users really want to leave one of these out, they can
          // pass `undefined` for the property value
          year: DATE_FORMAT_NUMERIC,
          month: DATE_FORMAT_2_DIGIT,
          day: DATE_FORMAT_2_DIGIT,
          // Merge in user supplied options
          ...this.dateFormatOptions,
          // Ensure hours/minutes/seconds are not shown
          // As we do not support the time portion (yet)
          hour: undefined,
          minute: undefined,
          second: undefined,
          // Ensure calendar is gregorian
          calendar: CALENDAR_GREGORY
        })
      },
      formatYearMonth() {
        // Returns a date formatter function
        return createDateFormatter(this.calendarLocale, {
          year: DATE_FORMAT_NUMERIC,
          month: CALENDAR_LONG,
          calendar: CALENDAR_GREGORY
        })
      },
      formatWeekdayName() {
        // Long weekday name for weekday header aria-label
        return createDateFormatter(this.calendarLocale, {
          weekday: CALENDAR_LONG,
          calendar: CALENDAR_GREGORY
        })
      },
      formatWeekdayNameShort() {
        // Weekday header cell format
        // defaults to 'short' 3 letter days, where possible
        return createDateFormatter(this.calendarLocale, {
          weekday: this.weekdayHeaderFormat || CALENDAR_SHORT,
          calendar: CALENDAR_GREGORY
        })
      },
      formatDay() {
        // Calendar grid day number formatter
        // We don't use DateTimeFormatter here as it can place extra
        // character(s) after the number (i.e the `zh` locale)
        const nf = new Intl.NumberFormat([this.computedLocale], {
          style: 'decimal',
          minimumIntegerDigits: 1,
          minimumFractionDigits: 0,
          maximumFractionDigits: 0,
          notation: 'standard'
        })
        // Return a formatter function instance
        return date => nf.format(date.getDate())
      },
      // Disabled states for the nav buttons
      prevDecadeDisabled() {
        const min = this.computedMin
        return this.disabled || (min && lastDateOfMonth(oneDecadeAgo(this.activeDate)) < min)
      },
      prevYearDisabled() {
        const min = this.computedMin
        return this.disabled || (min && lastDateOfMonth(oneYearAgo(this.activeDate)) < min)
      },
      prevMonthDisabled() {
        const min = this.computedMin
        return this.disabled || (min && lastDateOfMonth(oneMonthAgo(this.activeDate)) < min)
      },
      thisMonthDisabled() {
        // TODO: We could/should check if today is out of range
        return this.disabled
      },
      nextMonthDisabled() {
        const max = this.computedMax
        return this.disabled || (max && firstDateOfMonth(oneMonthAhead(this.activeDate)) > max)
      },
      nextYearDisabled() {
        const max = this.computedMax
        return this.disabled || (max && firstDateOfMonth(oneYearAhead(this.activeDate)) > max)
      },
      nextDecadeDisabled() {
        const max = this.computedMax
        return this.disabled || (max && firstDateOfMonth(oneDecadeAhead(this.activeDate)) > max)
      },
      // Calendar dates generation
      calendar() {
        const matrix = []
        const firstDay = this.calendarFirstDay
        const calendarYear = firstDay.getFullYear()
        const calendarMonth = firstDay.getMonth()
        const daysInMonth = this.calendarDaysInMonth
        const startIndex = firstDay.getDay() // `0`..`6`
        const weekOffset = (this.computedWeekStarts > startIndex ? 7 : 0) - this.computedWeekStarts
        // Build the calendar matrix
        let currentDay = 0 - weekOffset - startIndex
        for (let week = 0; week < 6 && currentDay < daysInMonth; week++) {
          // For each week
          matrix[week] = []
          // The following could be a map function
          for (let j = 0; j < 7; j++) {
            // For each day in week
            currentDay++
            const date = createDate(calendarYear, calendarMonth, currentDay)
            const month = date.getMonth()
            const dayYMD = formatYMD(date)
            const dayDisabled = this.dateDisabled(date)
            // TODO: This could be a normalizer method
            let dateInfo = this.computedDateInfoFn(dayYMD, parseYMD(dayYMD))
            dateInfo =
              isString(dateInfo) || isArray(dateInfo)
                ? /* istanbul ignore next */ { class: dateInfo }
                : isPlainObject(dateInfo)
                  ? { class: '', ...dateInfo }
                  : /* istanbul ignore next */ { class: '' }
            matrix[week].push({
              ymd: dayYMD,
              // Cell content
              day: this.formatDay(date),
              label: this.formatDateString(date),
              // Flags for styling
              isThisMonth: month === calendarMonth,
              isDisabled: dayDisabled,
              // TODO: Handle other dateInfo properties such as notes/events
              info: dateInfo
            })
          }
        }
        return matrix
      },
      calendarHeadings() {
        return this.calendar[0].map(d => {
          return {
            text: this.formatWeekdayNameShort(parseYMD(d.ymd)),
            label: this.formatWeekdayName(parseYMD(d.ymd))
          }
        })
      }
    },
    watch: {
      value(newVal, oldVal) {
        const selected = formatYMD(newVal) || ''
        const old = formatYMD(oldVal) || ''
        if (!datesEqual(selected, old)) {
          this.activeYMD = selected || this.activeYMD
          this.selectedYMD = selected
        }
      },
      selectedYMD(newYMD, oldYMD) {
        // TODO:
        //   Should we compare to `formatYMD(this.value)` and emit
        //   only if they are different?
        if (newYMD !== oldYMD) {
          this.$emit('input', this.valueAsDate ? parseYMD(newYMD) || null : newYMD || '')
        }
      },
      context(newVal, oldVal) {
        if (!looseEqual(newVal, oldVal)) {
          this.$emit('context', newVal)
        }
      },
      hidden(newVal) {
        // Reset the active focused day when hidden
        this.activeYMD =
          this.selectedYMD ||
          formatYMD(this.value || this.constrainDate(this.initialDate || this.getToday()))
        // Enable/disable the live regions
        this.setLive(!newVal)
      }
    },
    created() {
      this.$nextTick(() => {
        this.$emit('context', this.context)
      })
    },
    mounted() {
      this.setLive(true)
    },
    /* istanbul ignore next */
    activated() {
      this.setLive(true)
    },
    /* istanbul ignore next */
    deactivated() {
      this.setLive(false)
    },
    beforeDestroy() {
      this.setLive(false)
    },
    methods: {
      // Public method(s)
      focus() {
        if (!this.disabled) {
          attemptFocus(this.$refs.grid)
        }
      },
      blur() {
        if (!this.disabled) {
          attemptBlur(this.$refs.grid)
        }
      },
      // Private methods
      setLive(on) {
        if (on) {
          this.$nextTick(() => {
            requestAF(() => {
              this.isLive = true
            })
          })
        } else {
          this.isLive = false
        }
      },
      getToday() {
        return parseYMD(createDate())
      },
      constrainDate(date) {
        // Constrains a date between min and max
        // returns a new `Date` object instance
        return constrainDate(date, this.computedMin, this.computedMax)
      },
      emitSelected(date) {
        // Performed in a `$nextTick()` to (probably) ensure
        // the input event has emitted first
        this.$nextTick(() => {
          this.$emit('selected', formatYMD(date) || '', parseYMD(date) || null)
        })
      },
      // Event handlers
      setGridFocusFlag(evt) {
        // Sets the gridHasFocus flag to make date "button" look focused
        this.gridHasFocus = !this.disabled && evt.type === 'focus'
      },
      onKeydownWrapper(evt) {
        // Calendar keyboard navigation
        // Handles PAGEUP/PAGEDOWN/END/HOME/LEFT/UP/RIGHT/DOWN
        // Focuses grid after updating
        if (this.noKeyNav) {
          /* istanbul ignore next */
          return
        }
        const { altKey, ctrlKey, keyCode } = evt
        if (
          !arrayIncludes(
            [
              CODE_PAGEUP,
              CODE_PAGEDOWN,
              CODE_END,
              CODE_HOME,
              CODE_LEFT,
              CODE_UP,
              CODE_RIGHT,
              CODE_DOWN
            ],
            keyCode
          )
        ) {
          /* istanbul ignore next */
          return
        }
        stopEvent(evt)
        let activeDate = createDate(this.activeDate)
        let checkDate = createDate(this.activeDate)
        const day = activeDate.getDate()
        const constrainedToday = this.constrainDate(this.getToday())
        const isRTL = this.isRTL
        if (keyCode === CODE_PAGEUP) {
          // PAGEUP - Previous month/year
          activeDate = (altKey ? (ctrlKey ? oneDecadeAgo : oneYearAgo) : oneMonthAgo)(activeDate)
          // We check the first day of month to be in rage
          checkDate = createDate(activeDate)
          checkDate.setDate(1)
        } else if (keyCode === CODE_PAGEDOWN) {
          // PAGEDOWN - Next month/year
          activeDate = (altKey ? (ctrlKey ? oneDecadeAhead : oneYearAhead) : oneMonthAhead)(
            activeDate
          )
          // We check the last day of month to be in rage
          checkDate = createDate(activeDate)
          checkDate.setMonth(checkDate.getMonth() + 1)
          checkDate.setDate(0)
        } else if (keyCode === CODE_LEFT) {
          // LEFT - Previous day (or next day for RTL)
          activeDate.setDate(day + (isRTL ? 1 : -1))
          activeDate = this.constrainDate(activeDate)
          checkDate = activeDate
        } else if (keyCode === CODE_RIGHT) {
          // RIGHT - Next day (or previous day for RTL)
          activeDate.setDate(day + (isRTL ? -1 : 1))
          activeDate = this.constrainDate(activeDate)
          checkDate = activeDate
        } else if (keyCode === CODE_UP) {
          // UP - Previous week
          activeDate.setDate(day - 7)
          activeDate = this.constrainDate(activeDate)
          checkDate = activeDate
        } else if (keyCode === CODE_DOWN) {
          // DOWN - Next week
          activeDate.setDate(day + 7)
          activeDate = this.constrainDate(activeDate)
          checkDate = activeDate
        } else if (keyCode === CODE_HOME) {
          // HOME - Today
          activeDate = constrainedToday
          checkDate = activeDate
        } else if (keyCode === CODE_END) {
          // END - Selected date, or today if no selected date
          activeDate = parseYMD(this.selectedDate) || constrainedToday
          checkDate = activeDate
        }
        if (!this.dateOutOfRange(checkDate) && !datesEqual(activeDate, this.activeDate)) {
          // We only jump to date if within min/max
          // We don't check for individual disabled dates though (via user function)
          this.activeYMD = formatYMD(activeDate)
        }
        // Ensure grid is focused
        this.focus()
      },
      onKeydownGrid(evt) {
        // Pressing enter/space on grid to select active date
        const keyCode = evt.keyCode
        const activeDate = this.activeDate
        if (keyCode === CODE_ENTER || keyCode === CODE_SPACE) {
          stopEvent(evt)
          if (!this.disabled && !this.readonly && !this.dateDisabled(activeDate)) {
            this.selectedYMD = formatYMD(activeDate)
            this.emitSelected(activeDate)
          }
          // Ensure grid is focused
          this.focus()
        }
      },
      onClickDay(day) {
        // Clicking on a date "button" to select it
        const { selectedDate, activeDate } = this
        const clickedDate = parseYMD(day.ymd)
        if (!this.disabled && !day.isDisabled && !this.dateDisabled(clickedDate)) {
          if (!this.readonly) {
            // If readonly mode, we don't set the selected date, just the active date
            // If the clicked date is equal to the already selected date, we don't update the model
            this.selectedYMD = formatYMD(
              datesEqual(clickedDate, selectedDate) ? selectedDate : clickedDate
            )
            this.emitSelected(clickedDate)
          }
          this.activeYMD = formatYMD(
            datesEqual(clickedDate, activeDate) ? activeDate : createDate(clickedDate)
          )
          // Ensure grid is focused
          this.focus()
        }
      },
      gotoPrevDecade() {
        this.activeYMD = formatYMD(this.constrainDate(oneDecadeAgo(this.activeDate)))
      },
      gotoPrevYear() {
        this.activeYMD = formatYMD(this.constrainDate(oneYearAgo(this.activeDate)))
      },
      gotoPrevMonth() {
        this.activeYMD = formatYMD(this.constrainDate(oneMonthAgo(this.activeDate)))
      },
      gotoCurrentMonth() {
        // TODO: Maybe this goto date should be configurable?
        this.activeYMD = formatYMD(this.constrainDate(this.getToday()))
      },
      gotoNextMonth() {
        this.activeYMD = formatYMD(this.constrainDate(oneMonthAhead(this.activeDate)))
      },
      gotoNextYear() {
        this.activeYMD = formatYMD(this.constrainDate(oneYearAhead(this.activeDate)))
      },
      gotoNextDecade() {
        this.activeYMD = formatYMD(this.constrainDate(oneDecadeAhead(this.activeDate)))
      },
      onHeaderClick() {
        if (!this.disabled) {
          this.activeYMD = this.selectedYMD || formatYMD(this.getToday())
          this.focus()
        }
      }
    },
    render(h) {
      // If `hidden` prop is set, render just a placeholder node
      if (this.hidden) {
        return h()
      }
  
      const {
        valueId,
        widgetId,
        navId,
        gridId,
        gridCaptionId,
        gridHelpId,
        activeId,
        disabled,
        noKeyNav,
        isLive,
        isRTL,
        activeYMD,
        selectedYMD,
        safeId
      } = this
      const hideDecadeNav = !this.showDecadeNav
      const todayYMD = formatYMD(this.getToday())
      const highlightToday = !this.noHighlightToday
  
      // Header showing current selected date
      let $header = h(
        'output',
        {
          staticClass: 'form-control form-control-sm text-center',
          class: { 'text-muted': disabled, readonly: this.readonly || disabled },
          attrs: {
            id: valueId,
            for: gridId,
            role: 'status',
            tabindex: disabled ? null : '-1',
            // Mainly for testing purposes, as we do not know
            // the exact format `Intl` will format the date string
            'data-selected': toString(selectedYMD),
            // We wait until after mount to enable `aria-live`
            // to prevent initial announcement on page render
            'aria-live': isLive ? 'polite' : 'off',
            'aria-atomic': isLive ? 'true' : null
          },
          on: {
            // Transfer focus/click to focus grid
            // and focus active date (or today if no selection)
            click: this.onHeaderClick,
            focus: this.onHeaderClick
          }
        },
        this.selectedDate
          ? [
              // We use `bdi` elements here in case the label doesn't match the locale
              // Although IE 11 does not deal with <BDI> at all (equivalent to a span)
              h('bdi', { staticClass: 'sr-only' }, ` (${toString(this.labelSelected)}) `),
              h('bdi', this.formatDateString(this.selectedDate))
            ]
          : this.labelNoDateSelected || '\u00a0' // '&nbsp;'
      )
      $header = h(
        'header',
        {
          staticClass: 'b-calendar-header',
          class: { 'sr-only': this.hideHeader },
          attrs: { title: this.selectedDate ? this.labelSelectedDate || null : null }
        },
        [$header]
      )
  
      // Content for the date navigation buttons
      const navScope = { isRTL }
      const navProps = { shiftV: 0.5 }
      const navPrevProps = { ...navProps, flipH: isRTL }
      const navNextProps = { ...navProps, flipH: !isRTL }
      const $prevDecadeIcon =
        this.normalizeSlot('nav-prev-decade', navScope) ||
        h(BIconChevronBarLeft, { props: navPrevProps })
      const $prevYearIcon =
        this.normalizeSlot('nav-prev-year', navScope) ||
        h(BIconChevronDoubleLeft, { props: navPrevProps })
      const $prevMonthIcon =
        this.normalizeSlot('nav-prev-month', navScope) || h(BIconChevronLeft, { props: navPrevProps })
      const $thisMonthIcon =
        this.normalizeSlot('nav-this-month', navScope) || h(BIconCircleFill, { props: navProps })
      const $nextMonthIcon =
        this.normalizeSlot('nav-next-month', navScope) || h(BIconChevronLeft, { props: navNextProps })
      const $nextYearIcon =
        this.normalizeSlot('nav-next-year', navScope) ||
        h(BIconChevronDoubleLeft, { props: navNextProps })
      const $nextDecadeIcon =
        this.normalizeSlot('nav-next-decade', navScope) ||
        h(BIconChevronBarLeft, { props: navNextProps })
  
      // Utility to create the date navigation buttons
      const makeNavBtn = (content, label, handler, btnDisabled, shortcut) => {
        return h(
          'button',
          {
            staticClass: 'btn btn-sm border-0 flex-fill',
            class: [this.computedNavButtonVariant, { disabled: btnDisabled }],
            attrs: {
              title: label || null,
              type: 'button',
              tabindex: noKeyNav ? '-1' : null,
              'aria-label': label || null,
              'aria-disabled': btnDisabled ? 'true' : null,
              'aria-keyshortcuts': shortcut || null
            },
            on: btnDisabled ? {} : { click: handler }
          },
          [h('div', { attrs: { 'aria-hidden': 'true' } }, [content])]
        )
      }
  
      // Generate the date navigation buttons
      const $nav = h(
        'div',
        {
          staticClass: 'b-calendar-nav d-flex',
          attrs: {
            id: navId,
            role: 'group',
            tabindex: noKeyNav ? '-1' : null,
            'aria-hidden': disabled ? 'true' : null,
            'aria-label': this.labelNav || null,
            'aria-controls': gridId
          }
        },
        [
          hideDecadeNav
            ? h()
            : makeNavBtn(
                $prevDecadeIcon,
                this.labelPrevDecade,
                this.gotoPrevDecade,
                this.prevDecadeDisabled,
                'Ctrl+Alt+PageDown'
              ),
          makeNavBtn(
            $prevYearIcon,
            this.labelPrevYear,
            this.gotoPrevYear,
            this.prevYearDisabled,
            'Alt+PageDown'
          ),
          makeNavBtn(
            $prevMonthIcon,
            this.labelPrevMonth,
            this.gotoPrevMonth,
            this.prevMonthDisabled,
            'PageDown'
          ),
          makeNavBtn(
            $thisMonthIcon,
            this.labelCurrentMonth,
            this.gotoCurrentMonth,
            this.thisMonthDisabled,
            'Home'
          ),
          makeNavBtn(
            $nextMonthIcon,
            this.labelNextMonth,
            this.gotoNextMonth,
            this.nextMonthDisabled,
            'PageUp'
          ),
          makeNavBtn(
            $nextYearIcon,
            this.labelNextYear,
            this.gotoNextYear,
            this.nextYearDisabled,
            'Alt+PageUp'
          ),
          hideDecadeNav
            ? h()
            : makeNavBtn(
                $nextDecadeIcon,
                this.labelNextDecade,
                this.gotoNextDecade,
                this.nextDecadeDisabled,
                'Ctrl+Alt+PageUp'
              )
        ]
      )
  
      // Caption for calendar grid
      const $gridCaption = h(
        'header',
        {
          key: 'grid-caption',
          staticClass: 'b-calendar-grid-caption text-center font-weight-bold',
          class: { 'text-muted': disabled },
          attrs: {
            id: gridCaptionId,
            'aria-live': isLive ? 'polite' : null,
            'aria-atomic': isLive ? 'true' : null
          }
        },
        this.formatYearMonth(this.calendarFirstDay)
      )
  
      // Calendar weekday headings
      const $gridWeekDays = h(
        'div',
        {
          staticClass: 'b-calendar-grid-weekdays row no-gutters border-bottom',
          attrs: { 'aria-hidden': 'true' }
        },
        this.calendarHeadings.map((d, idx) => {
          return h(
            'small',
            {
              key: idx,
              staticClass: 'col text-truncate',
              class: { 'text-muted': disabled },
              attrs: {
                title: d.label === d.text ? null : d.label,
                'aria-label': d.label
              }
            },
            d.text
          )
        })
      )
  
      // Calendar day grid
      let $gridBody = this.calendar.map(week => {
        const $cells = week.map((day, dIndex) => {
          const isSelected = day.ymd === selectedYMD
          const isActive = day.ymd === activeYMD
          const isToday = day.ymd === todayYMD
          const idCell = safeId(`_cell-${day.ymd}_`)
          // "fake" button
          const $btn = h(
            'span',
            {
              staticClass: 'btn border-0 rounded-circle text-nowrap',
              // Should we add some classes to signify if today/selected/etc?
              class: {
                // Give the fake button a focus ring
                focus: isActive && this.gridHasFocus,
                // Styling
                disabled: day.isDisabled || disabled,
                active: isSelected, // makes the button look "pressed"
                // Selected date style (need to computed from variant)
                [this.computedVariant]: isSelected,
                // Today day style (if not selected), same variant color as selected date
                [this.computedTodayVariant]:
                  isToday && highlightToday && !isSelected && day.isThisMonth,
                // Non selected/today styling
                'btn-outline-light': !(isToday && highlightToday) && !isSelected && !isActive,
                'btn-light': !(isToday && highlightToday) && !isSelected && isActive,
                // Text styling
                'text-muted': !day.isThisMonth && !isSelected,
                'text-dark':
                  !(isToday && highlightToday) && !isSelected && !isActive && day.isThisMonth,
                'font-weight-bold': (isSelected || day.isThisMonth) && !day.isDisabled
              },
              on: { click: () => this.onClickDay(day) }
            },
            day.day
          )
          return h(
            'div', // Cell with button
            {
              key: dIndex,
              staticClass: 'col p-0',
              class: day.isDisabled ? 'bg-light' : day.info.class || '',
              attrs: {
                id: idCell,
                role: 'button',
                'data-date': day.ymd, // Primarily for testing purposes
                // Only days in the month are presented as buttons to screen readers
                'aria-hidden': day.isThisMonth ? null : 'true',
                'aria-disabled': day.isDisabled || disabled ? 'true' : null,
                'aria-label': [
                  day.label,
                  isSelected ? `(${this.labelSelected})` : null,
                  isToday ? `(${this.labelToday})` : null
                ]
                  .filter(identity)
                  .join(' '),
                // NVDA doesn't convey `aria-selected`, but does `aria-current`,
                // ChromeVox doesn't convey `aria-current`, but does `aria-selected`,
                // so we set both attributes for robustness
                'aria-selected': isSelected ? 'true' : null,
                'aria-current': isSelected ? 'date' : null
              }
            },
            [$btn]
          )
        })
        // Return the week "row"
        // We use the first day of the weeks YMD value as a
        // key for efficient DOM patching / element re-use
        return h('div', { key: week[0].ymd, staticClass: 'row no-gutters' }, $cells)
      })
      $gridBody = h(
        'div',
        {
          // A key is only required on the body if we add in transition support
          // key: this.activeYMD.slice(0, -3),
          staticClass: 'b-calendar-grid-body',
          style: disabled ? { pointerEvents: 'none' } : {}
        },
        $gridBody
      )
  
      const $gridHelp = h(
        'footer',
        {
          staticClass: 'b-calendar-grid-help border-top small text-muted text-center bg-light',
          attrs: {
            id: gridHelpId
          }
        },
        [h('div', { staticClass: 'small' }, this.labelHelp)]
      )
  
      const $grid = h(
        'div',
        {
          ref: 'grid',
          staticClass: 'b-calendar-grid form-control h-auto text-center',
          attrs: {
            id: gridId,
            role: 'application',
            tabindex: noKeyNav ? '-1' : disabled ? null : '0',
            'data-month': activeYMD.slice(0, -3), // `YYYY-MM`, mainly for testing
            'aria-roledescription': this.labelCalendar || null,
            'aria-labelledby': gridCaptionId,
            'aria-describedby': gridHelpId,
            // `aria-readonly` is not considered valid on `role="application"`
            // https://www.w3.org/TR/wai-aria-1.1/#aria-readonly
            // 'aria-readonly': this.readonly && !disabled ? 'true' : null,
            'aria-disabled': disabled ? 'true' : null,
            'aria-activedescendant': activeId
          },
          on: {
            keydown: this.onKeydownGrid,
            focus: this.setGridFocusFlag,
            blur: this.setGridFocusFlag
          }
        },
        [$gridCaption, $gridWeekDays, $gridBody, $gridHelp]
      )
  
      // Optional bottom slot
      let $slot = this.normalizeSlot()
      $slot = $slot ? h('footer', { staticClass: 'b-calendar-footer' }, $slot) : h()
  
      const $widget = h(
        'div',
        {
          staticClass: 'b-calendar-inner',
          style: this.block ? {} : { width: this.width },
          attrs: {
            id: widgetId,
            dir: isRTL ? 'rtl' : 'ltr',
            lang: this.computedLocale || null,
            role: 'group',
            'aria-disabled': disabled ? 'true' : null,
            // If datepicker controls an input, this will specify the ID of the input
            'aria-controls': this.ariaControls || null,
            // This should be a prop (so it can be changed to Date picker, etc, localized
            'aria-roledescription': this.roleDescription || null,
            'aria-describedby': [
              // Should the attr (if present) go last?
              // Or should this attr be a prop?
              this.bvAttrs['aria-describedby'],
              valueId,
              gridHelpId
            ]
              .filter(identity)
              .join(' ')
          },
          on: {
            keydown: this.onKeydownWrapper
          }
        },
        [$header, $nav, $grid, $slot]
      )
  
      // Wrap in an outer div that can be styled
      return h('div', { staticClass: 'b-calendar', class: { 'd-block': this.block } }, [$widget])
    }
  })
  