@adyen/bento-mcp 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0 (2026-07-08)
4
+
5
+ ### Build System
6
+
7
+ - added NX implicit dependencies to the MCP ([5b8045faa](https://github.com/Adyen/bento/commit/5b8045faa))
8
+
9
+ ### ❤️ Thank You
10
+
11
+ - daver
12
+
13
+
3
14
  ## 0.7.0 (2026-06-23)
4
15
 
5
16
  ### Features
@@ -1 +1 @@
1
- export interface BentoCurrencyProps { /** * Currency code based on the ISO 4217 standard */ currency?: BentoCurrencyISOCode | `${BentoCurrencyISOCode}`; /** * Disable the default typography for the component. Only use this when the component is inside another typography component. */ disableTypography?: boolean; /** * Accepts currency value in major units instead of minor ones */ majorUnits?: boolean; /** * Currency value to be formatted */ value?: number; /** * Determines how the currency symbol and negative values are displayed. * This is only applicable to certain countries such as `en-US`. * * - `'standard'`: Default format (e.g., `-1,234.56 USD`). * - `'accounting'`: Uses accounting conventions, such as wrapping negative numbers in parentheses (e.g., `(1,234.56) USD`). * * @default 'standard' */ currencySign?: Intl.NumberFormatOptions['currencySign']; } export enum BentoCurrencyISOCode { AED = 'AED', AFA = 'AFA', ALL = 'ALL', AMD = 'AMD', ANG = 'ANG', AOA = 'AOA', ARS = 'ARS', AUD = 'AUD', AWG = 'AWG', AZN = 'AZN', BAM = 'BAM', BBD = 'BBD', BDT = 'BDT', BEF = 'BEF', BGN = 'BGN', BHD = 'BHD', BIF = 'BIF', BMD = 'BMD', BND = 'BND', BOB = 'BOB', BRL = 'BRL', BSD = 'BSD', BTC = 'BTC', BTN = 'BTN', BWP = 'BWP', BYR = 'BYR', BZD = 'BZD', CAD = 'CAD', CDF = 'CDF', CHF = 'CHF', CLF = 'CLF', CLP = 'CLP', CNY = 'CNY', COP = 'COP', CRC = 'CRC', CUC = 'CUC', CVE = 'CVE', CZK = 'CZK', DEM = 'DEM', DJF = 'DJF', DKK = 'DKK', DOP = 'DOP', DZD = 'DZD', EEK = 'EEK', EGP = 'EGP', ERN = 'ERN', ETB = 'ETB', EUR = 'EUR', FJD = 'FJD', FKP = 'FKP', GBP = 'GBP', GEL = 'GEL', GHS = 'GHS', GIP = 'GIP', GMD = 'GMD', GNF = 'GNF', GRD = 'GRD', GTQ = 'GTQ', GYD = 'GYD', HKD = 'HKD', HNL = 'HNL', HRK = 'HRK', HTG = 'HTG', HUF = 'HUF', IDR = 'IDR', ILS = 'ILS', INR = 'INR', IQD = 'IQD', IRR = 'IRR', ISK = 'ISK', ITL = 'ITL', JMD = 'JMD', JOD = 'JOD', JPY = 'JPY', KES = 'KES', KGS = 'KGS', KHR = 'KHR', KMF = 'KMF', KPW = 'KPW', KRW = 'KRW', KWD = 'KWD', KYD = 'KYD', KZT = 'KZT', LAK = 'LAK', LBP = 'LBP', LKR = 'LKR', LRD = 'LRD', LSL = 'LSL', LTC = 'LTC', LTL = 'LTL', LVL = 'LVL', LYD = 'LYD', MAD = 'MAD', MDL = 'MDL', MGA = 'MGA', MKD = 'MKD', MMK = 'MMK', MNT = 'MNT', MOP = 'MOP', MRO = 'MRO', MRU = 'MRU', MUR = 'MUR', MVR = 'MVR', MWK = 'MWK', MXN = 'MXN', MYR = 'MYR', MZM = 'MZM', NAD = 'NAD', NGN = 'NGN', NIO = 'NIO', NOK = 'NOK', NPR = 'NPR', NZD = 'NZD', OMR = 'OMR', PAB = 'PAB', PEN = 'PEN', PGK = 'PGK', PHP = 'PHP', PKR = 'PKR', PLN = 'PLN', PYG = 'PYG', QAR = 'QAR', RON = 'RON', RSD = 'RSD', RUB = 'RUB', RWF = 'RWF', SAR = 'SAR', SBD = 'SBD', SCR = 'SCR', SDG = 'SDG', SEK = 'SEK', SGD = 'SGD', SHP = 'SHP', SKK = 'SKK', SLL = 'SLL', SOS = 'SOS', SRD = 'SRD', SSP = 'SSP', STD = 'STD', SVC = 'SVC', SYP = 'SYP', SZL = 'SZL', THB = 'THB', TJS = 'TJS', TMT = 'TMT', TND = 'TND', TOP = 'TOP', TRY = 'TRY', TTD = 'TTD', TWD = 'TWD', TZS = 'TZS', UAH = 'UAH', UGX = 'UGX', USD = 'USD', UYU = 'UYU', UZS = 'UZS', VEF = 'VEF', VND = 'VND', VUV = 'VUV', WST = 'WST', XAF = 'XAF', XCD = 'XCD', XDR = 'XDR', XOF = 'XOF', XPF = 'XPF', YER = 'YER', ZAR = 'ZAR', ZMK = 'ZMK', ZWL = 'ZWL', }
1
+ export interface BentoCurrencyProps { /** * Currency code based on the ISO 4217 standard */ currency?: BentoCurrencyISOCode | `${BentoCurrencyISOCode}`; /** * Disable the default typography for the component. Only use this when the component is inside another typography component. */ disableTypography?: boolean; /** * Accepts currency value in major units instead of minor ones */ majorUnits?: boolean; /** * Currency value to be formatted */ value?: number; /** * Determines how the currency symbol and negative values are displayed. * This is only applicable to certain countries such as `en-US`. * * - `'standard'`: Default format (e.g., `-1,234.56 USD`). * - `'accounting'`: Uses accounting conventions, such as wrapping negative numbers in parentheses (e.g., `(1,234.56) USD`). * * @default 'standard' */ currencySign?: Intl.NumberFormatOptions['currencySign']; /** * Makes only the numeric part of the formatted currency bold. */ strongerAmount?: boolean; } export enum BentoCurrencyISOCode { AED = 'AED', AFA = 'AFA', ALL = 'ALL', AMD = 'AMD', ANG = 'ANG', AOA = 'AOA', ARS = 'ARS', AUD = 'AUD', AWG = 'AWG', AZN = 'AZN', BAM = 'BAM', BBD = 'BBD', BDT = 'BDT', BEF = 'BEF', BGN = 'BGN', BHD = 'BHD', BIF = 'BIF', BMD = 'BMD', BND = 'BND', BOB = 'BOB', BRL = 'BRL', BSD = 'BSD', BTC = 'BTC', BTN = 'BTN', BWP = 'BWP', BYR = 'BYR', BZD = 'BZD', CAD = 'CAD', CDF = 'CDF', CHF = 'CHF', CLF = 'CLF', CLP = 'CLP', CNY = 'CNY', COP = 'COP', CRC = 'CRC', CUC = 'CUC', CVE = 'CVE', CZK = 'CZK', DEM = 'DEM', DJF = 'DJF', DKK = 'DKK', DOP = 'DOP', DZD = 'DZD', EEK = 'EEK', EGP = 'EGP', ERN = 'ERN', ETB = 'ETB', EUR = 'EUR', FJD = 'FJD', FKP = 'FKP', GBP = 'GBP', GEL = 'GEL', GHS = 'GHS', GIP = 'GIP', GMD = 'GMD', GNF = 'GNF', GRD = 'GRD', GTQ = 'GTQ', GYD = 'GYD', HKD = 'HKD', HNL = 'HNL', HRK = 'HRK', HTG = 'HTG', HUF = 'HUF', IDR = 'IDR', ILS = 'ILS', INR = 'INR', IQD = 'IQD', IRR = 'IRR', ISK = 'ISK', ITL = 'ITL', JMD = 'JMD', JOD = 'JOD', JPY = 'JPY', KES = 'KES', KGS = 'KGS', KHR = 'KHR', KMF = 'KMF', KPW = 'KPW', KRW = 'KRW', KWD = 'KWD', KYD = 'KYD', KZT = 'KZT', LAK = 'LAK', LBP = 'LBP', LKR = 'LKR', LRD = 'LRD', LSL = 'LSL', LTC = 'LTC', LTL = 'LTL', LVL = 'LVL', LYD = 'LYD', MAD = 'MAD', MDL = 'MDL', MGA = 'MGA', MKD = 'MKD', MMK = 'MMK', MNT = 'MNT', MOP = 'MOP', MRO = 'MRO', MRU = 'MRU', MUR = 'MUR', MVR = 'MVR', MWK = 'MWK', MXN = 'MXN', MYR = 'MYR', MZM = 'MZM', NAD = 'NAD', NGN = 'NGN', NIO = 'NIO', NOK = 'NOK', NPR = 'NPR', NZD = 'NZD', OMR = 'OMR', PAB = 'PAB', PEN = 'PEN', PGK = 'PGK', PHP = 'PHP', PKR = 'PKR', PLN = 'PLN', PYG = 'PYG', QAR = 'QAR', RON = 'RON', RSD = 'RSD', RUB = 'RUB', RWF = 'RWF', SAR = 'SAR', SBD = 'SBD', SCR = 'SCR', SDG = 'SDG', SEK = 'SEK', SGD = 'SGD', SHP = 'SHP', SKK = 'SKK', SLL = 'SLL', SOS = 'SOS', SRD = 'SRD', SSP = 'SSP', STD = 'STD', SVC = 'SVC', SYP = 'SYP', SZL = 'SZL', THB = 'THB', TJS = 'TJS', TMT = 'TMT', TND = 'TND', TOP = 'TOP', TRY = 'TRY', TTD = 'TTD', TWD = 'TWD', TZS = 'TZS', UAH = 'UAH', UGX = 'UGX', USD = 'USD', UYU = 'UYU', UZS = 'UZS', VEF = 'VEF', VND = 'VND', VUV = 'VUV', WST = 'WST', XAF = 'XAF', XCD = 'XCD', XDR = 'XDR', XOF = 'XOF', XPF = 'XPF', YER = 'YER', ZAR = 'ZAR', ZMK = 'ZMK', ZWL = 'ZWL', }
@@ -1 +1 @@
1
- <template> <div class="b-currency"> <bento-typography v-if="!disableTypography" class="b-currency__amount" el="span" variant="body" strong> {{ formattedCurrency }} </bento-typography> <template v-else>{{ formattedCurrency }}</template> </div> </template> <script setup lang="ts"> import { computed, onMounted, useSlots } from 'vue'; import { BentoTypography } from '@/components/typography'; import { useBentoCurrency } from '@/composables/use-bento-currency/use-bento-currency'; import { deprecate } from '@/utils/ts/deprecate'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { BentoCurrencyISOCode, type BentoCurrencyProps } from './currency.types'; const slots = useSlots(); const props = withDefaults(defineProps<BentoCurrencyProps>(), { currency: BentoCurrencyISOCode.EUR, disableTypography: false, majorUnits: false, value: undefined, currencySign: 'standard', }); const { formatCurrency } = useBentoCurrency(); const formattedCurrency = computed(() => { const rawCurrencyValue = props.value ?? getSlotText(slots)('default'); return formatCurrency(rawCurrencyValue, { currency: props.currency, currencySign: props.currencySign, majorUnits: props.majorUnits, }); }); onMounted(() => { if (getSlotText(slots)('default')) { deprecate( 'BentoCurrency "default" slot', `The use of "default" slot in "BentoCurrency" is no longer supported as it causes issues with dynamic content. Use the "value" property instead. <bento-currency :value="12345.67" currency="USD" major-units="false" />`, '2.0.0' ); } }); </script> <script lang="ts"> /** * A Currency component is used to display a formatted currency value. * * @example * import { BentoCurrency, BentoCurrencyISOCode } from '@adyen/bento-vue2'; * * export default { * components: { BentoCurrency }, * template: ` * <bento-currency * :currency="BentoCurrencyISOCode.USD" * :value="76.33" * /> * ` * } */ export default { name: 'bento-currency', i18n: {}, // required to enable E2E to run as they do not have vue-i18n set up }; </script> <style lang="scss" scoped src="./currency.scss" />
1
+ <template> <div class="b-currency" :aria-label="formattedCurrencyParts.formattedCurrency"> <bento-typography v-if="!disableTypography" class="b-currency__amount" el="span" variant="body" strong> <bento-typography el="span" :strongest="strongerAmount">{{ formattedCurrencyParts.currencyNumber }}</bento-typography> <span>{{ formattedCurrencyCode }}</span> </bento-typography> <template v-else>{{ formattedCurrencyParts.formattedCurrency }}</template> </div> </template> <script setup lang="ts"> import { computed, onMounted, useSlots } from 'vue'; import { BentoTypography } from '@/components/typography'; import { useBentoCurrency } from '@/composables/use-bento-currency/use-bento-currency'; import { deprecate } from '@/utils/ts/deprecate'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { BentoCurrencyISOCode, type BentoCurrencyProps } from './currency.types'; const slots = useSlots(); const props = withDefaults(defineProps<BentoCurrencyProps>(), { currency: BentoCurrencyISOCode.EUR, disableTypography: false, majorUnits: false, value: undefined, currencySign: 'standard', }); const { formatCurrencyParts } = useBentoCurrency(); const formattedCurrencyParts = computed(() => { const rawCurrencyValue = props.value ?? getSlotText(slots)('default'); return formatCurrencyParts(rawCurrencyValue, { currency: props.currency, currencySign: props.currencySign, majorUnits: props.majorUnits, }); }); const formattedCurrencyCode = computed(() => `\u00A0${formattedCurrencyParts.value.currencyCode}`); onMounted(() => { if (getSlotText(slots)('default')) { deprecate( 'BentoCurrency "default" slot', `The use of "default" slot in "BentoCurrency" is no longer supported as it causes issues with dynamic content. Use the "value" property instead. <bento-currency :value="12345.67" currency="USD" major-units="false" />`, '2.0.0' ); } }); </script> <script lang="ts"> /** * A Currency component is used to display a formatted currency value. * * @example * import { BentoCurrency, BentoCurrencyISOCode } from '@adyen/bento-vue2'; * * export default { * components: { BentoCurrency }, * template: ` * <bento-currency * :currency="BentoCurrencyISOCode.USD" * :value="76.33" * /> * ` * } */ export default { name: 'bento-currency', i18n: {}, // required to enable E2E to run as they do not have vue-i18n set up }; </script> <style lang="scss" scoped src="./currency.scss" />
@@ -1 +1 @@
1
- <template> <div class="b-date-range-picker-calendar"> <!-- Form --> <div class="b-date-range-picker-calendar__form-container" :class="computedFormContainerClasses" data-testid="date-range-picker-calendar-form-container" > <div class="b-date-range-picker-calendar__form"> <template v-if="hasSlot('title')"> <slot name="title" /> </template> <bento-dropdown v-if="quickSelectRanges" :aria-label="t('customRange')" :items="quickSelectRangeDefaultItems" :model-value="selectedRangeSelectorValue" @update:model-value="onRangeSelectorInput" /> <bento-segmented-control v-if="granularities" :items="granularityItems" :model-value="internalRangeDate.granularity" full-width @update:model-value="onGranularityInput" > </bento-segmented-control> <bento-alert v-if="maxRangeMessage" variant="tip"> <template #description> {{ maxRangeMessage }} </template> </bento-alert> <div> <bento-typography stronger el="span" class="b-date-range-picker-calendar__label"> From </bento-typography> <date-time-input :date-model-value="startDateValue" :date-props="{ ariaLabel: t('dateFrom'), type: dateInputType, description: !isTouchDevice ? t(dateFormat) : undefined, errorMessage: startDateErrorText, }" :error-message="startSharedError" :show-time-input="allowTimeInput" :time-model-value="dateTextInput.startTime" :time-props="{ ariaLabel: t('timeFrom'), errorMessage: startSharedError, }" class="b-date-range-picker-calendar__form-input" @update:date-model-value="onStartDateInput" @date:keydown="onDateInputKeyDown" @update:time-model-value="onStartTimeInput" @time:error="onStartTimeError" @time:valid="onStartTimeValid" /> </div> <div> <bento-typography stronger el="span" class="b-date-range-picker-calendar__label"> To </bento-typography> <date-time-input :date-model-value="endDateValue" :date-props="{ ariaLabel: t('dateTo'), type: dateInputType, description: !isTouchDevice ? t(dateFormat) : undefined, errorMessage: endDateErrorText, }" :error-message="endSharedError" :show-time-input="allowTimeInput" :time-model-value="dateTextInput.endTime" :time-props="{ ariaLabel: t('timeTo'), errorMessage: endSharedError, }" class="b-date-range-picker-calendar__form-input" @update:date-model-value="onEndDateInput" @date:keydown="onDateInputKeyDown" @update:time-model-value="onEndTimeInput" @time:error="onEndTimeError" @time:valid="onEndTimeValid" /> </div> </div> <div v-if="hasSlot('actions')"> <slot name="actions" /> </div> </div> <!-- Calendar --> <div v-if="!hasNoCalendars && !isTouchDevice" class="b-date-range-picker-calendar__calendars-container" data-testid="date-range-picker-calendar-calendar-container" > <calendar :value="internalRangeDate" :first-day-of-week="firstDayOfWeek" :is-date-disabled="isDateDisabled" :max-range="maxRange" :min="computedMinMax.min" :max="computedMinMax.max" :number-of-months="numberOfMonths" :show-end-date-on-open="showEndDateOnOpen" :granularity="selectedGranularity" :variant="variant" :year-range="yearRange" is-range @input="onDateInput" @start-date-selected="onStartDateSelected" @end-date-selected="onEndDateSelected" /> </div> </div> </template> <script setup lang="ts"> import { computed, onMounted, type PropType, reactive, ref, toRaw, useSlots } from 'vue'; import { Calendar, type CalendarGranularityType } from '@/internal'; import { DateTimeInput } from '../date-time-input'; import { BentoAlert } from '@/components/alert'; import { BentoTypography } from '@/components/typography'; import BentoDropdown from '@/components/dropdown/dropdown.vue'; import { BentoSegmentedControl, type BentoSegmentedControlItem } from '@/components/segmented-control'; import { useI18n } from '@/utils/ts/i18n'; import { debounce } from '@/utils/ts/debounce'; import { useDateInputFormatter, useHasSlot, useTouchDevice } from '@/composables'; import { useGranularityAdjustments } from '@/components/internal/calendar/composables/use-granularity-adjustments'; import { useGranularMinMaxDate } from '@/components/internal/calendar/components/calendar-month/composables/granular-min-max-date'; import { useGranularityMemory } from '../../composables/use-granularity-memory'; import { DateRangePickerCalendarEvent, type DateRangePickerCalendarFormData, type DateRangePickerCalendarRangeSelectorItems, } from './date-range-picker-calendar.types'; import type { BentoDateRangePickerGranularityConfig, BentoDateRangePickerValue, } from '../../date-range-picker.types'; import { endOfDay } from 'date-fns/endOfDay'; import { startOfDay } from 'date-fns/startOfDay'; import { isSameSecond } from 'date-fns/isSameSecond'; import { isSameDay } from 'date-fns/isSameDay'; import { isEqual } from 'date-fns/isEqual'; import { isToday } from 'date-fns/isToday'; import { setMilliseconds } from 'date-fns/setMilliseconds'; import { dateToNativeDateString, dateToNativeMonthString, dateToTimeInputString, parseNativeMonthString, } from '@/utils/ts/format-date/format-date'; import { isValidTimeString } from '@/utils/ts/time-input'; import { setTimeInDateObject } from '@/utils/ts/format-date'; import messages from './messages.json'; const FORM_INPUT_DEBOUNCE_TIME = 300; const RANGE_SELECTOR_CUSTOM_RANGE_KEY = 'customRange'; type MessageSchema = (typeof messages)['en-US']; const maxRangeMessageKeyMap: Record<CalendarGranularityType, keyof MessageSchema> = { daily: 'maxRangeDays', weekly: 'maxRangeWeeks', monthly: 'maxRangeMonths', quarterly: 'maxRangeQuarters', }; const props = defineProps({ /** * Allows user to enter time values in the form */ allowTimeInput: { type: Boolean, default: false }, /** * Ranges form persistance data */ dateFormData: { type: Object as PropType<DateRangePickerCalendarFormData>, default: () => ({ startDate: undefined, endDate: undefined, startTime: undefined, endTime: undefined }), }, /** * Allows you to set the first day of the week. 0 is sunday, 1 is monday, 6 is saturday */ firstDayOfWeek: Calendar.props.firstDayOfWeek, /** * A list of available granularities. * If present, the date picker will display a segmented control to change granularity. */ granularities: { type: Array as PropType<Array<BentoDateRangePickerGranularityConfig>>, default: null, }, /** * Indicate if a date should be disabled or not */ isDateDisabled: Calendar.props.isDateDisabled, /** * Set a maximum number of dates to be selectable by the range. */ maxRange: Calendar.props.maxRange, /** * Sets a minimum limit for selectable dates - dates before `min` will not be selectable */ min: Calendar.props.min, /** * Sets a maximum limit for selectable dates - dates after `max` will not be selectable */ max: Calendar.props.max, /** * Number of months rendered on pane */ numberOfMonths: { type: Calendar.props.numberOfMonths.type, default: Calendar.props.numberOfMonths.default, validator: (n: number) => n >= 0, }, /** * Displays the end date's month when the calendar is opened. Defaults to false i.e. the start date's month is displayed. */ showEndDateOnOpen: { type: Boolean, default: false }, /** * Enables the custom range selector. * If provided, must be an array that sets the custom range dropdown items. Items are of the structure: * `label` - label of custom range item. * `value` - a unique key of the custom range item. * `data` - an object `{ startDate: Date; endDate: Date }` to set the date picker range to upon selecting. */ quickSelectRanges: { type: Array as PropType<DateRangePickerCalendarRangeSelectorItems>, default: undefined, }, /** * Selected date */ value: { type: Object as PropType<BentoDateRangePickerValue>, default: undefined }, /** * The type of calendar to display. Defaults to showing days. */ variant: Calendar.props.variant, /** * Range of years to be selectable from the year dropdown */ yearRange: Calendar.props.yearRange, }); const emit = defineEmits([ DateRangePickerCalendarEvent.CUSTOM_RANGE, DateRangePickerCalendarEvent.INPUT, DateRangePickerCalendarEvent.ERROR, DateRangePickerCalendarEvent.FORM_DATE, DateRangePickerCalendarEvent.START_DATE_SELECTED, ]); const slots = useSlots(); const hasSlot = useHasSlot(slots); const { t, tc } = useI18n<{ message: MessageSchema }>({ messages }); const { isTouchDevice } = useTouchDevice(); const { dateFormat, isFormatValid, parseDate, onKeyDown, autoFormat, formatDate: formatDateUtil, } = useDateInputFormatter(); const calculatedIsRelativeToNowQuickSelectRanges = ref({}); const defaultGranularity = computed(() => (props.variant === 'month' ? 'monthly' : 'daily')); const hasNoCalendars = computed(() => props.numberOfMonths === 0); const internalRangeDate = reactive<BentoDateRangePickerValue>({ startDate: props.value?.startDate, endDate: props.value?.endDate, range: props.value?.range, ...(props.granularities ? { granularity: props.value?.granularity || defaultGranularity.value, } : {}), }); /** * Currently range selector dropdown item that is selected. * The logic will try and find in each range item: * - If an endDate does not exist, then only check if the startDate day matches and if today's day matches. * - if time input has been enabled, then check if the seconds match. * - if an endDate does exist then just check if the startDate day and the endDate day match. * * Note: All date comparisons use local time consistently. Both sides of each comparison * originate from the same timezone context, so no UTC normalization is needed. */ const selectedRangeSelectorValue = computed(() => { return ( // eslint-disable-next-line consistent-return props?.quickSelectRanges?.find(({ value, data }) => { if (!data.endDate) { // Check if this quick select range is explicitly marked as "relative to now" if (data.isRelativeToNow) { // Retrieve previously calculated start/end dates for this relative range. // We use these if the user has already clicked on this time of quick range const newlyCalculatedQuickSelectDates = calculatedIsRelativeToNowQuickSelectRanges.value?.[value]; return newlyCalculatedQuickSelectDates ? isEqual(newlyCalculatedQuickSelectDates.startDate, internalRangeDate.startDate) && isEqual(newlyCalculatedQuickSelectDates.endDate, internalRangeDate.endDate) : // If not stored calculated dates, fall back to comparing the quick select item's value // with the internal range's stored range identifier. value === internalRangeDate.range; } // This block handles quick select ranges that do not have an endDate // AND are NOT explicitly marked as 'isRelativeToNow'. // This means the endDate will be today at the end of the day. // Uses isEqual for startDate to prevent false matches (e.g. same-day selection // accidentally matching a "This week" range). When time input is enabled, // also verify the end time is at end of day so that manual time changes // correctly fall back to "Custom range". return ( isEqual(data.startDate, internalRangeDate.startDate) && isToday(internalRangeDate.endDate) && (!props.allowTimeInput || isSameSecond(internalRangeDate.endDate, endOfDay(internalRangeDate.endDate))) && (props.granularities ? data.granularity === internalRangeDate.granularity : true) ); } if (props.allowTimeInput) { const res = isSameSecond(data.startDate, internalRangeDate.startDate) && isSameSecond(data.endDate, internalRangeDate.endDate) && (props.granularities ? data.granularity === internalRangeDate.granularity : true); return res; } if (data.endDate) { return ( isSameDay(data.startDate, internalRangeDate.startDate) && isSameDay(data.endDate, internalRangeDate.endDate) && (props.granularities ? data.granularity === internalRangeDate.granularity : true) ); } })?.value ?? RANGE_SELECTOR_CUSTOM_RANGE_KEY ); }); const quickSelectRangeDefaultItems = computed(() => [ { label: t('customRange'), value: RANGE_SELECTOR_CUSTOM_RANGE_KEY }, ...(props.quickSelectRanges ? props.quickSelectRanges : []), ]); const selectedGranularity = computed( () => props.granularities?.find(({ type }) => type === internalRangeDate.granularity) || null ); const granularityItems = computed<Array<BentoSegmentedControlItem>>( () => props.granularities?.map(({ type, disabled }) => ({ label: t(type), value: type, disabled, })) ?? [] ); const { getGranularCalendarLimits } = useGranularMinMaxDate(props); const computedMinMax = computed(() => props.granularities ? getGranularCalendarLimits(selectedGranularity.value) : { min: props.min ? startOfDay(props.min) : null, max: props.max ? endOfDay(props.max) : null } ); const { adjustDateForQuarterly, adjustDateForMonthly, adjustDateForWeekly, adjustStartDateInputForWeekly, adjustEndDateInputForWeekly, adjustStartDateInputForMonthly, adjustEndDateInputForMonthly, adjustStartDateInputForQuarterly, adjustEndDateInputForQuarterly, } = useGranularityAdjustments(props.firstDayOfWeek); const { savedRanges, saveRange, resetSavedRanges } = useGranularityMemory(); /** * Format date to a string value in current format * @param dateToFormat - Date to be formatted */ const formatDate = (dateToFormat?: Date) => { if (!dateToFormat) { return ''; } return formatDateUtil(dateToFormat); }; // Form error keys const startDateError = ref<keyof MessageSchema | null>(null); const endDateError = ref<keyof MessageSchema | null>(null); // Translated error messages for the template const startDateErrorText = computed(() => (startDateError.value ? t(startDateError.value) : null)); const endDateErrorText = computed(() => (endDateError.value ? t(endDateError.value) : null)); // Shared errors exclude format errors since those only concern the date input const startSharedError = computed(() => startDateError.value === 'invalidDateFormat' ? null : startDateErrorText.value ); const endSharedError = computed(() => (endDateError.value === 'invalidDateFormat' ? null : endDateErrorText.value)); /** * Returns a translated error message for a given date string, or null if valid. * @param dateText The date string to validate. * @param counterpartDate Optional date to compare against for range order validation. * @param role Whether the date being validated is the 'start' or 'end' of the range. */ const getDateErrorMessage = ( dateText: string, counterpartDate?: Date, role?: 'start' | 'end', resolvedDateTime?: Date ): keyof MessageSchema | null => { if (!dateText || !isFormatValid(dateText)) { return 'invalidDateFormat'; } const date = parseDate(dateText); const dayDate = startOfDay(date); if ((props.min && dayDate < startOfDay(props.min)) || (props.max && dayDate > startOfDay(props.max))) { return 'selectedDateIsNotAvailable'; } if (props.isDateDisabled && props.isDateDisabled(dayDate)) { return 'selectedDateIsNotAvailable'; } if (counterpartDate) { // When time input is enabled and we have a resolved datetime, compare full datetimes; // otherwise fall back to day-level comparison. const current = resolvedDateTime ? setMilliseconds(resolvedDateTime, 0).getTime() : dayDate.getTime(); const counterpart = resolvedDateTime ? setMilliseconds(counterpartDate, 0).getTime() : startOfDay(counterpartDate).getTime(); const isInvalid = role === 'start' ? current >= counterpart : current <= counterpart; if (isInvalid) { return 'startDateMustPrecedeEndDate'; } } return null; }; // Form input text used in range variant const dateTextInput = reactive({ startDate: props.dateFormData.startDate, endDate: props.dateFormData.endDate, startTime: props.dateFormData.startTime, endTime: props.dateFormData.endTime, }); /** * Checks if a time string is valid and within the min/max bounds if a date is provided. * @param timeText The time string to validate. * @param date The date string to check the time against. * @returns True if the time is valid, false otherwise. */ const isValidTime = (timeText: string, date?: string) => { const isTimeAllowed = () => { if (date && isValidDate(date)) { const dateToCheckWithTime = setTimeInDateObject(parseDate(date), timeText); // Check if date is between min/max bounds, if there are any if ( (props.min && dateToCheckWithTime < setMilliseconds(props.min, 0)) || (props.max && dateToCheckWithTime > setMilliseconds(props.max, 0)) ) { return false; } } return true; }; return isValidTimeString(timeText) && isTimeAllowed(); }; const isMonthVariant = computed(() => props.variant === 'month'); const formatNativeDate = (date: Date): string => { if (!date) { return ''; } return isMonthVariant.value ? dateToNativeMonthString(date) : dateToNativeDateString(date); }; const nativeStartDate = computed(() => formatNativeDate(internalRangeDate.startDate)); const nativeEndDate = computed(() => formatNativeDate(internalRangeDate.endDate)); const dateInputType = computed(() => (isTouchDevice.value ? (isMonthVariant.value ? 'month' : 'date') : 'text')); const startDateValue = computed(() => (isTouchDevice.value ? nativeStartDate.value : dateTextInput.startDate)); const endDateValue = computed(() => (isTouchDevice.value ? nativeEndDate.value : dateTextInput.endDate)); /** * Sets the time in a date object if the time is valid and the variant is not 'month'. * @param date The date object to modify. * @param time The time string to set. * @returns The modified date object or the original if time is not set. */ const setTimeInDateRef = (date: Date, time: string) => { // Do not set time if month variant if (!time || !isValidTimeString(time) || props.variant === 'month') { return date; } return setTimeInDateObject(new Date(date), time); }; const onDateInput = (selectedRange: BentoDateRangePickerValue) => { dateTextInput.startTime = dateToTimeInputString( props.min && props.allowTimeInput && isSameDay(props.min, selectedRange.startDate) ? props.min : startOfDay(selectedRange.startDate) ); dateTextInput.endTime = dateToTimeInputString( props.max && props.allowTimeInput && isSameDay(props.max, selectedRange.endDate) ? props.max : endOfDay(selectedRange.endDate) ); internalRangeDate.startDate = setTimeInDateRef(selectedRange.startDate, dateTextInput.startTime); internalRangeDate.endDate = setTimeInDateRef(selectedRange.endDate, dateTextInput.endTime); // Resetting internal range date when selecting a custom range through the calendar internalRangeDate.range = null; if (props.granularities) { // Reset saved ranges on input resetSavedRanges(); } emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); }; /** * Checks if a date string is in current format and if the date is enabled. * @param dateText The date string to validate. * @returns True if the date is valid and enabled, false otherwise. */ const isValidDate = (dateText: string): boolean => !!dateText && !getDateErrorMessage(dateText); /** * When mounted, set the input error states to true if the values are not valid */ onMounted(() => { // Start date validation startDateError.value = props.dateFormData.startDate ? getDateErrorMessage(props.dateFormData.startDate) : null; // Start time validation is delegated to bento-input-time via events // End date validation endDateError.value = props.dateFormData.endDate ? getDateErrorMessage(props.dateFormData.endDate) : null; // End time validation is delegated to bento-input-time via events }); const onStartDateSelected = (newStartDate: Date) => { dateTextInput.startDate = formatDate(newStartDate); if (!dateTextInput.startTime) { // Set the time to the beginning of the day dateTextInput.startTime = dateToTimeInputString(newStartDate); } startDateError.value = null; if (endDateError.value === 'startDateMustPrecedeEndDate') { endDateError.value = null; } emit(DateRangePickerCalendarEvent.START_DATE_SELECTED, newStartDate); }; const onEndDateSelected = (newEndDate: Date) => { dateTextInput.endDate = formatDate(newEndDate); if (!dateTextInput.endTime) { // Set the time to the end of the day dateTextInput.endTime = dateToTimeInputString(endOfDay(newEndDate)); } endDateError.value = null; if (startDateError.value === 'startDateMustPrecedeEndDate') { startDateError.value = null; } }; const getDate = (keyName: 'startDate' | 'endDate') => { const dateProp = props.value[keyName]; if (dateProp) { return dateProp; } if (props.dateFormData[keyName] && isValidDate(props.dateFormData[keyName])) { return parseDate(props.dateFormData[keyName]); } return undefined; }; const validateStartDate = (startDateText: string) => { if (isValidDate(startDateText)) { let dateText = startDateText; startDateError.value = null; switch (selectedGranularity.value?.type) { case 'weekly': dateText = adjustStartDateInputForWeekly( startDateText, formatDate, computedMinMax.value.min, computedMinMax.value.max ); dateTextInput.startDate = dateText; break; case 'monthly': dateText = adjustStartDateInputForMonthly( startDateText, formatDate, computedMinMax.value.min, computedMinMax.value.max ); dateTextInput.startDate = dateText; break; case 'quarterly': dateText = adjustStartDateInputForQuarterly( startDateText, formatDate, computedMinMax.value.min, computedMinMax.value.max ); dateTextInput.startDate = dateText; break; case 'daily': default: break; } const startDate = startOfDay(parseDate(dateText)); internalRangeDate.startDate = setTimeInDateRef(startDate, dateTextInput.startTime); if (isValidDate(dateTextInput.endDate)) { internalRangeDate.endDate = setTimeInDateRef( startOfDay(parseDate(dateTextInput.endDate)), dateTextInput.endTime ); } const endDate = getDate('endDate'); const rangeError = getDateErrorMessage( dateText, props.allowTimeInput ? internalRangeDate.endDate : endDate, 'start', props.allowTimeInput ? internalRangeDate.startDate : undefined ); if (rangeError) { startDateError.value = rangeError; emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); return; } if (endDateError.value === 'startDateMustPrecedeEndDate') { endDateError.value = null; } // Resetting internal range date when selecting a custom range through the calendar internalRangeDate.range = null; emit(DateRangePickerCalendarEvent.FORM_DATE, { startDate, endDate, }); emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); } else { // Indicate form error startDateError.value = getDateErrorMessage(startDateText); // Emit Date picker calendar error emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); } }; const validateStartDateDebounced = debounce(validateStartDate, FORM_INPUT_DEBOUNCE_TIME); const onStartDateInput = (startDateText: string) => { // Set the date as string when input through the form // Bypassing autoFormat for native month input since it doesn't support YYYY-MM dateTextInput.startDate = isTouchDevice.value && isMonthVariant.value ? startDateText : autoFormat(startDateText); if (isTouchDevice.value) { if (isMonthVariant.value && startDateText) { const startDate = parseNativeMonthString(startDateText); internalRangeDate.startDate = setTimeInDateRef(startDate, dateTextInput.startTime); internalRangeDate.range = null; emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); } else if (dateTextInput.startDate) { validateStartDate(dateTextInput.startDate); } else { // on touch devices, skip the any intermediate values e.g. pressing zero first startDateError.value = null; } } else { validateStartDateDebounced(dateTextInput.startDate); } }; const validateEndDate = (endDateText: string) => { if (isValidDate(endDateText)) { let dateText = endDateText; endDateError.value = null; switch (selectedGranularity.value?.type) { case 'weekly': dateText = adjustEndDateInputForWeekly( endDateText, formatDate, computedMinMax.value.min, computedMinMax.value.max ); dateTextInput.endDate = dateText; break; case 'monthly': dateText = adjustEndDateInputForMonthly( endDateText, formatDate, computedMinMax.value.min, computedMinMax.value.max ); dateTextInput.endDate = dateText; break; case 'quarterly': dateText = adjustEndDateInputForQuarterly( endDateText, formatDate, computedMinMax.value.min, computedMinMax.value.max ); dateTextInput.endDate = dateText; break; case 'daily': default: break; } // Format date string to Date object const endDate = startOfDay(parseDate(dateText)); internalRangeDate.endDate = setTimeInDateRef(endDate, dateTextInput.endTime); if (isValidDate(dateTextInput.startDate)) { internalRangeDate.startDate = setTimeInDateRef( startOfDay(parseDate(dateTextInput.startDate)), dateTextInput.startTime ); } const startDate = getDate('startDate'); const rangeError = getDateErrorMessage( dateText, props.allowTimeInput ? internalRangeDate.startDate : startDate, 'end', props.allowTimeInput ? internalRangeDate.endDate : undefined ); if (rangeError) { endDateError.value = rangeError; emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); return; } if (startDateError.value === 'startDateMustPrecedeEndDate') { startDateError.value = null; } // Resetting internal range date when selecting a custom range through the calendar internalRangeDate.range = null; emit(DateRangePickerCalendarEvent.FORM_DATE, { startDate, endDate, }); emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); } else { // Indicate form error endDateError.value = getDateErrorMessage(endDateText); // Emit Date picker calendar error emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); } }; const validateEndDateDebounced = debounce(validateEndDate, FORM_INPUT_DEBOUNCE_TIME); const onEndDateInput = (endDateText: string) => { // Set the date as string when input through the form // Bypassing autoFormat for native month input since it doesn't support YYYY-MM dateTextInput.endDate = isTouchDevice.value && isMonthVariant.value ? endDateText : autoFormat(endDateText); if (isTouchDevice.value) { if (isMonthVariant.value && endDateText) { const endDate = parseNativeMonthString(endDateText); internalRangeDate.endDate = setTimeInDateRef(endDate, dateTextInput.endTime); internalRangeDate.range = null; emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); } else if (dateTextInput.endDate) { validateEndDate(dateTextInput.endDate); } else { // on touch devices, skip the any intermediate values e.g. pressing zero first endDateError.value = null; } } else { validateEndDateDebounced(dateTextInput.endDate); } }; const onDateInputKeyDown = (event: KeyboardEvent) => { onKeyDown(event); }; const onStartTimeError = () => { emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); }; const onStartTimeValid = () => { const startTimeText = dateTextInput.startTime; if (!isValidTime(startTimeText, dateTextInput.startDate)) { emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); return; } internalRangeDate.startDate = setTimeInDateRef(internalRangeDate.startDate, startTimeText); // Validate range order with updated time if (dateTextInput.startDate && internalRangeDate.endDate) { const rangeError = getDateErrorMessage( dateTextInput.startDate, internalRangeDate.endDate, 'start', internalRangeDate.startDate ); if (rangeError) { startDateError.value = rangeError; emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); return; } } startDateError.value = null; if (endDateError.value === 'startDateMustPrecedeEndDate') { endDateError.value = null; } const endDate = getDate('endDate'); emit(DateRangePickerCalendarEvent.FORM_DATE, { startDate: internalRangeDate.startDate, endDate, startTime: startTimeText, endTime: dateTextInput.endTime ?? props.dateFormData.endTime, }); emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); }; const onEndTimeError = () => { emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); }; const onEndTimeValid = () => { const endTimeText = dateTextInput.endTime; if (!isValidTime(endTimeText, dateTextInput.endDate)) { emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); return; } internalRangeDate.endDate = setTimeInDateRef(internalRangeDate.endDate, endTimeText); // Validate range order with updated time if (dateTextInput.endDate && internalRangeDate.startDate) { const rangeError = getDateErrorMessage( dateTextInput.endDate, internalRangeDate.startDate, 'end', internalRangeDate.endDate ); if (rangeError) { endDateError.value = rangeError; emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); return; } } endDateError.value = null; if (startDateError.value === 'startDateMustPrecedeEndDate') { startDateError.value = null; } emit(DateRangePickerCalendarEvent.FORM_DATE, { startDate: internalRangeDate.startDate, endDate: internalRangeDate.endDate, startTime: dateTextInput.startTime ?? props.dateFormData.startTime, endTime: endTimeText, }); emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); }; const onStartTimeInput = (startTimeText: string) => { dateTextInput.startTime = startTimeText; }; const onEndTimeInput = (endTimeText: string) => { dateTextInput.endTime = endTimeText; }; const setInternalRange = ( startDate: Date, endDate: Date, granularity?: CalendarGranularityType, range?: string ) => { internalRangeDate.startDate = startDate; internalRangeDate.endDate = endDate; if (granularity) { internalRangeDate.granularity = granularity; } if (range) { internalRangeDate.range = range; } }; const onRangeSelectorInput = (newCustomRangeValue: string) => { const foundResult = props.quickSelectRanges?.find(({ value }) => value === newCustomRangeValue); if (foundResult) { resetSavedRanges(); const date = foundResult.data; const newEndDate = date.endDate || (date.isRelativeToNow ? new Date(Date.now()) : endOfDay(new Date(Date.now()))); // If the time difference between start and end dates is available, // calculating start date dynamically const newStartDate = date.isRelativeToNow && date.timeDifference ? new Date(newEndDate.getTime() - date.timeDifference) : date.startDate; // Set Form input Dates dateTextInput.endDate = formatDate(newEndDate); dateTextInput.startDate = formatDate(newStartDate); setInternalRange(newStartDate, newEndDate, null, newCustomRangeValue); if (props.granularities) { internalRangeDate.granularity = date.granularity || defaultGranularity.value; } // Set Form input time to quick select range time dateTextInput.startTime = dateToTimeInputString(newStartDate); dateTextInput.endTime = dateToTimeInputString(newEndDate); // Set calendar selection internalRangeDate.startDate = newStartDate; internalRangeDate.endDate = newEndDate; if (date.isRelativeToNow) { // Store this calculated start and end dates to be used in the selectedRangeSelectorValue logic calculatedIsRelativeToNowQuickSelectRanges.value = { ...calculatedIsRelativeToNowQuickSelectRanges.value, [newCustomRangeValue]: { startDate: newStartDate, endDate: newEndDate, }, }; } // Emit everytime the date range changes emit(DateRangePickerCalendarEvent.CUSTOM_RANGE, { ...foundResult.data, startDate: newStartDate, endDate: newEndDate, range: foundResult.value, }); } }; const adjustDate = { weekly: adjustDateForWeekly, monthly: adjustDateForMonthly, quarterly: adjustDateForQuarterly, }; const onGranularityInput = (value: CalendarGranularityType) => { const previousValue = internalRangeDate.granularity; internalRangeDate.granularity = value; // Save previously selected range if (previousValue) { saveRange(previousValue, structuredClone(toRaw(internalRangeDate))); } if (internalRangeDate.startDate && internalRangeDate.endDate) { // Adjust date range based on new granularity switch (value) { case 'quarterly': case 'monthly': case 'weekly': { if (savedRanges[value]) { setInternalRange(savedRanges[value].startDate, savedRanges[value].endDate, value); break; } const { startDate, endDate } = adjustDate[value]( internalRangeDate.startDate, internalRangeDate.endDate, computedMinMax.value.min, computedMinMax.value.max ); setInternalRange(startDate, endDate, value); break; } case 'daily': if (savedRanges.daily) { setInternalRange(savedRanges.daily.startDate, savedRanges.daily.endDate, value); } break; default: break; } dateTextInput.startDate = formatDate(internalRangeDate.startDate); dateTextInput.endDate = formatDate(internalRangeDate.endDate); emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); } }; const computedFormContainerClasses = computed(() => ({ 'b-date-range-picker-calendar__form-container--no-calendars': hasNoCalendars.value, })); const maxRangeMessage = computed(() => { const granularity = selectedGranularity.value; const count = granularity?.maxRange || props.maxRange; if (!count) { return null; } const key = maxRangeMessageKeyMap[granularity?.type ?? 'daily']; return tc(key, count, { count }); }); </script> <script lang="ts"> /** * Calendar for range dates. Contains the logic to handle the range forms. * * @example * import DatePickerCalendarRange from './date-range-picker-calendar.vue'; * * export default { * components: { DatePickerCalendarRange }, * template: ` * <date-range-picker-calendar * :value="{startDate: new Date(), endDate: new Date() }" * :first-day-of-week="BentoDatePickerFirstDayOfWeek.MONDAY" * :is-date-disabled="(date: Date) => boolean" * :number-of-months="2" * @input="({ startDate, endDate }) => void" * /> * ` * } */ export default { name: 'date-range-picker-calendar', i18n: { messages }, }; </script> <style lang="scss" scoped src="./date-range-picker-calendar.scss" />
1
+ <template> <div class="b-date-range-picker-calendar"> <!-- Form --> <div class="b-date-range-picker-calendar__form-container" :class="computedFormContainerClasses" data-testid="date-range-picker-calendar-form-container" > <div class="b-date-range-picker-calendar__form"> <template v-if="hasSlot('title')"> <slot name="title" /> </template> <bento-dropdown v-if="quickSelectRanges" :aria-label="t('customRange')" :items="quickSelectRangeDefaultItems" :model-value="selectedRangeSelectorValue" @update:model-value="onRangeSelectorInput" /> <bento-segmented-control v-if="granularities" :items="granularityItems" :model-value="internalRangeDate.granularity" full-width @update:model-value="onGranularityInput" > </bento-segmented-control> <bento-alert v-if="maxRangeMessage" variant="tip"> <template #description> {{ maxRangeMessage }} </template> </bento-alert> <div> <bento-typography stronger el="span" class="b-date-range-picker-calendar__label"> From </bento-typography> <date-time-input :date-model-value="startDateValue" :date-props="{ ariaLabel: t('dateFrom'), type: dateInputType, description: !isTouchDevice ? t(dateFormat) : undefined, errorMessage: startDateErrorText, }" :error-message="startSharedError" :show-time-input="allowTimeInput" :time-model-value="dateTextInput.startTime" :time-props="{ ariaLabel: t('timeFrom'), errorMessage: startSharedError, }" class="b-date-range-picker-calendar__form-input" @update:date-model-value="onStartDateInput" @date:keydown="onDateInputKeyDown" @update:time-model-value="onStartTimeInput" @time:error="onStartTimeError" @time:valid="onStartTimeValid" /> </div> <div> <bento-typography stronger el="span" class="b-date-range-picker-calendar__label"> To </bento-typography> <date-time-input :date-model-value="endDateValue" :date-props="{ ariaLabel: t('dateTo'), type: dateInputType, description: !isTouchDevice ? t(dateFormat) : undefined, errorMessage: endDateErrorText, }" :error-message="endSharedError" :show-time-input="allowTimeInput" :time-model-value="dateTextInput.endTime" :time-props="{ ariaLabel: t('timeTo'), errorMessage: endSharedError, }" class="b-date-range-picker-calendar__form-input" @update:date-model-value="onEndDateInput" @date:keydown="onDateInputKeyDown" @update:time-model-value="onEndTimeInput" @time:error="onEndTimeError" @time:valid="onEndTimeValid" /> </div> </div> <div v-if="hasSlot('actions')"> <slot name="actions" /> </div> </div> <!-- Calendar --> <div v-if="!hasNoCalendars && !isTouchDevice" class="b-date-range-picker-calendar__calendars-container" data-testid="date-range-picker-calendar-calendar-container" > <calendar :value="internalRangeDate" :first-day-of-week="firstDayOfWeek" :is-date-disabled="isDateDisabled" :max-range="maxRange" :min="computedMinMax.min" :max="computedMinMax.max" :number-of-months="numberOfMonths" :show-end-date-on-open="showEndDateOnOpen" :granularity="selectedGranularity" :variant="variant" :year-range="yearRange" is-range @input="onDateInput" @start-date-selected="onStartDateSelected" @end-date-selected="onEndDateSelected" /> </div> </div> </template> <script setup lang="ts"> import { computed, onMounted, type PropType, reactive, ref, toRaw, useSlots } from 'vue'; import { Calendar, type CalendarGranularityType } from '@/internal'; import { DateTimeInput } from '../date-time-input'; import { BentoAlert } from '@/components/alert'; import { BentoTypography } from '@/components/typography'; import BentoDropdown from '@/components/dropdown/dropdown.vue'; import { BentoSegmentedControl, type BentoSegmentedControlItem } from '@/components/segmented-control'; import { useI18n } from '@/utils/ts/i18n'; import { debounce } from '@/utils/ts/debounce'; import { useDateInputFormatter, useHasSlot, useTouchDevice } from '@/composables'; import { useGranularityAdjustments } from '@/components/internal/calendar/composables/use-granularity-adjustments'; import { useGranularMinMaxDate } from '@/components/internal/calendar/components/calendar-month/composables/granular-min-max-date'; import { useGranularityMemory } from '../../composables/use-granularity-memory'; import { DateRangePickerCalendarEvent, type DateRangePickerCalendarFormData, type DateRangePickerCalendarRangeSelectorItems, } from './date-range-picker-calendar.types'; import type { BentoDateRangePickerGranularityConfig, BentoDateRangePickerValue, } from '../../date-range-picker.types'; import { endOfDay } from 'date-fns/endOfDay'; import { startOfDay } from 'date-fns/startOfDay'; import { isSameSecond } from 'date-fns/isSameSecond'; import { isSameDay } from 'date-fns/isSameDay'; import { isEqual } from 'date-fns/isEqual'; import { isToday } from 'date-fns/isToday'; import { setMilliseconds } from 'date-fns/setMilliseconds'; import { dateToNativeDateString, dateToNativeMonthString, dateToTimeInputString, parseNativeMonthString, } from '@/utils/ts/format-date/format-date'; import { isValidTimeString } from '@/utils/ts/time-input'; import { setTimeInDateObject } from '@/utils/ts/format-date'; import messages from './messages.json'; const FORM_INPUT_DEBOUNCE_TIME = 300; const RANGE_SELECTOR_CUSTOM_RANGE_KEY = 'customRange'; type MessageSchema = (typeof messages)['en-US']; const maxRangeMessageKeyMap: Record<CalendarGranularityType, keyof MessageSchema> = { daily: 'maxRangeDays', weekly: 'maxRangeWeeks', monthly: 'maxRangeMonths', quarterly: 'maxRangeQuarters', }; const props = defineProps({ /** * Allows user to enter time values in the form */ allowTimeInput: { type: Boolean, default: false }, /** * Ranges form persistance data */ dateFormData: { type: Object as PropType<DateRangePickerCalendarFormData>, default: () => ({ startDate: undefined, endDate: undefined, startTime: undefined, endTime: undefined }), }, /** * Allows you to set the first day of the week. 0 is sunday, 1 is monday, 6 is saturday */ firstDayOfWeek: Calendar.props.firstDayOfWeek, /** * A list of available granularities. * If present, the date picker will display a segmented control to change granularity. */ granularities: { type: Array as PropType<Array<BentoDateRangePickerGranularityConfig>>, default: null, }, /** * Indicate if a date should be disabled or not */ isDateDisabled: Calendar.props.isDateDisabled, /** * Set a maximum number of dates to be selectable by the range. */ maxRange: Calendar.props.maxRange, /** * Sets a minimum limit for selectable dates - dates before `min` will not be selectable */ min: Calendar.props.min, /** * Sets a maximum limit for selectable dates - dates after `max` will not be selectable */ max: Calendar.props.max, /** * Number of months rendered on pane */ numberOfMonths: { type: Calendar.props.numberOfMonths.type, default: Calendar.props.numberOfMonths.default, validator: (n: number) => n >= 0, }, /** * Displays the end date's month when the calendar is opened. Defaults to false i.e. the start date's month is displayed. */ showEndDateOnOpen: { type: Boolean, default: false }, /** * Enables the custom range selector. * If provided, must be an array that sets the custom range dropdown items. Items are of the structure: * `label` - label of custom range item. * `value` - a unique key of the custom range item. * `data` - an object `{ startDate: Date; endDate: Date }` to set the date picker range to upon selecting. */ quickSelectRanges: { type: Array as PropType<DateRangePickerCalendarRangeSelectorItems>, default: undefined, }, /** * Selected date */ value: { type: Object as PropType<BentoDateRangePickerValue>, default: undefined }, /** * The type of calendar to display. Defaults to showing days. */ variant: Calendar.props.variant, /** * Range of years to be selectable from the year dropdown */ yearRange: Calendar.props.yearRange, }); const emit = defineEmits([ DateRangePickerCalendarEvent.CUSTOM_RANGE, DateRangePickerCalendarEvent.INPUT, DateRangePickerCalendarEvent.ERROR, DateRangePickerCalendarEvent.FORM_DATE, DateRangePickerCalendarEvent.START_DATE_SELECTED, ]); const slots = useSlots(); const hasSlot = useHasSlot(slots); const { t, tc } = useI18n<{ message: MessageSchema }>({ messages }); const { isTouchDevice } = useTouchDevice(); const { dateFormat, isFormatValid, parseDate, onKeyDown, autoFormat, formatDate: formatDateUtil, } = useDateInputFormatter(); const calculatedIsRelativeToNowQuickSelectRanges = ref({}); const defaultGranularity = computed(() => (props.variant === 'month' ? 'monthly' : 'daily')); const hasNoCalendars = computed(() => props.numberOfMonths === 0); const internalRangeDate = reactive<BentoDateRangePickerValue>({ startDate: props.value?.startDate, endDate: props.value?.endDate, range: props.value?.range, ...(props.granularities ? { granularity: props.value?.granularity || defaultGranularity.value, } : {}), }); /** * Currently range selector dropdown item that is selected. * The logic will try and find in each range item: * - If an endDate does not exist, then only check if the startDate day matches and if today's day matches. * - if time input has been enabled, then check if the seconds match. * - if an endDate does exist then just check if the startDate day and the endDate day match. * * Note: All date comparisons use local time consistently. Both sides of each comparison * originate from the same timezone context, so no UTC normalization is needed. */ const selectedRangeSelectorValue = computed(() => { return ( // eslint-disable-next-line consistent-return props?.quickSelectRanges?.find(({ value, data }) => { if (!data.endDate) { // Check if this quick select range is explicitly marked as "relative to now" if (data.isRelativeToNow) { // Retrieve previously calculated start/end dates for this relative range. // We use these if the user has already clicked on this time of quick range const newlyCalculatedQuickSelectDates = calculatedIsRelativeToNowQuickSelectRanges.value?.[value]; return newlyCalculatedQuickSelectDates ? isEqual(newlyCalculatedQuickSelectDates.startDate, internalRangeDate.startDate) && isEqual(newlyCalculatedQuickSelectDates.endDate, internalRangeDate.endDate) : // If not stored calculated dates, fall back to comparing the quick select item's value // with the internal range's stored range identifier. value === internalRangeDate.range; } // This block handles quick select ranges that do not have an endDate // AND are NOT explicitly marked as 'isRelativeToNow'. // This means the endDate will be today at the end of the day. // Uses isEqual for startDate to prevent false matches (e.g. same-day selection // accidentally matching a "This week" range). When time input is enabled, // also verify the end time is at end of day so that manual time changes // correctly fall back to "Custom range". return ( isEqual(data.startDate, internalRangeDate.startDate) && isToday(internalRangeDate.endDate) && (!props.allowTimeInput || isSameSecond(internalRangeDate.endDate, endOfDay(internalRangeDate.endDate))) && (props.granularities ? data.granularity === internalRangeDate.granularity : true) ); } if (props.allowTimeInput) { const res = isSameSecond(data.startDate, internalRangeDate.startDate) && isSameSecond(data.endDate, internalRangeDate.endDate) && (props.granularities ? data.granularity === internalRangeDate.granularity : true); return res; } if (data.endDate) { return ( isSameDay(data.startDate, internalRangeDate.startDate) && isSameDay(data.endDate, internalRangeDate.endDate) && (props.granularities ? data.granularity === internalRangeDate.granularity : true) ); } })?.value ?? RANGE_SELECTOR_CUSTOM_RANGE_KEY ); }); const quickSelectRangeDefaultItems = computed(() => [ { label: t('customRange'), value: RANGE_SELECTOR_CUSTOM_RANGE_KEY }, ...(props.quickSelectRanges ? props.quickSelectRanges : []), ]); const selectedGranularity = computed( () => props.granularities?.find(({ type }) => type === internalRangeDate.granularity) || null ); const granularityItems = computed<Array<BentoSegmentedControlItem>>( () => props.granularities?.map(({ type, disabled }) => ({ label: t(type), value: type, disabled, })) ?? [] ); const { getGranularCalendarLimits } = useGranularMinMaxDate(props); const computedMinMax = computed(() => props.granularities ? getGranularCalendarLimits(selectedGranularity.value) : { min: props.min ? startOfDay(props.min) : null, max: props.max ? endOfDay(props.max) : null } ); const { adjustDateForQuarterly, adjustDateForMonthly, adjustDateForWeekly, adjustStartDateInputForWeekly, adjustEndDateInputForWeekly, adjustStartDateInputForMonthly, adjustEndDateInputForMonthly, adjustStartDateInputForQuarterly, adjustEndDateInputForQuarterly, } = useGranularityAdjustments(props.firstDayOfWeek); const { savedRanges, saveRange, resetSavedRanges } = useGranularityMemory(); /** * Format date to a string value in current format * @param dateToFormat - Date to be formatted */ const formatDate = (dateToFormat?: Date) => { if (!dateToFormat) { return ''; } return formatDateUtil(dateToFormat); }; // Form error keys const startDateError = ref<keyof MessageSchema | null>(null); const endDateError = ref<keyof MessageSchema | null>(null); // Translated error messages for the template const startDateErrorText = computed(() => (startDateError.value ? t(startDateError.value) : null)); const endDateErrorText = computed(() => (endDateError.value ? t(endDateError.value) : null)); // Shared errors exclude format errors since those only concern the date input const startSharedError = computed(() => startDateError.value === 'invalidDateFormat' ? null : startDateErrorText.value ); const endSharedError = computed(() => (endDateError.value === 'invalidDateFormat' ? null : endDateErrorText.value)); /** * Returns a translated error message for a given date string, or null if valid. * @param dateText The date string to validate. * @param counterpartDate Optional date to compare against for range order validation. * @param role Whether the date being validated is the 'start' or 'end' of the range. */ const getDateErrorMessage = ( dateText: string, counterpartDate?: Date, role?: 'start' | 'end', resolvedDateTime?: Date ): keyof MessageSchema | null => { if (!dateText || !isFormatValid(dateText)) { return 'invalidDateFormat'; } const date = parseDate(dateText); const dayDate = startOfDay(date); if ((props.min && dayDate < startOfDay(props.min)) || (props.max && dayDate > startOfDay(props.max))) { return 'selectedDateIsNotAvailable'; } if (props.isDateDisabled && props.isDateDisabled(dayDate)) { return 'selectedDateIsNotAvailable'; } if (counterpartDate) { // When time input is enabled and we have a resolved datetime, compare full datetimes; // otherwise fall back to day-level comparison. const current = resolvedDateTime ? setMilliseconds(resolvedDateTime, 0).getTime() : dayDate.getTime(); const counterpart = resolvedDateTime ? setMilliseconds(counterpartDate, 0).getTime() : startOfDay(counterpartDate).getTime(); const isInvalid = role === 'start' ? current >= counterpart : current <= counterpart; if (isInvalid) { return 'startDateMustPrecedeEndDate'; } } return null; }; // Form input text used in range variant const dateTextInput = reactive({ startDate: props.dateFormData.startDate, endDate: props.dateFormData.endDate, startTime: props.dateFormData.startTime, endTime: props.dateFormData.endTime, }); /** * Checks if a time string is valid and within the min/max bounds if a date is provided. * @param timeText The time string to validate. * @param date The date string to check the time against. * @returns True if the time is valid, false otherwise. */ const isValidTime = (timeText: string, date?: string) => { const isTimeAllowed = () => { if (date && isValidDate(date)) { const dateToCheckWithTime = setTimeInDateObject(parseDate(date), timeText); // Check if date is between min/max bounds, if there are any if ( (props.min && dateToCheckWithTime < setMilliseconds(props.min, 0)) || (props.max && dateToCheckWithTime > setMilliseconds(props.max, 0)) ) { return false; } } return true; }; return isValidTimeString(timeText) && isTimeAllowed(); }; const isMonthVariant = computed(() => props.variant === 'month'); const formatNativeDate = (date: Date): string => { if (!date) { return ''; } return isMonthVariant.value ? dateToNativeMonthString(date) : dateToNativeDateString(date); }; const nativeStartDate = computed(() => formatNativeDate(internalRangeDate.startDate)); const nativeEndDate = computed(() => formatNativeDate(internalRangeDate.endDate)); const dateInputType = computed(() => (isTouchDevice.value ? (isMonthVariant.value ? 'month' : 'date') : 'text')); const startDateValue = computed(() => (isTouchDevice.value ? nativeStartDate.value : dateTextInput.startDate)); const endDateValue = computed(() => (isTouchDevice.value ? nativeEndDate.value : dateTextInput.endDate)); /** * Sets the time in a date object if the time is valid and the variant is not 'month'. * @param date The date object to modify. * @param time The time string to set. * @returns The modified date object or the original if time is not set. */ const setTimeInDateRef = (date: Date, time: string) => { // Do not set time if month variant if (!time || !isValidTimeString(time) || props.variant === 'month') { return date; } return setTimeInDateObject(new Date(date), time); }; const onDateInput = (selectedRange: BentoDateRangePickerValue) => { dateTextInput.startTime = dateToTimeInputString( props.min && props.allowTimeInput && isSameDay(props.min, selectedRange.startDate) ? props.min : startOfDay(selectedRange.startDate) ); dateTextInput.endTime = dateToTimeInputString( props.max && props.allowTimeInput && isSameDay(props.max, selectedRange.endDate) ? props.max : endOfDay(selectedRange.endDate) ); internalRangeDate.startDate = setTimeInDateRef(selectedRange.startDate, dateTextInput.startTime); internalRangeDate.endDate = setTimeInDateRef(selectedRange.endDate, dateTextInput.endTime); // Resetting internal range date when selecting a custom range through the calendar internalRangeDate.range = null; if (props.granularities) { // Reset saved ranges on input resetSavedRanges(); } emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); }; /** * Checks if a date string is in current format and if the date is enabled. * @param dateText The date string to validate. * @returns True if the date is valid and enabled, false otherwise. */ const isValidDate = (dateText: string): boolean => !!dateText && !getDateErrorMessage(dateText); /** * When mounted, set the input error states to true if the values are not valid */ onMounted(() => { // Start date validation startDateError.value = props.dateFormData.startDate ? getDateErrorMessage(props.dateFormData.startDate) : null; // Start time validation is delegated to bento-input-time via events // End date validation endDateError.value = props.dateFormData.endDate ? getDateErrorMessage(props.dateFormData.endDate) : null; // End time validation is delegated to bento-input-time via events }); const onStartDateSelected = (newStartDate: Date) => { dateTextInput.startDate = formatDate(newStartDate); if (!dateTextInput.startTime) { // Set the time to the beginning of the day dateTextInput.startTime = dateToTimeInputString(newStartDate); } startDateError.value = null; if (endDateError.value === 'startDateMustPrecedeEndDate') { endDateError.value = null; } emit(DateRangePickerCalendarEvent.START_DATE_SELECTED, newStartDate); }; const onEndDateSelected = (newEndDate: Date) => { dateTextInput.endDate = formatDate(newEndDate); if (!dateTextInput.endTime) { // Set the time to the end of the day dateTextInput.endTime = dateToTimeInputString(endOfDay(newEndDate)); } endDateError.value = null; if (startDateError.value === 'startDateMustPrecedeEndDate') { startDateError.value = null; } }; const getDate = (keyName: 'startDate' | 'endDate') => { const dateProp = props.value?.[keyName]; if (dateProp) { return dateProp; } if (props.dateFormData[keyName] && isValidDate(props.dateFormData[keyName])) { return parseDate(props.dateFormData[keyName]); } return undefined; }; const validateStartDate = (startDateText: string) => { if (isValidDate(startDateText)) { let dateText = startDateText; startDateError.value = null; switch (selectedGranularity.value?.type) { case 'weekly': dateText = adjustStartDateInputForWeekly( startDateText, formatDate, computedMinMax.value.min, computedMinMax.value.max ); dateTextInput.startDate = dateText; break; case 'monthly': dateText = adjustStartDateInputForMonthly( startDateText, formatDate, computedMinMax.value.min, computedMinMax.value.max ); dateTextInput.startDate = dateText; break; case 'quarterly': dateText = adjustStartDateInputForQuarterly( startDateText, formatDate, computedMinMax.value.min, computedMinMax.value.max ); dateTextInput.startDate = dateText; break; case 'daily': default: break; } const startDate = startOfDay(parseDate(dateText)); internalRangeDate.startDate = setTimeInDateRef(startDate, dateTextInput.startTime); if (isValidDate(dateTextInput.endDate)) { internalRangeDate.endDate = setTimeInDateRef( startOfDay(parseDate(dateTextInput.endDate)), dateTextInput.endTime ); } const endDate = getDate('endDate'); const rangeError = getDateErrorMessage( dateText, props.allowTimeInput ? internalRangeDate.endDate : endDate, 'start', props.allowTimeInput ? internalRangeDate.startDate : undefined ); if (rangeError) { startDateError.value = rangeError; emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); return; } if (endDateError.value === 'startDateMustPrecedeEndDate') { endDateError.value = null; } // Resetting internal range date when selecting a custom range through the calendar internalRangeDate.range = null; emit(DateRangePickerCalendarEvent.FORM_DATE, { startDate, endDate, }); emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); } else { // Indicate form error startDateError.value = getDateErrorMessage(startDateText); // Emit Date picker calendar error emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); } }; const validateStartDateDebounced = debounce(validateStartDate, FORM_INPUT_DEBOUNCE_TIME); const onStartDateInput = (startDateText: string) => { // Set the date as string when input through the form // Bypassing autoFormat for native month input since it doesn't support YYYY-MM dateTextInput.startDate = isTouchDevice.value && isMonthVariant.value ? startDateText : autoFormat(startDateText); if (isTouchDevice.value) { if (isMonthVariant.value && startDateText) { const startDate = parseNativeMonthString(startDateText); internalRangeDate.startDate = setTimeInDateRef(startDate, dateTextInput.startTime); internalRangeDate.range = null; emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); } else if (dateTextInput.startDate) { validateStartDate(dateTextInput.startDate); } else { // on touch devices, skip the any intermediate values e.g. pressing zero first startDateError.value = null; } } else { validateStartDateDebounced(dateTextInput.startDate); } }; const validateEndDate = (endDateText: string) => { if (isValidDate(endDateText)) { let dateText = endDateText; endDateError.value = null; switch (selectedGranularity.value?.type) { case 'weekly': dateText = adjustEndDateInputForWeekly( endDateText, formatDate, computedMinMax.value.min, computedMinMax.value.max ); dateTextInput.endDate = dateText; break; case 'monthly': dateText = adjustEndDateInputForMonthly( endDateText, formatDate, computedMinMax.value.min, computedMinMax.value.max ); dateTextInput.endDate = dateText; break; case 'quarterly': dateText = adjustEndDateInputForQuarterly( endDateText, formatDate, computedMinMax.value.min, computedMinMax.value.max ); dateTextInput.endDate = dateText; break; case 'daily': default: break; } // Format date string to Date object const endDate = startOfDay(parseDate(dateText)); internalRangeDate.endDate = setTimeInDateRef(endDate, dateTextInput.endTime); if (isValidDate(dateTextInput.startDate)) { internalRangeDate.startDate = setTimeInDateRef( startOfDay(parseDate(dateTextInput.startDate)), dateTextInput.startTime ); } const startDate = getDate('startDate'); const rangeError = getDateErrorMessage( dateText, props.allowTimeInput ? internalRangeDate.startDate : startDate, 'end', props.allowTimeInput ? internalRangeDate.endDate : undefined ); if (rangeError) { endDateError.value = rangeError; emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); return; } if (startDateError.value === 'startDateMustPrecedeEndDate') { startDateError.value = null; } // Resetting internal range date when selecting a custom range through the calendar internalRangeDate.range = null; emit(DateRangePickerCalendarEvent.FORM_DATE, { startDate, endDate, }); emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); } else { // Indicate form error endDateError.value = getDateErrorMessage(endDateText); // Emit Date picker calendar error emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); } }; const validateEndDateDebounced = debounce(validateEndDate, FORM_INPUT_DEBOUNCE_TIME); const onEndDateInput = (endDateText: string) => { // Set the date as string when input through the form // Bypassing autoFormat for native month input since it doesn't support YYYY-MM dateTextInput.endDate = isTouchDevice.value && isMonthVariant.value ? endDateText : autoFormat(endDateText); if (isTouchDevice.value) { if (isMonthVariant.value && endDateText) { const endDate = parseNativeMonthString(endDateText); internalRangeDate.endDate = setTimeInDateRef(endDate, dateTextInput.endTime); internalRangeDate.range = null; emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); } else if (dateTextInput.endDate) { validateEndDate(dateTextInput.endDate); } else { // on touch devices, skip the any intermediate values e.g. pressing zero first endDateError.value = null; } } else { validateEndDateDebounced(dateTextInput.endDate); } }; const onDateInputKeyDown = (event: KeyboardEvent) => { onKeyDown(event); }; const onStartTimeError = () => { emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); }; const onStartTimeValid = () => { const startTimeText = dateTextInput.startTime; if (!isValidTime(startTimeText, dateTextInput.startDate)) { emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); return; } internalRangeDate.startDate = setTimeInDateRef(internalRangeDate.startDate, startTimeText); // Validate range order with updated time if (dateTextInput.startDate && internalRangeDate.endDate) { const rangeError = getDateErrorMessage( dateTextInput.startDate, internalRangeDate.endDate, 'start', internalRangeDate.startDate ); if (rangeError) { startDateError.value = rangeError; emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); return; } } startDateError.value = null; if (endDateError.value === 'startDateMustPrecedeEndDate') { endDateError.value = null; } const endDate = getDate('endDate'); emit(DateRangePickerCalendarEvent.FORM_DATE, { startDate: internalRangeDate.startDate, endDate, startTime: startTimeText, endTime: dateTextInput.endTime ?? props.dateFormData.endTime, }); emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); }; const onEndTimeError = () => { emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); }; const onEndTimeValid = () => { const endTimeText = dateTextInput.endTime; if (!isValidTime(endTimeText, dateTextInput.endDate)) { emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); return; } internalRangeDate.endDate = setTimeInDateRef(internalRangeDate.endDate, endTimeText); // Validate range order with updated time if (dateTextInput.endDate && internalRangeDate.startDate) { const rangeError = getDateErrorMessage( dateTextInput.endDate, internalRangeDate.startDate, 'end', internalRangeDate.endDate ); if (rangeError) { endDateError.value = rangeError; emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); return; } } endDateError.value = null; if (startDateError.value === 'startDateMustPrecedeEndDate') { startDateError.value = null; } emit(DateRangePickerCalendarEvent.FORM_DATE, { startDate: internalRangeDate.startDate, endDate: internalRangeDate.endDate, startTime: dateTextInput.startTime ?? props.dateFormData.startTime, endTime: endTimeText, }); emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); }; const onStartTimeInput = (startTimeText: string) => { dateTextInput.startTime = startTimeText; }; const onEndTimeInput = (endTimeText: string) => { dateTextInput.endTime = endTimeText; }; const setInternalRange = ( startDate: Date, endDate: Date, granularity?: CalendarGranularityType, range?: string ) => { internalRangeDate.startDate = startDate; internalRangeDate.endDate = endDate; if (granularity) { internalRangeDate.granularity = granularity; } if (range) { internalRangeDate.range = range; } }; const onRangeSelectorInput = (newCustomRangeValue: string) => { const foundResult = props.quickSelectRanges?.find(({ value }) => value === newCustomRangeValue); if (foundResult) { resetSavedRanges(); const date = foundResult.data; const newEndDate = date.endDate || (date.isRelativeToNow ? new Date(Date.now()) : endOfDay(new Date(Date.now()))); // If the time difference between start and end dates is available, // calculating start date dynamically const newStartDate = date.isRelativeToNow && date.timeDifference ? new Date(newEndDate.getTime() - date.timeDifference) : date.startDate; // Set Form input Dates dateTextInput.endDate = formatDate(newEndDate); dateTextInput.startDate = formatDate(newStartDate); setInternalRange(newStartDate, newEndDate, null, newCustomRangeValue); if (props.granularities) { internalRangeDate.granularity = date.granularity || defaultGranularity.value; } // Set Form input time to quick select range time dateTextInput.startTime = dateToTimeInputString(newStartDate); dateTextInput.endTime = dateToTimeInputString(newEndDate); // Set calendar selection internalRangeDate.startDate = newStartDate; internalRangeDate.endDate = newEndDate; if (date.isRelativeToNow) { // Store this calculated start and end dates to be used in the selectedRangeSelectorValue logic calculatedIsRelativeToNowQuickSelectRanges.value = { ...calculatedIsRelativeToNowQuickSelectRanges.value, [newCustomRangeValue]: { startDate: newStartDate, endDate: newEndDate, }, }; } // Emit everytime the date range changes emit(DateRangePickerCalendarEvent.CUSTOM_RANGE, { ...foundResult.data, startDate: newStartDate, endDate: newEndDate, range: foundResult.value, }); } }; const adjustDate = { weekly: adjustDateForWeekly, monthly: adjustDateForMonthly, quarterly: adjustDateForQuarterly, }; const onGranularityInput = (value: CalendarGranularityType) => { const previousValue = internalRangeDate.granularity; internalRangeDate.granularity = value; // Save previously selected range if (previousValue) { saveRange(previousValue, structuredClone(toRaw(internalRangeDate))); } if (internalRangeDate.startDate && internalRangeDate.endDate) { // Adjust date range based on new granularity switch (value) { case 'quarterly': case 'monthly': case 'weekly': { if (savedRanges[value]) { setInternalRange(savedRanges[value].startDate, savedRanges[value].endDate, value); break; } const { startDate, endDate } = adjustDate[value]( internalRangeDate.startDate, internalRangeDate.endDate, computedMinMax.value.min, computedMinMax.value.max ); setInternalRange(startDate, endDate, value); break; } case 'daily': if (savedRanges.daily) { setInternalRange(savedRanges.daily.startDate, savedRanges.daily.endDate, value); } break; default: break; } dateTextInput.startDate = formatDate(internalRangeDate.startDate); dateTextInput.endDate = formatDate(internalRangeDate.endDate); emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); } }; const computedFormContainerClasses = computed(() => ({ 'b-date-range-picker-calendar__form-container--no-calendars': hasNoCalendars.value, })); const maxRangeMessage = computed(() => { const granularity = selectedGranularity.value; const count = granularity?.maxRange || props.maxRange; if (!count) { return null; } const key = maxRangeMessageKeyMap[granularity?.type ?? 'daily']; return tc(key, count, { count }); }); </script> <script lang="ts"> /** * Calendar for range dates. Contains the logic to handle the range forms. * * @example * import DatePickerCalendarRange from './date-range-picker-calendar.vue'; * * export default { * components: { DatePickerCalendarRange }, * template: ` * <date-range-picker-calendar * :value="{startDate: new Date(), endDate: new Date() }" * :first-day-of-week="BentoDatePickerFirstDayOfWeek.MONDAY" * :is-date-disabled="(date: Date) => boolean" * :number-of-months="2" * @input="({ startDate, endDate }) => void" * /> * ` * } */ export default { name: 'date-range-picker-calendar', i18n: { messages }, }; </script> <style lang="scss" scoped src="./date-range-picker-calendar.scss" />
@@ -1 +1 @@
1
- <!-- TODO: Remove aria props as these are part of JSX elements /vue/types/jsx.d.ts --> <!-- eslint-disable vue/attribute-hyphenation --> <template> <div v-bento-click-outside-directive="closeDateRangePicker" class="b-date-range-picker"> <!-- Label --> <field-label v-if="label" :id="labelId" :label="label" :tooltip-text="tooltipText" :optional="optional" :required="required" /> <!-- Input --> <div @keydown.enter="onEnterPressedOverInput"> <dropdown-input-default ref="inputContainerRef" always-combobox-is-input :ariaControls="datePickerContainerId" :ariaExpanded="isDateRangePickerOpen" :aria-labelledby="dropdownInputAriaLabelledBy" :aria-describedby="ariaDescribedBy" :aria-required="required" :disabled="disabled" :is-invalid="!!errorMessage || shouldDisplayError" :open="isDateRangePickerOpen" :display-value="displayedDateValue" :readonly="isReadOnly" @open="openDateRangePicker" @clear="clearDateRangePickerValue" @close="closeDateRangePicker" /> </div> <!-- Error Messages --> <error-message v-if="shouldDisplayError" :id="dateErrorId" :error-message="t('invalidDateFormat')" class="b-date-range-picker__error-message" /> <error-message v-if="!!errorMessage" :id="errorId" :error-message="errorMessage" class="b-date-range-picker__error-message" /> <!-- Description --> <bento-typography v-if="description" :id="descriptionId" class="b-date-range-picker__description" :class="descriptionConditionalClasses" el="span" > {{ description }} </bento-typography> <!-- Calendar container --> <bento-popover v-if="inputContainerRef" :id="datePickerContainerId" class="b-date-range-picker__container" :open="isDateRangePickerOpen" :target-element="inputContainerRef" :aria-label="popoverAriaLabel" position="bottom-start" without-space fit-content trap-all overflow-visible :trap-all-options="trapAllOptions" @keydown.esc.native="closeDateRangePicker" > <date-range-picker-calendar :allow-time-input="withTimeInput" :quick-select-ranges="adjustedQuickSelectRanges" :value="dateRangePickerCalendarValue" :first-day-of-week="resolvedFirstDayOfWeek" :granularities="granularities" :is-date-disabled="isDateDisabled" :max-range="maxRange" :min="min" :max="max" :number-of-months="computedNumberOfMonths" :show-end-date-on-open="showEndDateOnOpen" :date-form-data="displayedTextInputValue" :variant="variant" :year-range="yearRange" @custom-range="onRangeSelectorInput" @input="onDateRangePickerInput" @error="onDateRangePickerError" @form-date="onDateRangePickerFormDateInputUpdate" > <template v-if="hasActions" #actions> <bento-divider /> <div class="b-date-range-picker__footer"> <bento-button-actions :actions="rangePickerButtonActions" :layout="BentoButtonActionsLayout.SPACE_BETWEEN" /> </div> </template> </date-range-picker-calendar> </bento-popover> </div> </template> <script setup lang="ts"> import { computed, type HTMLAttributes, onMounted, ref, toRaw, toRef, useAttrs, watch } from 'vue'; import { useBreakpoints } from '@vueuse/core'; // Components import { BentoButtonActions, BentoButtonActionsLayout } from '../button'; import { BentoDivider } from '@/components/divider'; import { BentoPopover } from '@/components/popover'; import { BentoTypography } from '../typography'; import { DateRangePickerCalendar } from './components/date-range-picker-calendar'; import { DropdownInputDefault } from '@/components/dropdown/components'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { dateToTimeInputString } from '@/utils/ts/format-date'; import { useDateInputFormatter } from '@/composables/use-date-input-formatter/use-date-input-formatter'; // Composables import { useDateRangePickerCalendarText } from './composables/use-date-range-picker-calendar-text'; import { useFirstDayOfWeek, useFormLayoutFieldLoading } from '@/composables'; // Directives import { BentoClickOutsideDirective as vBentoClickOutsideDirective } from '@/directives'; // Types import { type BentoDateRangePickerEmits, type BentoDateRangePickerProps, type BentoDateRangePickerValue, } from './date-range-picker.types'; import { type DateRangePickerCalendarFormData, type DateRangePickerCalendarRangeSelectorItem, } from './components/date-range-picker-calendar/date-range-picker-calendar.types'; import { deprecate } from '@/utils/ts/deprecate'; import messages from './messages.json'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; type MessageSchema = (typeof messages)['en-US']; const emit = defineEmits<BentoDateRangePickerEmits>(); const props = withDefaults(defineProps<BentoDateRangePickerProps>(), { description: undefined, disabled: false, errorMessage: null, firstDayOfWeek: undefined, hasActions: false, isDateDisabled: undefined, label: undefined, max: null, maxRange: null, min: null, modelValue: null, numberOfMonths: undefined, optional: false, placeholder: null, quickSelectRanges: undefined, readonly: false, required: false, showEndDateOnOpen: false, tooltipText: null, value: null, variant: undefined, // Default will be set by @/internal/calendar yearRange: undefined, }); const attrs: HTMLAttributes = useAttrs(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const { dateToInputDateString } = useDateInputFormatter(); const datePickerContainerId = generateUid('bento-date-range-picker-container'); const labelId = generateUid('bento-date-range-picker-label'); const descriptionId = generateUid('bento-date-range-picker-description'); const errorId = generateUid('bento-date-range-picker-error'); const dateErrorId = generateUid('bento-date-range-picker-date-error'); const popoverAriaLabel = computed(() => props.label || t('dateRangePicker', { name: datePickerContainerId })); const { resolvedFirstDayOfWeek } = useFirstDayOfWeek(toRef(props, 'firstDayOfWeek')); const breakpoints = useBreakpoints({ none: 0, one: 710, // width of date range picker with one panes two: 1030, // width of date range picker with two panes three: 1290, // width of date range picker with three panes }); const computedNumberOfMonths = computed(() => { const activeBreakpoint = breakpoints.active().value; const breakpointToCalendarPanesMap = { none: 0, one: 1, two: 2, }; if (activeBreakpoint in breakpointToCalendarPanesMap) { const maxPanes = breakpointToCalendarPanesMap[activeBreakpoint as keyof typeof breakpointToCalendarPanesMap]; // If numberOfMonths is not provided, use the max panes for the breakpoint. // Otherwise, use the smaller of the two values. return props.numberOfMonths === undefined ? maxPanes : Math.min(props.numberOfMonths, maxPanes); } // Default case for 'three' and larger breakpoints, respect user-provided numberOfMonths. return props.numberOfMonths; }); const adjustedQuickSelectRanges = computed<Array<DateRangePickerCalendarRangeSelectorItem>>(() => { return ( props?.quickSelectRanges && props.quickSelectRanges.map((range: DateRangePickerCalendarRangeSelectorItem) => { const timeDiff = !range.data.endDate ? new Date(Date.now()).getTime() - range.data.startDate.getTime() : undefined; return { ...range, data: { ...range.data, // Adding timeDifference to calculate an up-to-date endDate on range selection timeDifference: timeDiff, }, }; }) ); }); const dropdownInputAriaLabelledBy = computed(() => (props.label ? labelId : (attrs?.['aria-labelledby'] ?? null))); const isDateIncorrect = ref(false); const inputContainerRef = ref(null); const isDateRangePickerOpen = ref(false); const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const { emitValue } = useFormFieldEmits(emit); watch( () => props.disabled, () => { // Close date picker popover if it's opened when it's disabled if (props.disabled && isDateRangePickerOpen.value) { isDateRangePickerOpen.value = false; } } ); const internalModelValue = computed(() => props.modelValue || props.value); const shouldDisplayError = computed(() => { if (props.hasActions) { return !isApplyDisabled.value && isDateIncorrect.value; } return isDateIncorrect.value; }); const descriptionConditionalClasses = computed(() => ({ 'b-date-range-picker__description--error': !!props.errorMessage || shouldDisplayError.value, })); const isMonthVariant = computed(() => props.variant === 'month'); const withTimeInput = computed(() => props.allowTimeInput && props.variant !== 'month'); const { dateDisplayValue, textInputDisplayValue, formatTextInputDisplayValue } = useDateRangePickerCalendarText({ datePickerValue: internalModelValue, isDateIncorrect, quickSelectRanges: adjustedQuickSelectRanges, isMonthVariant, withTimeInput, placeholder: toRef(props, 'placeholder'), }); const ariaDescribedBy = computed( () => `${props.description ? descriptionId : ''} ${props.errorMessage ? errorId : ''} ${ isDateIncorrect.value ? dateErrorId : '' }`.trim() || null ); const closeDateRangePicker = () => { isDateRangePickerOpen.value = false; }; const openDateRangePicker = () => { isDateRangePickerOpen.value = true; formatTextInputDisplayValue(); }; /* * Used when `hasActions` is set to true */ const localDateDisplayValue = ref(dateDisplayValue.value); const localTextInputDisplayValue = ref<DateRangePickerCalendarFormData>( // textInputDisplayValue is a reactive object structuredClone(toRaw(textInputDisplayValue)) ); const selectedDate = ref<BentoDateRangePickerValue>(props.modelValue ?? props.value); const isApplyDisabled = ref(true); const dateRangePickerCalendarValue = computed(() => props.hasActions ? selectedDate.value : internalModelValue.value ); const displayedTextInputValue = ref<DateRangePickerCalendarFormData>( props.hasActions ? localTextInputDisplayValue.value : textInputDisplayValue ); const displayedDateValue = computed(() => props.hasActions ? localDateDisplayValue.value : dateDisplayValue.value ); const trapAllOptions = { allowOutsideClick: true, }; onMounted(() => { if (!props.hasActions) { deprecate( 'BentoDateRangePicker "hasActions" property', `Set the BentoDateRangePicker "hasActions" property true which will display the "Apply" and "Cancel" buttons. This will be the default behavior in the v2 and "hasActions" will be removed.`, '2.0.0' ); } if (props.value) { deprecate( 'BentoDateRangePicker "value" property', 'The use of "value" prop in "BentoDateRangePicker" is no longer supported. Use the "v-model" or "model-value" property instead.', '2.0.0' ); } if ( props.granularities && props.variant !== 'month' && props.granularities.find(({ type }) => type === 'quarterly') ) { throw new Error('Quarterly granularity is only supported by BentoDateRangePicker with variant: "month"'); } }); // Sync the local draft display state back to the committed model value. // Since `displayedTextInputValue` is a mutable ref, it must point to the new cloned draft object. const syncLocalDisplayStateFromCommittedValue = () => { localDateDisplayValue.value = dateDisplayValue.value; localTextInputDisplayValue.value = structuredClone(toRaw(textInputDisplayValue)); displayedTextInputValue.value = props.hasActions ? localTextInputDisplayValue.value : textInputDisplayValue; }; // On update for the value we need to update all the locally stored data watch([() => props.value, () => props.modelValue], () => { syncLocalDisplayStateFromCommittedValue(); }); const cancelDateRangePicker = () => { // Reset local date range picker state syncLocalDisplayStateFromCommittedValue(); selectedDate.value = internalModelValue.value; isDateIncorrect.value = false; isApplyDisabled.value = true; closeDateRangePicker(); }; const rangePickerButtonActions = computed(() => [ { title: t('apply'), event: () => { emitValue(selectedDate.value); closeDateRangePicker(); isApplyDisabled.value = true; }, disabled: isApplyDisabled.value, }, { title: t('cancel'), event: cancelDateRangePicker, }, ]); watch(selectedDate, date => { // skip emit until Apply button is clicked if (props.hasActions) { isApplyDisabled.value = isDateIncorrect.value || date === internalModelValue.value; return; } emitValue(date); }); const onDateRangePickerInput = (selectedRange: BentoDateRangePickerValue) => { // Reset the errors when the date is correct isDateIncorrect.value = false; displayedTextInputValue.value.startDate = dateToInputDateString(selectedRange.startDate); displayedTextInputValue.value.endDate = dateToInputDateString(selectedRange.endDate); displayedTextInputValue.value.startTime = dateToTimeInputString(selectedRange.startDate); displayedTextInputValue.value.endTime = dateToTimeInputString(selectedRange.endDate); selectedDate.value = selectedRange; }; const clearDateRangePickerValue = async () => { if (isDateIncorrect.value) { isDateIncorrect.value = false; } selectedDate.value = null; }; const onEnterPressedOverInput = () => { // open the datepicker if (!isDateRangePickerOpen.value) { isDateRangePickerOpen.value = true; return; } // Close the date picker if the date is correct and it is open if (!isDateIncorrect.value) { isDateRangePickerOpen.value = false; } }; const onDateRangePickerFormDateInputUpdate = (dateFormDate: DateRangePickerCalendarFormData) => { isDateIncorrect.value = false; if (dateFormDate.startTime) { displayedTextInputValue.value.startTime = dateFormDate.startTime; } if (dateFormDate.endTime) { displayedTextInputValue.value.endTime = dateFormDate.endTime; } }; const onDateRangePickerError = dateTextInput => { isDateIncorrect.value = true; displayedTextInputValue.value.startDate = dateTextInput.startDate; displayedTextInputValue.value.endDate = dateTextInput.endDate; // Remove current date range selection selectedDate.value = { startDate: null, endDate: null }; }; const onRangeSelectorInput = (quickSelectRanges?: BentoDateRangePickerValue) => { if (quickSelectRanges) { displayedTextInputValue.value.startDate = dateToInputDateString(quickSelectRanges.startDate); displayedTextInputValue.value.endDate = dateToInputDateString(quickSelectRanges.endDate); displayedTextInputValue.value.startTime = dateToTimeInputString(quickSelectRanges.startDate); displayedTextInputValue.value.endTime = dateToTimeInputString(quickSelectRanges.endDate); selectedDate.value = quickSelectRanges; } }; </script> <script lang="ts"> /** * Date range picker selector. * * @example * import { BentoDateRangePicker } from '@adyen/bento-vue2'; * * export default { * components: { BentoDateRangePicker }, * template: ` * <bento-date-range-picker * label="Label" * description="Supporting text" * required * optional * disabled * @input="onDateChanged" * v-model="selectedDate" * /> * `, * setup() { * const selectedDate = ref(new Date()); // reactive({ startDate: new Date(), endDate: new Date() }) * return { * selectedDate, * } * } * } */ export default { i18n: { messages }, name: 'bento-date-range-picker', model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./date-range-picker.scss" />
1
+ <!-- TODO: Remove aria props as these are part of JSX elements /vue/types/jsx.d.ts --> <!-- eslint-disable vue/attribute-hyphenation --> <template> <div v-bento-click-outside-directive="closeDateRangePicker" class="b-date-range-picker"> <!-- Label --> <field-label v-if="label" :id="labelId" :label="label" :tooltip-text="tooltipText" :optional="optional" :required="required" /> <!-- Input --> <div @keydown.enter="onEnterPressedOverInput"> <dropdown-input-default ref="inputContainerRef" always-combobox-is-input :ariaControls="datePickerContainerId" :ariaExpanded="isDateRangePickerOpen" :aria-labelledby="dropdownInputAriaLabelledBy" :aria-describedby="ariaDescribedBy" :aria-required="required" :disabled="disabled" :is-invalid="!!errorMessage || shouldDisplayError" :open="isDateRangePickerOpen" :display-value="displayedDateValue" :readonly="isReadOnly" @open="openDateRangePicker" @clear="clearDateRangePickerValue" @close="closeDateRangePicker" /> </div> <!-- Error Messages --> <error-message v-if="shouldDisplayError" :id="dateErrorId" :error-message="t('invalidDateFormat')" class="b-date-range-picker__error-message" /> <error-message v-if="!!errorMessage" :id="errorId" :error-message="errorMessage" class="b-date-range-picker__error-message" /> <!-- Description --> <bento-typography v-if="description" :id="descriptionId" class="b-date-range-picker__description" :class="descriptionConditionalClasses" el="span" > {{ description }} </bento-typography> <!-- Calendar container --> <bento-popover v-if="inputContainerRef" :id="datePickerContainerId" class="b-date-range-picker__container" :open="isDateRangePickerOpen" :target-element="inputContainerRef" :aria-label="popoverAriaLabel" position="bottom-start" without-space fit-content trap-all overflow-visible :trap-all-options="trapAllOptions" @keydown.esc.native="closeDateRangePicker" > <date-range-picker-calendar :allow-time-input="withTimeInput" :quick-select-ranges="adjustedQuickSelectRanges" :value="dateRangePickerCalendarValue" :first-day-of-week="resolvedFirstDayOfWeek" :granularities="granularities" :is-date-disabled="isDateDisabled" :max-range="maxRange" :min="min" :max="max" :number-of-months="computedNumberOfMonths" :show-end-date-on-open="showEndDateOnOpen" :date-form-data="displayedTextInputValue" :variant="variant" :year-range="yearRange" @custom-range="onRangeSelectorInput" @input="onDateRangePickerInput" @error="onDateRangePickerError" @form-date="onDateRangePickerFormDateInputUpdate" > <template v-if="hasActions" #actions> <bento-divider /> <div class="b-date-range-picker__footer"> <bento-button-actions :actions="rangePickerButtonActions" :layout="BentoButtonActionsLayout.SPACE_BETWEEN" /> </div> </template> </date-range-picker-calendar> </bento-popover> </div> </template> <script setup lang="ts"> import { computed, type HTMLAttributes, onMounted, ref, toRaw, toRef, useAttrs, watch } from 'vue'; import { useBreakpoints } from '@vueuse/core'; // Components import { BentoButtonActions, BentoButtonActionsLayout } from '../button'; import { BentoDivider } from '@/components/divider'; import { BentoPopover } from '@/components/popover'; import { BentoTypography } from '../typography'; import { DateRangePickerCalendar } from './components/date-range-picker-calendar'; import { DropdownInputDefault } from '@/components/dropdown/components'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { dateToTimeInputString } from '@/utils/ts/format-date'; import { useDateInputFormatter } from '@/composables/use-date-input-formatter/use-date-input-formatter'; // Composables import { useDateRangePickerCalendarText } from './composables/use-date-range-picker-calendar-text'; import { useFirstDayOfWeek, useFormLayoutFieldLoading } from '@/composables'; // Directives import { BentoClickOutsideDirective as vBentoClickOutsideDirective } from '@/directives'; // Types import { type BentoDateRangePickerEmits, type BentoDateRangePickerProps, type BentoDateRangePickerValue, } from './date-range-picker.types'; import { type DateRangePickerCalendarFormData, type DateRangePickerCalendarRangeSelectorItem, } from './components/date-range-picker-calendar/date-range-picker-calendar.types'; import { deprecate } from '@/utils/ts/deprecate'; import messages from './messages.json'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; type MessageSchema = (typeof messages)['en-US']; const emit = defineEmits<BentoDateRangePickerEmits>(); const props = withDefaults(defineProps<BentoDateRangePickerProps>(), { description: undefined, disabled: false, errorMessage: null, firstDayOfWeek: undefined, hasActions: false, isDateDisabled: undefined, label: undefined, max: null, maxRange: null, min: null, modelValue: null, numberOfMonths: undefined, optional: false, placeholder: null, quickSelectRanges: undefined, readonly: false, required: false, showEndDateOnOpen: false, tooltipText: null, value: null, variant: undefined, // Default will be set by @/internal/calendar yearRange: undefined, }); const attrs: HTMLAttributes = useAttrs(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const { dateToInputDateString } = useDateInputFormatter(); const datePickerContainerId = generateUid('bento-date-range-picker-container'); const labelId = generateUid('bento-date-range-picker-label'); const descriptionId = generateUid('bento-date-range-picker-description'); const errorId = generateUid('bento-date-range-picker-error'); const dateErrorId = generateUid('bento-date-range-picker-date-error'); const popoverAriaLabel = computed(() => props.label || t('dateRangePicker', { name: datePickerContainerId })); const { resolvedFirstDayOfWeek } = useFirstDayOfWeek(toRef(props, 'firstDayOfWeek')); const breakpoints = useBreakpoints({ none: 0, one: 710, // width of date range picker with one panes two: 1030, // width of date range picker with two panes three: 1290, // width of date range picker with three panes }); const computedNumberOfMonths = computed(() => { const activeBreakpoint = breakpoints.active().value; const breakpointToCalendarPanesMap = { none: 0, one: 1, two: 2, }; if (activeBreakpoint in breakpointToCalendarPanesMap) { const maxPanes = breakpointToCalendarPanesMap[activeBreakpoint as keyof typeof breakpointToCalendarPanesMap]; // If numberOfMonths is not provided, use the max panes for the breakpoint. // Otherwise, use the smaller of the two values. return props.numberOfMonths === undefined ? maxPanes : Math.min(props.numberOfMonths, maxPanes); } // Default case for 'three' and larger breakpoints, respect user-provided numberOfMonths. return props.numberOfMonths; }); const adjustedQuickSelectRanges = computed<Array<DateRangePickerCalendarRangeSelectorItem>>(() => { return ( props?.quickSelectRanges && props.quickSelectRanges.map((range: DateRangePickerCalendarRangeSelectorItem) => { const timeDiff = !range.data.endDate ? new Date(Date.now()).getTime() - range.data.startDate.getTime() : undefined; return { ...range, data: { ...range.data, // Adding timeDifference to calculate an up-to-date endDate on range selection timeDifference: timeDiff, }, }; }) ); }); const dropdownInputAriaLabelledBy = computed(() => (props.label ? labelId : (attrs?.['aria-labelledby'] ?? null))); const isDateIncorrect = ref(false); const inputContainerRef = ref(null); const isDateRangePickerOpen = ref(false); const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const { emitValue } = useFormFieldEmits(emit); watch( () => props.disabled, () => { // Close date picker popover if it's opened when it's disabled if (props.disabled && isDateRangePickerOpen.value) { isDateRangePickerOpen.value = false; } } ); const internalModelValue = computed(() => props.modelValue || props.value); const shouldDisplayError = computed(() => { if (props.hasActions) { return !isApplyDisabled.value && isDateIncorrect.value; } return isDateIncorrect.value; }); const descriptionConditionalClasses = computed(() => ({ 'b-date-range-picker__description--error': !!props.errorMessage || shouldDisplayError.value, })); const isMonthVariant = computed(() => props.variant === 'month'); const withTimeInput = computed(() => props.allowTimeInput && props.variant !== 'month'); const { dateDisplayValue, textInputDisplayValue, formatTextInputDisplayValue } = useDateRangePickerCalendarText({ datePickerValue: internalModelValue, isDateIncorrect, quickSelectRanges: adjustedQuickSelectRanges, isMonthVariant, withTimeInput, placeholder: toRef(props, 'placeholder'), }); const ariaDescribedBy = computed( () => `${props.description ? descriptionId : ''} ${props.errorMessage ? errorId : ''} ${ isDateIncorrect.value ? dateErrorId : '' }`.trim() || null ); const closeDateRangePicker = () => { isDateRangePickerOpen.value = false; }; const openDateRangePicker = () => { isDateRangePickerOpen.value = true; formatTextInputDisplayValue(); }; /* * Used when `hasActions` is set to true */ const localDateDisplayValue = ref(dateDisplayValue.value); const localTextInputDisplayValue = ref<DateRangePickerCalendarFormData>( // textInputDisplayValue is a reactive object structuredClone(toRaw(textInputDisplayValue)) ); const selectedDate = ref<BentoDateRangePickerValue>(props.modelValue ?? props.value); const isApplyDisabled = ref(true); const dateRangePickerCalendarValue = computed(() => props.hasActions ? selectedDate.value : internalModelValue.value ); const displayedTextInputValue = ref<DateRangePickerCalendarFormData>( props.hasActions ? localTextInputDisplayValue.value : textInputDisplayValue ); const displayedDateValue = computed(() => props.hasActions ? localDateDisplayValue.value : dateDisplayValue.value ); const trapAllOptions = { allowOutsideClick: true, }; onMounted(() => { if (!props.hasActions) { deprecate( 'BentoDateRangePicker "hasActions" property', `Set the BentoDateRangePicker "hasActions" property true which will display the "Apply" and "Cancel" buttons. This will be the default behavior in the v2 and "hasActions" will be removed.`, '2.0.0' ); } if (props.value) { deprecate( 'BentoDateRangePicker "value" property', 'The use of "value" prop in "BentoDateRangePicker" is no longer supported. Use the "v-model" or "model-value" property instead.', '2.0.0' ); } if ( props.granularities && props.variant !== 'month' && props.granularities.find(({ type }) => type === 'quarterly') ) { throw new Error('Quarterly granularity is only supported by BentoDateRangePicker with variant: "month"'); } }); // Sync the local draft display state back to the committed model value. // Since `displayedTextInputValue` is a mutable ref, it must point to the new cloned draft object. const syncLocalDisplayStateFromCommittedValue = () => { localDateDisplayValue.value = dateDisplayValue.value; localTextInputDisplayValue.value = structuredClone(toRaw(textInputDisplayValue)); displayedTextInputValue.value = props.hasActions ? localTextInputDisplayValue.value : textInputDisplayValue; selectedDate.value = internalModelValue.value; }; // On update for the value we need to update all the locally stored data watch([() => props.value, () => props.modelValue], () => { syncLocalDisplayStateFromCommittedValue(); }); const cancelDateRangePicker = () => { // Reset local date range picker state syncLocalDisplayStateFromCommittedValue(); isDateIncorrect.value = false; isApplyDisabled.value = true; closeDateRangePicker(); }; const rangePickerButtonActions = computed(() => [ { title: t('apply'), event: () => { emitValue(selectedDate.value); closeDateRangePicker(); isApplyDisabled.value = true; }, disabled: isApplyDisabled.value, }, { title: t('cancel'), event: cancelDateRangePicker, }, ]); watch(selectedDate, date => { // skip emit until Apply button is clicked if (props.hasActions) { isApplyDisabled.value = isDateIncorrect.value || date === internalModelValue.value; return; } emitValue(date); }); const onDateRangePickerInput = (selectedRange: BentoDateRangePickerValue) => { // Reset the errors when the date is correct isDateIncorrect.value = false; displayedTextInputValue.value.startDate = dateToInputDateString(selectedRange.startDate); displayedTextInputValue.value.endDate = dateToInputDateString(selectedRange.endDate); displayedTextInputValue.value.startTime = dateToTimeInputString(selectedRange.startDate); displayedTextInputValue.value.endTime = dateToTimeInputString(selectedRange.endDate); selectedDate.value = selectedRange; }; const clearDateRangePickerValue = async () => { if (isDateIncorrect.value) { isDateIncorrect.value = false; } selectedDate.value = null; }; const onEnterPressedOverInput = () => { // open the datepicker if (!isDateRangePickerOpen.value) { isDateRangePickerOpen.value = true; return; } // Close the date picker if the date is correct and it is open if (!isDateIncorrect.value) { isDateRangePickerOpen.value = false; } }; const onDateRangePickerFormDateInputUpdate = (dateFormDate: DateRangePickerCalendarFormData) => { isDateIncorrect.value = false; if (dateFormDate.startTime) { displayedTextInputValue.value.startTime = dateFormDate.startTime; } if (dateFormDate.endTime) { displayedTextInputValue.value.endTime = dateFormDate.endTime; } }; const onDateRangePickerError = dateTextInput => { isDateIncorrect.value = true; displayedTextInputValue.value.startDate = dateTextInput.startDate; displayedTextInputValue.value.endDate = dateTextInput.endDate; // Remove current date range selection selectedDate.value = { startDate: null, endDate: null }; }; const onRangeSelectorInput = (quickSelectRanges?: BentoDateRangePickerValue) => { if (quickSelectRanges) { displayedTextInputValue.value.startDate = dateToInputDateString(quickSelectRanges.startDate); displayedTextInputValue.value.endDate = dateToInputDateString(quickSelectRanges.endDate); displayedTextInputValue.value.startTime = dateToTimeInputString(quickSelectRanges.startDate); displayedTextInputValue.value.endTime = dateToTimeInputString(quickSelectRanges.endDate); selectedDate.value = quickSelectRanges; } }; </script> <script lang="ts"> /** * Date range picker selector. * * @example * import { BentoDateRangePicker } from '@adyen/bento-vue2'; * * export default { * components: { BentoDateRangePicker }, * template: ` * <bento-date-range-picker * label="Label" * description="Supporting text" * required * optional * disabled * @input="onDateChanged" * v-model="selectedDate" * /> * `, * setup() { * const selectedDate = ref(new Date()); // reactive({ startDate: new Date(), endDate: new Date() }) * return { * selectedDate, * } * } * } */ export default { i18n: { messages }, name: 'bento-date-range-picker', model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./date-range-picker.scss" />
@@ -1 +1 @@
1
- <!-- TODO: Remove aria props as these are part of JSX elements /vue/types/jsx.d.ts --> <!-- eslint-disable vue/attribute-hyphenation --> <template> <div v-bento-click-outside-directive="clickOutsideDropdownOptions" class="b-dropdown" :class="conditionalClasses"> <field-label v-if="label" :id="labelId" :condensed="condensed" :label="label" :tooltip-text="tooltipText" :optional="optional" :required="required" /> <div ref="inputContainerRefWrapper" v-bento-keyboard-navigation-directive v-on="textboxKeyboardNavigationListeners" > <component :is="computedSize" ref="inputContainerRef" :condensed="condensed" :value="searchTerm" :additional-items-selected="additionalItemsSelected" :aria-controls="dropdownOptionsContainerId" :ariaExpanded="isDropdownOpen" :aria-labelledby="label ? labelId : null" :aria-describedby="ariaDescribedBy" :aria-label="computedAriaLabel" :aria-readonly="isReadOnly" :aria-required="required" :readonly="isReadOnly" :disabled="disabled" :display-value="displayValue" :dynamic-filtering="dynamicFiltering" :is-invalid="!!errorMessage || error" :multiple="multiple" :open="isDropdownOpen" :selected-value-item="!isInputComponent ? selectedValueItem : undefined" :show-slot-content-in-multiple="showSlotContentInMultiple" @input="searchTerm = $event" @open="toggleDropdown" @close="toggleDropdown" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </component> </div> <bento-teleport v-if="inputContainerRef" :disabled="teleport?.disabled" :to="teleport?.to"> <bento-dropdown-options-container :id="dropdownOptionsContainerId" ref="optionsContainerRef" :aria-label="computedAriaLabel" :component-loading="componentLoading" :empty-state="emptyState" :disabled="disabled" :is-option-disabled="isOptionDisabled" :items="filteredItems" :multiple="multiple" :static-categories="staticCategories" :open="isDropdownOpen" :selected-value="cachedSelectedListboxOptions" :searching="isSearching" :target-element="inputContainerRefWrapper" :loading="loading" :has-more-items="hasMoreItems" :lazy-load-type="lazyLoadType" :virtual-scroll="virtualScroll" v-on="listboxListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-dropdown-options-container> </bento-teleport> <error-message v-if="!!errorMessage" :id="errorId" :error-message="errorMessage" class="b-dropdown__error-message" /> <bento-typography v-if="description || hasSlot('description')" :id="descriptionId" class="b-dropdown__description" :class="descriptionConditionalClasses" el="span" > <slot name="description"> {{ description }} </slot> </bento-typography> </div> </template> <script setup lang="ts"> import { computed, defineComponent, inject, nextTick, type Ref, ref, toRef, toRefs, useAttrs, useSlots, watch, } from 'vue'; // Components import { BentoDropdownOptionsContainer } from './components/dropdown-options-container'; import { BentoTypography } from '@/components/typography'; import { DropdownInputDefault, DropdownInputSmall } from './components'; import { BentoListbox, useCachedSelectedValues, useMultiLevelItems } from '@/internal/listbox'; import { ErrorMessage } from '@/internal/error-message'; import { FieldLabel } from '@/internal/field-label'; import { BentoTeleport } from '@/internal/teleport'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { generateUid } from '@/core/utils/ts'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; // Composables import { useDisplayValue, useListboxKeyboardNavigation, useTextboxKeyboardNavigation } from './composables'; import { useVisibleDomOptions } from '@/internal/listbox/composables/use-visible-dom-options/use-visible-dom-options'; import { useFormLayoutFieldLoading } from '@/composables/use-form-layout-loading/use-form-layout-loading'; import { useSearchBarFilter } from '@/components/search-bar/useSearchBarDataFilter'; import { useHasSlot } from '@/composables'; // Directives import { BentoClickOutsideDirective as vBentoClickOutsideDirective } from '@/directives/click-outside'; import { BentoKeyboardNavigationDirective as vBentoKeyboardNavigationDirective } from '@/directives'; // Types import { BentoDropdownEvent, type BentoDropdownProps, BentoDropdownSize } from './dropdown.types'; import { BentoListboxEvent, type BentoListboxItemRole, type BentoListboxMultiSelectValue, type BentoListboxOptions, type BentoListboxSelectedValue, type BentoListboxSelectedValueLabelPair, } from '@/types/listbox'; import { DROPDOWN_SMALL_SIZE_INJECTION_KEY } from './dropdown.keys'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = withDefaults(defineProps<BentoDropdownProps>(), { ariaLabel: null, condensed: false, emptyState: undefined, disabled: false, dynamicFiltering: false, error: false, errorMessage: null, description: null, hasMoreItems: false, isOptionDisabled: undefined, items: () => [], label: null, loading: false, componentLoading: false, lazyLoadType: BentoListbox.props.lazyLoadType.default, multiple: false, optional: false, placeholder: null, readonly: false, required: false, search: undefined, showSlotContentInMultiple: false, size: null, staticCategories: false, teleport: () => ({ disabled: true }), tooltipText: null, value: null, modelValue: null, virtualScroll: false, enableValueLabelPair: false, }); const emit = defineEmits<{ /** * Emitted when an option is clicked. Updates the selected option from the "value" property linked to the v-model. * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', selectedValue: BentoListboxSelectedValue | any); // eslint-disable-line @typescript-eslint/no-explicit-any /** * Emitted when an option is clicked. Updates the selected option from the "modelValue" property linked to the v-model. */ (e: 'update:model-value', selectedValue: BentoListboxSelectedValue | any); // eslint-disable-line @typescript-eslint/no-explicit-any /** * Emitted when: * - `lazyLoad` is `automatic` and the scroll meets the end of the list * - `lazyload` is `button` and the button is clicked */ (e: 'show-more'); /** * Emitted when the dropdown is opened. */ (e: 'open'); }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const { emitValue } = useFormFieldEmits<BentoListboxSelectedValue>(emit); const inputContainerRef = ref(null); const inputContainerRefWrapper = ref(null); const optionsContainerRef: Ref<InstanceType<typeof BentoDropdownOptionsContainer>> = ref(null); const isDropdownOpen = ref(false); const dropdownOptionsContainerId = generateUid('bento-dropdown-options-container'); const labelId = generateUid('dropdown-label'); const descriptionId = generateUid('dropdown-description'); const errorId = generateUid('dropdown-error'); const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const dropdownValue = computed(() => props.modelValue ?? props.value); const { cachedSelectedListboxOptions, cachedListboxSelectedValues, setCachedValues } = useCachedSelectedValues( toRef(props, 'items'), dropdownValue, toRef(props, 'multiple'), toRef(props, 'enableValueLabelPair') ); const ariaLabelAttribute = ref(attrs['aria-label']); const ariaDescribedByAttribute = ref(attrs['aria-describedby']); const { ariaLabel, label } = toRefs(props); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => t('ariaLabelFallback')); const computedAriaLabel = useAriaLabel({ // TODO: take ariaLabel ref away when aria-label prop is removed ariaLabel: ariaLabel.value || (ariaLabelAttribute as Ref<string>), label, defaultFallback: computedAriaLabelFallbackMessage, }); // Inject input field component key to change the dropdown size const isInputComponent = props.size === BentoDropdownSize.SMALL || inject(DROPDOWN_SMALL_SIZE_INJECTION_KEY, false); const computedSize = computed(() => (isInputComponent ? BentoDropdownSize.SMALL : BentoDropdownSize.DEFAULT)); const conditionalClasses = computed(() => ({ 'b-dropdown--condensed': props.condensed, })); // Description styles const descriptionConditionalClasses = computed(() => ({ 'b-dropdown__description--error': !!props.errorMessage, })); // Filtering const searchTerm = ref(''); const items = toRef(props, 'items'); const staticCategories = toRef(props, 'staticCategories'); const filteredItems = useSearchBarFilter( searchTerm, items, props?.search?.searchEvent, staticCategories, props?.search?.debounceTime ); const isSearching = computed(() => props.dynamicFiltering && !!searchTerm.value); const displayValue = useDisplayValue({ items, placeholder: toRef(props, 'placeholder'), isMultiSelect: toRef(props, 'multiple'), selectedValues: cachedSelectedListboxOptions, }); const selectedValueItem = computed(() => { if (props?.multiple && Array.isArray(dropdownValue.value)) { return props.items.filter( ({ value }) => props?.value && (props?.value as BentoListboxMultiSelectValue).includes(value) ); } if (!props.multiple && props.staticCategories) { // Find the category that holds the selected value const category = props.items.find(categoryItem => categoryItem.items?.some(item => item.value === dropdownValue.value) ); // Find the specific item within the category const selectedCategoryItem = category ? category.items.find(item => item.value === dropdownValue.value) : props.items.find(({ value }) => value === dropdownValue.value); return selectedCategoryItem ?? null; } return props.items.find(({ value }) => value === dropdownValue.value); }); const selectedValueIndex = computed(() => { const searchList = isMultiLevelSelect.value ? flattenedItemsList.value : filteredItems.value; const index = searchList.findIndex(({ value }) => { return props.enableValueLabelPair ? (dropdownValue.value as BentoListboxSelectedValueLabelPair)?.value === value : dropdownValue.value === value; }); return index > -1 ? index : 0; }); const { isMultiLevelSelect, flattenedItemsList } = useMultiLevelItems(items); const isTreePattern = computed(() => isMultiLevelSelect.value && !props.staticCategories); const computedOptionRole = computed<BentoListboxItemRole>(() => (isTreePattern.value ? 'treeitem' : 'option')); const additionalItemsSelected = computed(() => { if (props.multiple) { const selectedItemsCount = Array.isArray(dropdownValue.value) ? dropdownValue.value.length : 0; const total = isMultiLevelSelect.value ? flattenedItemsList.value.length : items.value.length; if (selectedItemsCount === 0) { return null; } // Display count always - show "all" only when is not external filtering (hasMoreItems = true) if (!props.hasMoreItems && total > 0 && total === selectedItemsCount) { return t('all'); } return selectedItemsCount; } return null; }); // ARIA const ariaDescribedBy = computed( () => [ props.description ? descriptionId : null, ariaDescribedByAttribute.value, props.errorMessage ? errorId : null, ] .filter(Boolean) .join(' ') || null ); const listboxRef = computed(() => optionsContainerRef.value?.$el as HTMLDivElement); const { visibleDomOptions, updateVisibleDomOptions } = useVisibleDomOptions( listboxRef, filteredItems, computedOptionRole ); const openDropdown = () => { if (isReadOnly.value) { return; } isDropdownOpen.value = true; nextTick(() => { updateVisibleDomOptions(); }); }; const closeDropdown = () => { isDropdownOpen.value = false; }; const toggleDropdown = () => { if (isReadOnly.value) { return; } isDropdownOpen.value = !isDropdownOpen.value; nextTick(() => { updateVisibleDomOptions(); }); // Clear search if (isSearching.value) { searchTerm.value = ''; } }; const clickOutsideDropdown = () => { if (isSearching.value) { searchTerm.value = ''; } if (isDropdownOpen.value) { isDropdownOpen.value = false; // Reset the internal dropdown options state optionsContainerRef.value.onOutsideDropdownClick(); } }; const clickOutsideDropdownOptions = computed(() => [ clickOutsideDropdown, { ignore: [listboxRef], }, ]); const onOptionSelected = async (selectedValue: BentoListboxOptions) => { // Clear and close the dropdown on single selection if (!props.multiple) { searchTerm.value = ''; isDropdownOpen.value = false; } setCachedValues(selectedValue); /** * Emit the value/label pair objects if enableValueLabelPair is enabled, * otherwise just emit the list of selected values (number or string) */ emitValue(cachedListboxSelectedValues.value); // Focus the input after selection and dropdown has closed if (!props.multiple) { await nextTick(); focus(); } }; const emitShowMoreEvent = () => { emit(BentoDropdownEvent.SHOW_MORE); }; const textboxKeyboardNavigationListeners = useTextboxKeyboardNavigation( { inputContainerRef, optionsContainerRef, selectedValueIndex, isDropdownOpen, isDynamicFltering: toRef(props, 'dynamicFiltering'), items: visibleDomOptions, isMultiple: props.multiple, totalItemCount: computed(() => filteredItems.value.length), isVirtualScroll: !props?.multiple && props?.virtualScroll !== false && props?.lazyLoadType === 'none', }, toggleDropdown, openDropdown, closeDropdown ); const listboxKeyboardNavigationListeners = useListboxKeyboardNavigation( { inputContainerRef, isDropdownOpen, }, closeDropdown ); const listboxListeners = { [BentoListboxEvent.CLOSE_DROPDOWN]: closeDropdown, [BentoListboxEvent.SELECT]: onOptionSelected, [BentoListboxEvent.SHOW_MORE]: emitShowMoreEvent, ...listboxKeyboardNavigationListeners, }; if (props.error) { deprecate( 'BentoDropdown "error" property', `Use the BentoDropdown "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-dropdown errorMessage="Error message text" />`, '2.0.0' ); } if (props.size) { deprecate('BentoDropdown "size" property', `Do not use. Only 'default' size should be used.`, '2.0.0'); } if (props.value) { deprecate( 'BentoDropdown "value" property', `The use of "value" prop in "BentoDropdown" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } const focus = () => { inputContainerRef.value.focus(); }; const scrollToSelectedOption = async () => { await nextTick(); if (!isDropdownOpen.value || props.multiple || !optionsContainerRef.value) { return; } const index = selectedValueIndex.value; if (index <= 0) { return; } const dropdownOptionsListboxRef = optionsContainerRef.value.dropdownOptionsListboxRef; const listbox = dropdownOptionsListboxRef?.listboxRef; const listboxSingleSelect = dropdownOptionsListboxRef?.listboxItemRef; listboxSingleSelect?.scrollToItem?.(index, listbox); }; watch( () => isDropdownOpen.value, (isOpen: boolean) => { if (isOpen) { emit('open'); scrollToSelectedOption(); } } ); defineExpose({ focus, }); </script> <script lang="ts"> /** * A Dropdown shows a selected option's. * Use a dropdown when you want users to select options * from a list of pre-defined options. * * @example * import { BentoDropdown } from '@adyen/bento-vue2'; * import type { BentoDropdownOptions } from '@adyen/bento-vue2' * * export default { * components: { BentoDropdown }, * template: ` * <bento-dropdown * v-model="selectedValue" * :disabled="false" * :multiple="true" * :isOptionsDisabled="option => option.value === 2" * :items="options" * /> * `, * setup() { * const selectedValue = ref(2) // Default value * const options: BentoDropdownOptions = [ * { label: 'Option 1', value: 1 }, * { label: 'Option 2', value: 2 }, * ]; * return { options, selectedValue }; * } * } */ export default defineComponent({ i18n: { messages }, name: 'b-dropdown', components: { DropdownInputDefault, DropdownInputSmall, }, model: { prop: 'modelValue' }, }); </script> <style lang="scss" scoped src="./dropdown.scss" />
1
+ <!-- TODO: Remove aria props as these are part of JSX elements /vue/types/jsx.d.ts --> <!-- eslint-disable vue/attribute-hyphenation --> <template> <div v-bento-click-outside-directive="clickOutsideDropdownOptions" class="b-dropdown" :class="conditionalClasses"> <field-label v-if="label" :id="labelId" :condensed="condensed" :label="label" :tooltip-text="tooltipText" :optional="optional" :required="required" /> <div ref="inputContainerRefWrapper" v-bento-keyboard-navigation-directive v-on="textboxKeyboardNavigationListeners" > <component :is="computedSize" ref="inputContainerRef" :condensed="condensed" :value="searchTerm" :additional-items-selected="additionalItemsSelected" :aria-controls="dropdownOptionsContainerId" :ariaExpanded="isDropdownOpen" :aria-labelledby="label ? labelId : null" :aria-describedby="ariaDescribedBy" :aria-label="computedAriaLabel" :aria-readonly="isReadOnly" :aria-required="required" :readonly="isReadOnly" :disabled="disabled" :display-value="displayValue" :dynamic-filtering="dynamicFiltering" :is-invalid="!!errorMessage || error" :multiple="multiple" :open="isDropdownOpen" :selected-value-item="!isInputComponent ? selectedValueItem : undefined" :show-slot-content-in-multiple="showSlotContentInMultiple" @input="searchTerm = $event" @open="toggleDropdown" @close="toggleDropdown" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </component> </div> <bento-teleport v-if="inputContainerRef" :disabled="teleport?.disabled" :to="teleport?.to"> <bento-dropdown-options-container :id="dropdownOptionsContainerId" ref="optionsContainerRef" :aria-label="computedAriaLabel" :component-loading="componentLoading" :empty-state="emptyState" :disabled="disabled" :is-option-disabled="isOptionDisabled" :items="filteredItems" :multiple="multiple" :static-categories="staticCategories" :open="isDropdownOpen" :selected-value="cachedSelectedListboxOptions" :searching="isSearching" :target-element="inputContainerRefWrapper" :loading="loading" :has-more-items="hasMoreItems" :lazy-load-type="lazyLoadType" :virtual-scroll="virtualScroll" v-on="listboxListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-dropdown-options-container> </bento-teleport> <error-message v-if="!!errorMessage" :id="errorId" :error-message="errorMessage" class="b-dropdown__error-message" /> <bento-typography v-if="description || hasSlot('description')" :id="descriptionId" class="b-dropdown__description" :class="descriptionConditionalClasses" el="span" > <slot name="description"> {{ description }} </slot> </bento-typography> </div> </template> <script setup lang="ts"> import { computed, defineComponent, inject, nextTick, type Ref, ref, toRef, toRefs, useAttrs, useSlots, watch, } from 'vue'; // Components import { BentoDropdownOptionsContainer } from './components/dropdown-options-container'; import { BentoTypography } from '@/components/typography'; import { DropdownInputDefault, DropdownInputSmall } from './components'; import { BentoListbox, useCachedSelectedValues, useMultiLevelItems } from '@/internal/listbox'; import { ErrorMessage } from '@/internal/error-message'; import { FieldLabel } from '@/internal/field-label'; import { BentoTeleport } from '@/internal/teleport'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { generateUid } from '@/core/utils/ts'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; // Composables import { useDisplayValue, useListboxKeyboardNavigation, useTextboxKeyboardNavigation } from './composables'; import { useVisibleDomOptions } from '@/internal/listbox/composables/use-visible-dom-options/use-visible-dom-options'; import { useFormLayoutFieldLoading } from '@/composables/use-form-layout-loading/use-form-layout-loading'; import { useSearchBarFilter } from '@/components/search-bar/useSearchBarDataFilter'; import { useHasSlot } from '@/composables'; // Directives import { BentoClickOutsideDirective as vBentoClickOutsideDirective } from '@/directives/click-outside'; import { BentoKeyboardNavigationDirective as vBentoKeyboardNavigationDirective } from '@/directives'; // Types import { BentoDropdownEvent, type BentoDropdownProps, BentoDropdownSize } from './dropdown.types'; import { BentoListboxEvent, type BentoListboxItemRole, type BentoListboxMultiSelectValue, type BentoListboxOptions, type BentoListboxSelectedValue, type BentoListboxSelectedValueLabelPair, } from '@/types/listbox'; import { DROPDOWN_SMALL_SIZE_INJECTION_KEY } from './dropdown.keys'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = withDefaults(defineProps<BentoDropdownProps>(), { ariaLabel: null, condensed: false, emptyState: undefined, disabled: false, dynamicFiltering: false, error: false, errorMessage: null, description: null, hasMoreItems: false, isOptionDisabled: undefined, items: () => [], label: null, loading: false, componentLoading: false, lazyLoadType: BentoListbox.props.lazyLoadType.default, multiple: false, optional: false, placeholder: null, readonly: false, required: false, search: undefined, showSlotContentInMultiple: false, size: null, staticCategories: false, teleport: () => ({ disabled: true }), tooltipText: null, value: null, modelValue: null, virtualScroll: false, enableValueLabelPair: false, }); const emit = defineEmits<{ /** * Emitted when an option is clicked. Updates the selected option from the "value" property linked to the v-model. * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', selectedValue: BentoListboxSelectedValue | any); // eslint-disable-line @typescript-eslint/no-explicit-any /** * Emitted when an option is clicked. Updates the selected option from the "modelValue" property linked to the v-model. */ (e: 'update:model-value', selectedValue: BentoListboxSelectedValue | any); // eslint-disable-line @typescript-eslint/no-explicit-any /** * Emitted when: * - `lazyLoad` is `automatic` and the scroll meets the end of the list * - `lazyload` is `button` and the button is clicked */ (e: 'show-more'); /** * Emitted when the dropdown is opened. */ (e: 'open'); }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const { emitValue } = useFormFieldEmits<BentoListboxSelectedValue>(emit); const inputContainerRef = ref(null); const inputContainerRefWrapper = ref(null); const optionsContainerRef: Ref<InstanceType<typeof BentoDropdownOptionsContainer>> = ref(null); const isDropdownOpen = ref(false); const dropdownOptionsContainerId = generateUid('bento-dropdown-options-container'); const labelId = generateUid('dropdown-label'); const descriptionId = generateUid('dropdown-description'); const errorId = generateUid('dropdown-error'); const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const dropdownValue = computed(() => props.modelValue ?? props.value); const { cachedSelectedListboxOptions, cachedListboxSelectedValues, setCachedValues } = useCachedSelectedValues( toRef(props, 'items'), dropdownValue, toRef(props, 'multiple'), toRef(props, 'enableValueLabelPair') ); const ariaLabelAttribute = ref(attrs['aria-label']); const ariaDescribedByAttribute = ref(attrs['aria-describedby']); const { ariaLabel, label } = toRefs(props); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => t('ariaLabelFallback')); const computedAriaLabel = useAriaLabel({ // TODO: take ariaLabel ref away when aria-label prop is removed ariaLabel: ariaLabel.value || (ariaLabelAttribute as Ref<string>), label, defaultFallback: computedAriaLabelFallbackMessage, }); // Inject input field component key to change the dropdown size const isInputComponent = props.size === BentoDropdownSize.SMALL || inject(DROPDOWN_SMALL_SIZE_INJECTION_KEY, false); const computedSize = computed(() => (isInputComponent ? BentoDropdownSize.SMALL : BentoDropdownSize.DEFAULT)); const conditionalClasses = computed(() => ({ 'b-dropdown--condensed': props.condensed, })); // Description styles const descriptionConditionalClasses = computed(() => ({ 'b-dropdown__description--error': !!props.errorMessage, })); // Filtering const searchTerm = ref(''); const items = toRef(props, 'items'); const staticCategories = toRef(props, 'staticCategories'); const filteredItems = useSearchBarFilter( searchTerm, items, props?.search?.searchEvent, staticCategories, props?.search?.debounceTime ); const isSearching = computed(() => props.dynamicFiltering && !!searchTerm.value); const displayValue = useDisplayValue({ items, placeholder: toRef(props, 'placeholder'), isMultiSelect: toRef(props, 'multiple'), selectedValues: cachedSelectedListboxOptions, }); const selectedValueItem = computed(() => { if (props?.multiple && Array.isArray(dropdownValue.value)) { return props.items.filter( ({ value }) => props?.value && (props?.value as BentoListboxMultiSelectValue).includes(value) ); } if (!props.multiple && props.staticCategories) { // Find the category that holds the selected value const category = props.items.find(categoryItem => categoryItem.items?.some(item => item.value === dropdownValue.value) ); // Find the specific item within the category const selectedCategoryItem = category ? category.items.find(item => item.value === dropdownValue.value) : props.items.find(({ value }) => value === dropdownValue.value); return selectedCategoryItem ?? null; } return props.items.find(({ value }) => value === dropdownValue.value); }); const selectedValueIndex = computed(() => { const searchList = isMultiLevelSelect.value ? flattenedItemsList.value : filteredItems.value; const index = searchList.findIndex(({ value }) => { return props.enableValueLabelPair ? (dropdownValue.value as BentoListboxSelectedValueLabelPair)?.value === value : dropdownValue.value === value; }); return index > -1 ? index : 0; }); const { isMultiLevelSelect, flattenedItemsList } = useMultiLevelItems(items); const isTreePattern = computed(() => isMultiLevelSelect.value && !props.staticCategories); const computedOptionRole = computed<BentoListboxItemRole>(() => (isTreePattern.value ? 'treeitem' : 'option')); const additionalItemsSelected = computed(() => { if (props.multiple) { const selectedItemsCount = Array.isArray(dropdownValue.value) ? dropdownValue.value.length : 0; const total = isMultiLevelSelect.value ? flattenedItemsList.value.length : items.value.length; if (selectedItemsCount === 0) { return null; } // Display count always - show "all" only when is not external filtering (hasMoreItems = true) if (!props.hasMoreItems && total > 0 && total === selectedItemsCount) { return t('all'); } return selectedItemsCount; } return null; }); // ARIA const ariaDescribedBy = computed( () => [ props.description ? descriptionId : null, ariaDescribedByAttribute.value, props.errorMessage ? errorId : null, ] .filter(Boolean) .join(' ') || null ); const listboxRef = computed(() => { const dropdownListboxRef = optionsContainerRef.value?.dropdownOptionsListboxRef; // Vue 3 exposes the ref object, while Vue 2 exposes the unwrapped DOM element. return (dropdownListboxRef?.listboxRef?.value ?? dropdownListboxRef?.listboxRef) as HTMLDivElement; }); const { visibleDomOptions, updateVisibleDomOptions } = useVisibleDomOptions( listboxRef, filteredItems, computedOptionRole ); const openDropdown = () => { if (isReadOnly.value) { return; } isDropdownOpen.value = true; nextTick(() => { updateVisibleDomOptions(); }); }; const closeDropdown = () => { isDropdownOpen.value = false; }; const toggleDropdown = () => { if (isReadOnly.value) { return; } isDropdownOpen.value = !isDropdownOpen.value; nextTick(() => { updateVisibleDomOptions(); }); // Clear search if (isSearching.value) { searchTerm.value = ''; } }; const clickOutsideDropdown = () => { if (isSearching.value) { searchTerm.value = ''; } if (isDropdownOpen.value) { isDropdownOpen.value = false; // Reset the internal dropdown options state optionsContainerRef.value.onOutsideDropdownClick(); } }; const clickOutsideDropdownOptions = computed(() => [ clickOutsideDropdown, { ignore: [listboxRef], }, ]); const onOptionSelected = async (selectedValue: BentoListboxOptions) => { // Clear and close the dropdown on single selection if (!props.multiple) { searchTerm.value = ''; isDropdownOpen.value = false; } setCachedValues(selectedValue); /** * Emit the value/label pair objects if enableValueLabelPair is enabled, * otherwise just emit the list of selected values (number or string) */ emitValue(cachedListboxSelectedValues.value); // Focus the input after selection and dropdown has closed if (!props.multiple) { await nextTick(); focus(); } }; const emitShowMoreEvent = () => { emit(BentoDropdownEvent.SHOW_MORE); }; const textboxKeyboardNavigationListeners = useTextboxKeyboardNavigation( { inputContainerRef, optionsContainerRef, selectedValueIndex, isDropdownOpen, isDynamicFltering: toRef(props, 'dynamicFiltering'), items: visibleDomOptions, isMultiple: props.multiple, totalItemCount: computed(() => filteredItems.value.length), isVirtualScroll: !props?.multiple && props?.virtualScroll !== false && props?.lazyLoadType === 'none', }, toggleDropdown, openDropdown, closeDropdown ); const listboxKeyboardNavigationListeners = useListboxKeyboardNavigation( { inputContainerRef, isDropdownOpen, }, closeDropdown ); const listboxListeners = { [BentoListboxEvent.CLOSE_DROPDOWN]: closeDropdown, [BentoListboxEvent.SELECT]: onOptionSelected, [BentoListboxEvent.SHOW_MORE]: emitShowMoreEvent, ...listboxKeyboardNavigationListeners, }; if (props.error) { deprecate( 'BentoDropdown "error" property', `Use the BentoDropdown "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-dropdown errorMessage="Error message text" />`, '2.0.0' ); } if (props.size) { deprecate('BentoDropdown "size" property', `Do not use. Only 'default' size should be used.`, '2.0.0'); } if (props.value) { deprecate( 'BentoDropdown "value" property', `The use of "value" prop in "BentoDropdown" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } const focus = () => { inputContainerRef.value?.focus(); }; const scrollToSelectedOption = async () => { await nextTick(); if (!isDropdownOpen.value || props.multiple || !optionsContainerRef.value) { return; } const index = selectedValueIndex.value; if (index <= 0) { return; } const dropdownOptionsListboxRef = optionsContainerRef.value.dropdownOptionsListboxRef; const listbox = dropdownOptionsListboxRef?.listboxRef; const listboxSingleSelect = dropdownOptionsListboxRef?.listboxItemRef; listboxSingleSelect?.scrollToItem?.(index, listbox); }; watch( () => isDropdownOpen.value, (isOpen: boolean) => { if (isOpen) { emit('open'); scrollToSelectedOption(); } } ); defineExpose({ focus, }); </script> <script lang="ts"> /** * A Dropdown shows a selected option's. * Use a dropdown when you want users to select options * from a list of pre-defined options. * * @example * import { BentoDropdown } from '@adyen/bento-vue2'; * import type { BentoDropdownOptions } from '@adyen/bento-vue2' * * export default { * components: { BentoDropdown }, * template: ` * <bento-dropdown * v-model="selectedValue" * :disabled="false" * :multiple="true" * :isOptionsDisabled="option => option.value === 2" * :items="options" * /> * `, * setup() { * const selectedValue = ref(2) // Default value * const options: BentoDropdownOptions = [ * { label: 'Option 1', value: 1 }, * { label: 'Option 2', value: 2 }, * ]; * return { options, selectedValue }; * } * } */ export default defineComponent({ i18n: { messages }, name: 'b-dropdown', components: { DropdownInputDefault, DropdownInputSmall, }, model: { prop: 'modelValue' }, }); </script> <style lang="scss" scoped src="./dropdown.scss" />
@@ -1 +1 @@
1
- <template> <bento-typography class="b-input-field" v-bind="getRootAttributes($attrs)" :data-testid="getDataTestId($attrs)" :class="inputFieldConditionalClasses" el="div" variant="body" > <field-label v-if="hasSlot('default') || label" :for="inputFieldId" data-testid="input-field-label" :tooltip-text="tooltipText" :optional="optional" :required="required" :label="label" @click="focusInput" > <slot></slot> </field-label> <div class="b-input-field__input-box" data-testid="input-box" :aria-disabled="disabled" @click="focusInput"> <!-- Optional Icon for Default text variant --> <span v-if="isDefaultVariant && (hasSlot('defaultIconBefore') || hasSlot('iconBefore'))" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-before" > <slot v-if="hasSlot('iconBefore')" name="iconBefore" /> <slot v-if="hasSlot('defaultIconBefore') && !hasSlot('iconBefore')" name="defaultIconBefore" /> </span> <!-- Mandatory Icon for Payment Method variant --> <span v-if="isPaymentMethodVariant && hasSlot('paymentMethod')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__payment-method" > <slot name="paymentMethod" /> </span> <!-- Dropdown at start --> <div v-if="shouldDisplayDropdownAtStart" :id="dropdownId" class="b-input-field__dropdown b-input-field__dropdown--start" @click.stop > <bento-dropdown v-if="dropdown" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> <!-- Static Value at start --> <bento-typography v-if="shouldDisplayStaticValueAtStart" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--start" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <div class="b-input-field__input-container"> <input :id="inputFieldId" v-bind="$attrs" ref="inputFieldElement" class="b-input-field__input" :aria-label="computedAriaLabel" :aria-describedby="computedAriaDescribedBy" :aria-owns="computedAriaOwns" :aria-invalid="shouldShowError" :placeholder="placeholder" :required="required" :aria-required="required" :type="type" :value="modelValue ?? value" :disabled="disabled" :readonly="isReadOnly" @input="onInput" @change="emit('change')" @focus="onFocus" @blur="onBlur" @keydown="emit('keydown', $event)" @keyup="emit('keyup', $event)" @keydown.esc="emit('escape-pressed')" @click="emit('click', $event)" /> <!-- Hint at the end --> <bento-typography v-if="hint && isFocused && (modelValue || value)" el="span" class="b-input-field__hint" > {{ hint }} </bento-typography> </div> <span v-if="hasSlot('iconAfter')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-after" data-testid="input-text__icon-after" > <slot name="iconAfter" /> </span> <!-- Static Value at the end --> <bento-typography v-if="shouldDisplayStaticValueAtEnd" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--end" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <!-- Dropdown at end --> <div v-if="shouldDisplayDropdownAtEnd" :id="dropdownId" data-testid="input-field-dropdown-container-end" class="b-input-field__dropdown b-input-field__dropdown--end" @click.stop > <bento-dropdown v-if="dropdown" :id="dropdownId" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> </div> <error-message v-if="shouldShowError && !!errorMessage" :id="errorId" :error-message="errorMessage" class="b-input-field__error-message" /> <span v-if="hasSlot('description') || description" :id="descriptionId" class="b-input-field__description" @click="focusInput" > <bento-typography el="span" variant="body"> <slot id="description" name="description"> {{ description }} </slot> </bento-typography> </span> </bento-typography> </template> <script setup lang="ts"> import { computed, inject, provide, type Ref, ref, toRef, toRefs, useAttrs, useSlots } from 'vue'; // Components import { BentoDropdown } from '@/components/dropdown'; import { DROPDOWN_SMALL_SIZE_INJECTION_KEY } from '@/components/dropdown/dropdown.keys'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { generateUid } from '@/core/utils/ts'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { INPUT_FIELD_HINT_INJECTION_KEY, INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, } from '@/components/input-field/input-field.keys'; import { type BentoListboxSelectedValue } from '@/types/listbox'; // Composables import { useFormLayoutFieldLoading, useHasSlot } from '@/composables'; // Types import { type BentoInputDropdownProps, BentoInputFieldElementPosition, type BentoInputFieldProps, BentoInputFieldStateClass, BentoInputFieldType, type BentoInputFieldValue, BentoInputFieldVariant, } from './input-field.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const props = withDefaults(defineProps<BentoInputFieldProps>(), { ariaHidden: true, ariaLabel: undefined, condensed: false, description: '', disabled: false, dropdown: undefined, dropdownPosition: BentoInputFieldElementPosition.START, error: false, errorMessage: null, label: '', modelValue: undefined, optional: false, placeholder: '', required: false, readonly: false, slashedZero: false, staticValue: '', staticValuePosition: BentoInputFieldElementPosition.START, tooltipText: null, type: BentoInputFieldType.TEXT, value: undefined, variant: BentoInputFieldVariant.DEFAULT, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the input is clicked */ (e: 'click', event: MouseEvent); /** * Emitted when the user modifies the element's value. Unlike the 'input' event, the change event is not necessarily fired for each alteration to an element's value */ (e: 'change'): void; /** * Emitted when the Input Field's internal dropdown changes it's value */ (e: 'dropdown-input', selectedValue: BentoListboxSelectedValue): void; /** * Emitted when the "ESC" key is pressed */ (e: 'escape-pressed'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when a key is pressed and lifted up */ (e: 'keyup', event: KeyboardEvent): void; /** * Emitted when the component's model changes * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', inputValue: string): void; /** * Emitted when the Input Field's internal dropdown changes it's value. Contains every dropdown prop passed with the updated selected value as 'value' */ (e: 'update:dropdown', updatedDropdownProps: BentoInputDropdownProps): void; /** * Emitted when the component's model changes */ (e: 'update:model-value', inputValue: BentoInputFieldValue): void; }>(); const { emitValue } = useFormFieldEmits<BentoInputFieldValue>(emit); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const hasSlot = useHasSlot(slots); const inputFieldElement = ref<HTMLInputElement>(null); const inputDropdownElement = ref(null); const inputFieldId = generateUid('input'); const dropdownId = generateUid('input-dropdown'); const staticValueId = generateUid('input-static-value'); const descriptionId = generateUid('input-description'); const errorId = generateUid('input-error'); const isFocused = ref(false); /** * Gets the overriden or the default data-testid attribute * @param attributes - the live attributes object */ function getDataTestId(attributes: Record<string, unknown>) { return (attributes['data-testid'] as string) ?? 'input-text-container'; } /** * Filters out all aria attributes for the root element. * @param attributes - the live attributes object */ function getRootAttributes(attributes: Record<string, unknown>) { return Object.fromEntries( Object.entries(attributes).filter(([key]) => !key.startsWith('aria-') && key !== 'data-testid') ); } // Provide DROPDOWN_SMALL_SIZE_INJECTION_KEY as true, to set the input only dropdown size variant in the BentoDropdown comp provide(DROPDOWN_SMALL_SIZE_INJECTION_KEY, true); // Renders the input with a hint if a string is provided const hint = inject<Ref<string | undefined>>(INPUT_FIELD_HINT_INJECTION_KEY, ref(undefined)); const onFocus = () => { isFocused.value = true; emit('focus'); }; const onBlur = () => { isFocused.value = false; emit('blur'); }; const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const isDropdownReadOnly = computed<BentoInputDropdownProps['readonly']>( () => isReadOnly.value || props.dropdown?.readonly ); const shouldShowStaticValue = computed( () => props.variant === BentoInputFieldVariant.STATIC_VALUE && !!slots.staticValue ); const isDefaultVariant = computed(() => props.variant === BentoInputFieldVariant.DEFAULT); const isPaymentMethodVariant = computed(() => props.variant === BentoInputFieldVariant.PAYMENT_METHOD); const shouldDisplayStaticValueAtStart = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.START ); const shouldDisplayStaticValueAtEnd = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.END ); const shouldShowDropdown = computed(() => props.variant === BentoInputFieldVariant.DROPDOWN && !!props.dropdown); const shouldDisplayDropdownAtStart = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.START ); const shouldDisplayDropdownAtEnd = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.END ); const shouldShowError = computed( () => !props.disabled && !isReadOnly.value && (!!props.errorMessage || props.error) ); const computedLabel = computed(() => (getSlotText(slots)('default') as string) || props.label); const computedDescription = computed(() => (getSlotText(slots)('description') as string) || props.description); const computedAriaOwns = computed(() => { if (shouldShowDropdown.value === true && shouldShowStaticValue.value === true) { return `${staticValueId} ${dropdownId}`; } if (shouldShowDropdown.value === true) { return `${dropdownId}`; } if (shouldShowStaticValue.value === true) { return `${staticValueId}`; } return null; }); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => attrs?.['aria-labelledby'] ? undefined : t('ariaLabelFallback') ); const computedAriaLabelDropdowFallback = computed(() => t('ariaLabelDropdownFallback', { inputLabel: computedAriaLabel.value }) ); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel } = toRefs(props); const computedAriaLabel = useAriaLabel({ ariaLabel: ariaLabelAttribute.value as string, // TODO: take this line away when the aria-label props is removed label: ariaLabel, defaultFallback: computedLabel.value || computedAriaLabelFallbackMessage, }); const computedAriaDescribedBy = computed( () => `${computedDescription.value ? descriptionId : ''} ${ shouldShowError.value && !!props.errorMessage ? errorId : '' }` ); const inputFieldConditionalClasses = computed(() => ({ [`b-input-field--${BentoInputFieldStateClass.CONDENSED}`]: props.condensed, [`b-input-field--${BentoInputFieldStateClass.DISABLED}`]: props.disabled, [`b-input-field--${BentoInputFieldStateClass.READONLY}`]: isReadOnly.value, [`b-input-field--${BentoInputFieldStateClass.ERROR}`]: shouldShowError.value, [`b-input-field--slashed-zero`]: props.slashedZero, })); const inputFieldDropdownProps = computed(() => ({ 'aria-label': computedAriaLabelDropdowFallback.value, ...props.dropdown, })); const onInput = (event: Event) => { const inputValue = (event.target as HTMLInputElement).value; if (!props.disabled && !isReadOnly.value) { emitValue(inputValue); } }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { emit('dropdown-input', selectedValue); emit('update:dropdown', { ...props?.dropdown, value: selectedValue }); }; const focusInput = () => { inputFieldElement.value.focus(); }; const focus = () => { if (props.variant === 'dropdown') { inputDropdownElement.value.focus(); } else { focusInput(); } }; // Expose focus methods for parent components in Vue 3. defineExpose({ /** * Focuses the input field. */ focusInput, /** * Focuses the dropdown when using the dropdown variant; otherwise focuses the input field. */ focus, /** * Reference to the native input element. */ inputFieldElement, }); const inputFieldParentComponent = inject(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, ''); if (!Object.values(BentoInputFieldType).includes(props.type as BentoInputFieldType) && !inputFieldParentComponent) { if (props.type === 'tel') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-phone-number" instead.` ); } else if (props.type === 'password') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-password" instead.` ); } else { printDevelopmentWarning(`"bento-input-field" dropdown does not support the type '${props.type}'`); } } /** * Deprecations */ if (props.error) { deprecate( 'BentoInputField "error" property', `Use the BentoInputField "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-dropdown errorMessage="Error message text" />`, '2.0.0' ); } if (hasSlot('defaultIconBefore')) { deprecate( 'BentoInputField "defaultIconBefore" slot', `Use the BentoInputField "iconBefore" slot instead to add an icon at the left side of the input field <bento-input-field> \t<template #iconBefore> \t\t<my-icon-goes-here /> \t</template> </bento-input-field>`, '2.0.0' ); } if (props.value) { deprecate( 'BentoInputField "value" property', `The use of "value" prop in "BentoInputField" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field component is used to create interactive controls for web-based * forms in order to accept data from the user. * * @example * import { BentoInputField } from '@adyen/bento-vue2' * * const inputValue = ref(''); * * export default { * components: { BentoInputField }, * tempate: ` * <bento-input-field * :model-value="inputValue" * :@update:model-value="newValue => inputValue.value = newValue" * > * } */ export default { name: 'bento-input-field', inheritAttrs: false, model: { prop: 'modelValue' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./input-field.scss" />
1
+ <template> <bento-typography class="b-input-field" v-bind="getRootAttributes($attrs)" :data-testid="getDataTestId($attrs)" :class="inputFieldConditionalClasses" el="div" variant="body" > <field-label v-if="hasSlot('default') || label" :for="inputFieldId" data-testid="input-field-label" :tooltip-text="tooltipText" :optional="optional" :required="required" :label="label" @click="focusInput" > <slot></slot> </field-label> <div class="b-input-field__input-box" data-testid="input-box" :aria-disabled="disabled" @click="focusInput"> <!-- Optional Icon for Default text variant --> <span v-if="isDefaultVariant && (hasSlot('defaultIconBefore') || hasSlot('iconBefore'))" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-before" > <slot v-if="hasSlot('iconBefore')" name="iconBefore" /> <slot v-if="hasSlot('defaultIconBefore') && !hasSlot('iconBefore')" name="defaultIconBefore" /> </span> <!-- Mandatory Icon for Payment Method variant --> <span v-if="isPaymentMethodVariant && hasSlot('paymentMethod')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__payment-method" > <slot name="paymentMethod" /> </span> <!-- Dropdown at start --> <div v-if="shouldDisplayDropdownAtStart" :id="dropdownId" class="b-input-field__dropdown b-input-field__dropdown--start" @click.stop > <bento-dropdown v-if="dropdown" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> <!-- Static Value at start --> <bento-typography v-if="shouldDisplayStaticValueAtStart" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--start" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <div class="b-input-field__input-container"> <input :id="inputFieldId" v-bind="$attrs" ref="inputFieldElement" class="b-input-field__input" :aria-label="computedAriaLabel" :aria-describedby="computedAriaDescribedBy" :aria-owns="computedAriaOwns" :aria-invalid="shouldShowError" :placeholder="placeholder" :required="required" :aria-required="required" :type="type" :value="modelValue ?? value" :disabled="disabled" :readonly="isReadOnly" @input="onInput" @change="emit('change')" @focus="onFocus" @blur="onBlur" @keydown="emit('keydown', $event)" @keyup="emit('keyup', $event)" @keydown.esc="emit('escape-pressed')" @click="emit('click', $event)" /> <!-- Hint at the end --> <bento-typography v-if="hint && isFocused && (modelValue || value)" el="span" class="b-input-field__hint" > {{ hint }} </bento-typography> </div> <span v-if="hasSlot('iconAfter')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-after" data-testid="input-text__icon-after" > <slot name="iconAfter" /> </span> <!-- Static Value at the end --> <bento-typography v-if="shouldDisplayStaticValueAtEnd" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--end" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <!-- Dropdown at end --> <div v-if="shouldDisplayDropdownAtEnd" :id="dropdownId" data-testid="input-field-dropdown-container-end" class="b-input-field__dropdown b-input-field__dropdown--end" @click.stop > <bento-dropdown v-if="dropdown" :id="dropdownId" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> </div> <div v-if="(shouldShowError && !!errorMessage) || hasSlot('description') || description" class="b-input-field__footer" > <error-message v-if="shouldShowError && !!errorMessage" :id="errorId" :error-message="errorMessage" /> <span v-if="hasSlot('description') || description" :id="descriptionId" class="b-input-field__description" @click="focusInput" > <bento-typography el="span" variant="body"> <slot id="description" name="description"> {{ description }} </slot> </bento-typography> </span> </div> </bento-typography> </template> <script setup lang="ts"> import { computed, inject, provide, type Ref, ref, toRef, toRefs, useAttrs, useSlots } from 'vue'; // Components import { BentoDropdown } from '@/components/dropdown'; import { DROPDOWN_SMALL_SIZE_INJECTION_KEY } from '@/components/dropdown/dropdown.keys'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { generateUid } from '@/core/utils/ts'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { INPUT_FIELD_HINT_INJECTION_KEY, INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, } from '@/components/input-field/input-field.keys'; import { type BentoListboxSelectedValue } from '@/types/listbox'; // Composables import { useFormLayoutFieldLoading, useHasSlot } from '@/composables'; // Types import { type BentoInputDropdownProps, BentoInputFieldElementPosition, type BentoInputFieldProps, BentoInputFieldStateClass, BentoInputFieldType, type BentoInputFieldValue, BentoInputFieldVariant, } from './input-field.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const props = withDefaults(defineProps<BentoInputFieldProps>(), { ariaHidden: true, ariaLabel: undefined, condensed: false, description: '', disabled: false, dropdown: undefined, dropdownPosition: BentoInputFieldElementPosition.START, error: false, errorMessage: null, label: '', modelValue: undefined, optional: false, placeholder: '', required: false, readonly: false, slashedZero: false, staticValue: '', staticValuePosition: BentoInputFieldElementPosition.START, tooltipText: null, type: BentoInputFieldType.TEXT, value: undefined, variant: BentoInputFieldVariant.DEFAULT, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the input is clicked */ (e: 'click', event: MouseEvent); /** * Emitted when the user modifies the element's value. Unlike the 'input' event, the change event is not necessarily fired for each alteration to an element's value */ (e: 'change'): void; /** * Emitted when the Input Field's internal dropdown changes it's value */ (e: 'dropdown-input', selectedValue: BentoListboxSelectedValue): void; /** * Emitted when the "ESC" key is pressed */ (e: 'escape-pressed'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when a key is pressed and lifted up */ (e: 'keyup', event: KeyboardEvent): void; /** * Emitted when the component's model changes * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', inputValue: string): void; /** * Emitted when the Input Field's internal dropdown changes it's value. Contains every dropdown prop passed with the updated selected value as 'value' */ (e: 'update:dropdown', updatedDropdownProps: BentoInputDropdownProps): void; /** * Emitted when the component's model changes */ (e: 'update:model-value', inputValue: BentoInputFieldValue): void; }>(); const { emitValue } = useFormFieldEmits<BentoInputFieldValue>(emit); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const hasSlot = useHasSlot(slots); const inputFieldElement = ref<HTMLInputElement>(null); const inputDropdownElement = ref(null); const inputFieldId = generateUid('input'); const dropdownId = generateUid('input-dropdown'); const staticValueId = generateUid('input-static-value'); const descriptionId = generateUid('input-description'); const errorId = generateUid('input-error'); const isFocused = ref(false); /** * Gets the overriden or the default data-testid attribute * @param attributes - the live attributes object */ function getDataTestId(attributes: Record<string, unknown>) { return (attributes['data-testid'] as string) ?? 'input-text-container'; } /** * Filters out all aria attributes for the root element. * @param attributes - the live attributes object */ function getRootAttributes(attributes: Record<string, unknown>) { return Object.fromEntries( Object.entries(attributes).filter(([key]) => !key.startsWith('aria-') && key !== 'data-testid') ); } // Provide DROPDOWN_SMALL_SIZE_INJECTION_KEY as true, to set the input only dropdown size variant in the BentoDropdown comp provide(DROPDOWN_SMALL_SIZE_INJECTION_KEY, true); // Renders the input with a hint if a string is provided const hint = inject<Ref<string | undefined>>(INPUT_FIELD_HINT_INJECTION_KEY, ref(undefined)); const onFocus = () => { isFocused.value = true; emit('focus'); }; const onBlur = () => { isFocused.value = false; emit('blur'); }; const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const isDropdownReadOnly = computed<BentoInputDropdownProps['readonly']>( () => isReadOnly.value || props.dropdown?.readonly ); const shouldShowStaticValue = computed( () => props.variant === BentoInputFieldVariant.STATIC_VALUE && !!slots.staticValue ); const isDefaultVariant = computed(() => props.variant === BentoInputFieldVariant.DEFAULT); const isPaymentMethodVariant = computed(() => props.variant === BentoInputFieldVariant.PAYMENT_METHOD); const shouldDisplayStaticValueAtStart = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.START ); const shouldDisplayStaticValueAtEnd = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.END ); const shouldShowDropdown = computed(() => props.variant === BentoInputFieldVariant.DROPDOWN && !!props.dropdown); const shouldDisplayDropdownAtStart = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.START ); const shouldDisplayDropdownAtEnd = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.END ); const shouldShowError = computed( () => !props.disabled && !isReadOnly.value && (!!props.errorMessage || props.error) ); const computedLabel = computed(() => (getSlotText(slots)('default') as string) || props.label); const computedDescription = computed(() => (getSlotText(slots)('description') as string) || props.description); const computedAriaOwns = computed(() => { if (shouldShowDropdown.value === true && shouldShowStaticValue.value === true) { return `${staticValueId} ${dropdownId}`; } if (shouldShowDropdown.value === true) { return `${dropdownId}`; } if (shouldShowStaticValue.value === true) { return `${staticValueId}`; } return null; }); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => attrs?.['aria-labelledby'] ? undefined : t('ariaLabelFallback') ); const computedAriaLabelDropdowFallback = computed(() => t('ariaLabelDropdownFallback', { inputLabel: computedAriaLabel.value }) ); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel } = toRefs(props); const computedAriaLabel = useAriaLabel({ ariaLabel: ariaLabelAttribute.value as string, // TODO: take this line away when the aria-label props is removed label: ariaLabel, defaultFallback: computedLabel.value || computedAriaLabelFallbackMessage, }); const computedAriaDescribedBy = computed( () => `${computedDescription.value ? descriptionId : ''} ${ shouldShowError.value && !!props.errorMessage ? errorId : '' }` ); const inputFieldConditionalClasses = computed(() => ({ [`b-input-field--${BentoInputFieldStateClass.CONDENSED}`]: props.condensed, [`b-input-field--${BentoInputFieldStateClass.DISABLED}`]: props.disabled, [`b-input-field--${BentoInputFieldStateClass.READONLY}`]: isReadOnly.value, [`b-input-field--${BentoInputFieldStateClass.ERROR}`]: shouldShowError.value, [`b-input-field--slashed-zero`]: props.slashedZero, })); const inputFieldDropdownProps = computed(() => ({ 'aria-label': computedAriaLabelDropdowFallback.value, ...props.dropdown, })); const onInput = (event: Event) => { const inputValue = (event.target as HTMLInputElement).value; if (!props.disabled && !isReadOnly.value) { emitValue(inputValue); } }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { emit('dropdown-input', selectedValue); emit('update:dropdown', { ...props?.dropdown, value: selectedValue }); }; const focusInput = () => { inputFieldElement.value.focus(); }; const focus = () => { if (props.variant === 'dropdown') { inputDropdownElement.value.focus(); } else { focusInput(); } }; // Expose focus methods for parent components in Vue 3. defineExpose({ /** * Focuses the input field. */ focusInput, /** * Focuses the dropdown when using the dropdown variant; otherwise focuses the input field. */ focus, /** * Reference to the native input element. */ inputFieldElement, }); const inputFieldParentComponent = inject(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, ''); if (!Object.values(BentoInputFieldType).includes(props.type as BentoInputFieldType) && !inputFieldParentComponent) { if (props.type === 'tel') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-phone-number" instead.` ); } else if (props.type === 'password') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-password" instead.` ); } else { printDevelopmentWarning(`"bento-input-field" dropdown does not support the type '${props.type}'`); } } /** * Deprecations */ if (props.error) { deprecate( 'BentoInputField "error" property', `Use the BentoInputField "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-dropdown errorMessage="Error message text" />`, '2.0.0' ); } if (hasSlot('defaultIconBefore')) { deprecate( 'BentoInputField "defaultIconBefore" slot', `Use the BentoInputField "iconBefore" slot instead to add an icon at the left side of the input field <bento-input-field> \t<template #iconBefore> \t\t<my-icon-goes-here /> \t</template> </bento-input-field>`, '2.0.0' ); } if (props.value) { deprecate( 'BentoInputField "value" property', `The use of "value" prop in "BentoInputField" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field component is used to create interactive controls for web-based * forms in order to accept data from the user. * * @example * import { BentoInputField } from '@adyen/bento-vue2' * * const inputValue = ref(''); * * export default { * components: { BentoInputField }, * tempate: ` * <bento-input-field * :model-value="inputValue" * :@update:model-value="newValue => inputValue.value = newValue" * > * } */ export default { name: 'bento-input-field', inheritAttrs: false, model: { prop: 'modelValue' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./input-field.scss" />
@@ -0,0 +1 @@
1
+ import { type BentoButtonActionsList } from '@/components/button/components/button-actions/button-actions.types'; export interface BentoInspectorPageProps { /** * List of action buttons rendered in the page footer. * * @default undefined */ actions?: BentoButtonActionsList; /** * Unique identifier for this page within the inspector navigation. */ pageId: string; /** * Page ID to navigate back to when leaving this page. * * @default null */ previousPage?: string; /** * Heading text displayed in the page header. */ title: string; }
@@ -0,0 +1 @@
1
+ <template> <dialog-page class="b-inspector-page" :page-id="pageId" :previous-page="previousPage" :absolute-position="false"> <template #header> <div class="b-inspector-page__header"> <bento-typography el="h2" variant="title" class="b-inspector-page__title"> {{ title }} </bento-typography> </div> </template> <template #content> <div class="b-inspector-page__content"> <bento-typography el="div" variant="body"> <slot></slot> </bento-typography> </div> </template> <template v-if="actions && actions.length" #footer> <div class="b-inspector-page__actions"> <footer-actions :actions="actions" /> </div> </template> </dialog-page> </template> <script setup lang="ts"> import { DialogPage } from '@/internal/dialog-page'; import { BentoTypography } from '@/components/typography'; import { FooterActions } from '@/internal/footer-actions'; import { type BentoInspectorPageProps } from './inspector-page.types'; withDefaults(defineProps<BentoInspectorPageProps>(), { actions: undefined, previousPage: null, }); </script> <style lang="scss" scoped src="./inspector-page.scss" />
@@ -52,6 +52,20 @@ Use `large` (595px) inspectors:
52
52
  - To provide a more expansive layout for detailed information.
53
53
  - To accommodate rich content such as images and charts.
54
54
 
55
+ ## Navigation
56
+
57
+ Inspector supports deeper level navigation (user can go deeper in hierarchy and go back) through the use of the
58
+ `bento-inspector-page` component.
59
+
60
+ Each `bento-inspector-page` should have a `pageId`, which can then be passed to the `active-page` prop in
61
+ `bento-inspector` to control which page is shown.
62
+
63
+ Unlike the sidepanel, the inspector does not track navigation history automatically. This means that every sub page
64
+ **must** set the `previous-page` prop to the `pageId` of its parent page. This is what renders the back arrow in the
65
+ header and links the sub page back to its parent.
66
+
67
+ <Canvas of={InspectorStories.MultiplePages} />
68
+
55
69
  ## Modifiers
56
70
 
57
71
  ### Title
@@ -1 +1 @@
1
- import { isVue2 } from 'vue-demi'; import { computed, ref, watch } from 'vue'; import { BentoButton } from '@/components/button'; import { BentoDataGrid } from '@/components/data-grid'; import { BentoStructuredList, BentoStructuredListItem } from '@/components/structured-list'; import { BentoTypography } from '@/components/typography'; import { BentoHeader } from '@/components/header'; import PlusIcon from '@adyen/ui-assets-icons-16/vue/plus'; import SearchIcon from '@adyen/ui-assets-icons-16/vue/search'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoInspector from './inspector.vue'; import type { Meta, StoryObj } from '@storybook/vue'; import { BentoInspectorSizeOptions } from './inspector.types'; import './inspector.stories.scss?module'; const meta: Meta = { title: 'Inspector', component: BentoInspector, parameters: { layout: 'fullscreen', }, argTypes: { size: { options: BentoInspectorSizeOptions, control: { type: 'select', }, }, relativeLayout: { options: ['push-content', 'overlay-content'], control: { type: 'select', }, }, }, }; export default meta; type Story = StoryObj<typeof BentoInspector>; const DEFAULT_PROPS = { title: 'Title', size: 'large', isOpen: false, activePage: '', relativeLayout: 'push-content', }; const defaultCode = ` <template> <bento-inspector :is-open="isOpen" @update:is-open="isOpen = $event" :active-page="activePage" @update:active-page="activePage = $event" title="Inspector title" expandable > <template #page> // Your page content <bento-button :aria-expanded="isOpen" @click="openInspector">{{ inspectorButtonTitle }}</bento-button> </template> <template #content> // Your inspector content </template> </bento-inspector> </template> <script setup lang="ts"> import { ref } from 'vue'; import { BentoButton, BentoInspector } from '@adyen/bento-vue2'; const isOpen = ref(false); const activePage = ref('') const inspectorButtonTitle = computed(() => (isOpen.value ? 'Move to inspector' : 'Open inspector')); const openInspector = () => { isOpen.value = true; activePage.value = 'PageId1234'; }; </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoDataGrid, BentoHeader, BentoInspector, BentoStructuredList, BentoStructuredListItem, BentoTypography, SearchIcon, }, props: Object.keys(argTypes), template: ` <bento-inspector v-bind="args" :is-open="isOpen" :title="title" :active-page="activePage" @update:is-open="isOpen = $event" @update:active-page="activePage = $event" expandable > <template #page> <div style="padding:16px"> <bento-header v-bind="headerProps"></bento-header> <bento-data-grid v-bind="gridArgs"> <template #item-paymentMethods="{item}"> <div> {{ item.paymentMethods }} <bento-button variant="tertiary" @click="openInspector(item)" style="float:right" > <template #iconLeft> <search-icon :svg-title="inspectorButtonTitle" /> </template> </bento-button> </div> </template> </bento-data-grid> </div> </template> <template #content> <div style="display: flex; flex-direction: column; height: 100%"> <bento-structured-list style="flex-grow: 1"> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> </bento-structured-list> <div> <bento-button style="float: right">Action</bento-button> </div> </div> </template> </bento-inspector> `, setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(args.isOpen); const title = ref(args.title); const activePage = ref(''); const inspectorButtonTitle = computed(() => (isOpen.value ? 'Move to inspector' : 'Open inspector')); const gridData = [ { id: 1, paymentMethods: 'Visa', amount: '1,234 USD', numeric: '54,213' }, { id: 2, paymentMethods: 'MasterCard', amount: '1,234 USD', numeric: '54,213' }, { id: 3, paymentMethods: 'iDeal', amount: '1,234 USD', numeric: '54,213' }, { id: 4, paymentMethods: 'Klarna', amount: '1,234 USD', numeric: '54,213' }, { id: 5, paymentMethods: 'Apple Pay', amount: '1,234 USD', numeric: '54,213' }, ]; const openInspector = value => { title.value = value.paymentMethods; isOpen.value = true; activePage.value = value.paymentMethods; }; const gridColumns = [ { field: 'paymentMethods', label: 'Payment methods', minWidth: 200, mandatory: true, }, { field: 'amount', label: 'Amount', numeric: true, minWidth: 300 }, { field: 'numeric', label: 'Numeric', numeric: true, minWidth: 300 }, ]; const gridArgs = { data: gridData, columns: gridColumns, pagination: {}, }; const headerProps = { title: 'Payment methods list', actions: [ { title: 'Primary action', event: () => undefined }, { title: 'Secondary action', icon: PlusIcon, event: () => undefined }, { title: 'Tertiary action', icon: PlusIcon, event: () => undefined }, { title: 'Another action', event: () => undefined }, ], lastUpdated: new Date(), }; watch( () => args.isOpen, () => { isOpen.value = args.isOpen; } ); watch( () => args.title, () => { title.value = args.title; } ); watch( () => args.activePage, () => { activePage.value = args.activePage; } ); return { args, isOpen, gridArgs, title, headerProps, activePage, openInspector, inspectorButtonTitle, }; }, }), args: DEFAULT_PROPS, parameters: storybookDocsParameter(defaultCode), };
1
+ import { isVue2 } from 'vue-demi'; import { computed, ref, watch } from 'vue'; import { BentoButton } from '@/components/button'; import { BentoDataGrid } from '@/components/data-grid'; import { BentoStructuredList, BentoStructuredListItem } from '@/components/structured-list'; import { BentoTypography } from '@/components/typography'; import { BentoHeader } from '@/components/header'; import { BentoInspectorPage } from './components/inspector-page/index.js'; import BentoInspector from './inspector.vue'; import InspectorWithMultiplePagesExample from './__tests__/inspector-with-multiple-pages-example.vue?raw'; import PlusIcon from '@adyen/ui-assets-icons-16/vue/plus'; import SearchIcon from '@adyen/ui-assets-icons-16/vue/search'; import ShieldIcon from '@adyen/ui-assets-icons-16/vue/shield'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import type { Meta, StoryObj } from '@storybook/vue'; import { BentoInspectorSizeOptions } from './inspector.types'; import './inspector.stories.scss?module'; const meta: Meta = { title: 'Inspector', component: BentoInspector, parameters: { layout: 'fullscreen', }, argTypes: { size: { options: BentoInspectorSizeOptions, control: { type: 'select', }, }, relativeLayout: { options: ['push-content', 'overlay-content'], control: { type: 'select', }, }, }, }; export default meta; type Story = StoryObj<typeof BentoInspector>; const DEFAULT_PROPS = { title: 'Title', size: 'large', isOpen: false, activePage: '', relativeLayout: 'push-content', }; const defaultCode = ` <template> <bento-inspector :is-open="isOpen" @update:is-open="isOpen = $event" :active-page="activePage" @update:active-page="activePage = $event" title="Inspector title" expandable > <template #page> // Your page content <bento-button :aria-expanded="isOpen" @click="openInspector">{{ inspectorButtonTitle }}</bento-button> </template> <template #content> // Your inspector content </template> </bento-inspector> </template> <script setup lang="ts"> import { ref } from 'vue'; import { BentoButton, BentoInspector } from '@adyen/bento-vue2'; const isOpen = ref(false); const activePage = ref('') const inspectorButtonTitle = computed(() => (isOpen.value ? 'Move to inspector' : 'Open inspector')); const openInspector = () => { isOpen.value = true; activePage.value = 'PageId1234'; }; </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoDataGrid, BentoHeader, BentoInspector, BentoStructuredList, BentoStructuredListItem, BentoTypography, SearchIcon, }, props: Object.keys(argTypes), template: ` <bento-inspector v-bind="args" :is-open="isOpen" :title="title" :active-page="activePage" @update:is-open="isOpen = $event" @update:active-page="activePage = $event" expandable > <template #page> <div style="padding:16px"> <bento-header v-bind="headerProps"></bento-header> <bento-data-grid v-bind="gridArgs"> <template #item-paymentMethods="{item}"> <div> {{ item.paymentMethods }} <bento-button variant="tertiary" @click="openInspector(item)" style="float:right" > <template #iconLeft> <search-icon :svg-title="inspectorButtonTitle" /> </template> </bento-button> </div> </template> </bento-data-grid> </div> </template> <template #content> <div style="display: flex; flex-direction: column; height: 100%"> <bento-structured-list style="flex-grow: 1"> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> </bento-structured-list> <div> <bento-button style="float: right">Action</bento-button> </div> </div> </template> </bento-inspector> `, setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(args.isOpen); const title = ref(args.title); const activePage = ref(''); const inspectorButtonTitle = computed(() => (isOpen.value ? 'Move to inspector' : 'Open inspector')); const gridData = [ { id: 1, paymentMethods: 'Visa', amount: '1,234 USD', numeric: '54,213' }, { id: 2, paymentMethods: 'MasterCard', amount: '1,234 USD', numeric: '54,213' }, { id: 3, paymentMethods: 'iDeal', amount: '1,234 USD', numeric: '54,213' }, { id: 4, paymentMethods: 'Klarna', amount: '1,234 USD', numeric: '54,213' }, { id: 5, paymentMethods: 'Apple Pay', amount: '1,234 USD', numeric: '54,213' }, ]; const openInspector = value => { title.value = value.paymentMethods; isOpen.value = true; activePage.value = value.paymentMethods; }; const gridColumns = [ { field: 'paymentMethods', label: 'Payment methods', minWidth: 200, mandatory: true, }, { field: 'amount', label: 'Amount', numeric: true, minWidth: 300 }, { field: 'numeric', label: 'Numeric', numeric: true, minWidth: 300 }, ]; const gridArgs = { data: gridData, columns: gridColumns, pagination: {}, }; const headerProps = { title: 'Payment methods list', actions: [ { title: 'Primary action', event: () => undefined }, { title: 'Secondary action', icon: PlusIcon, event: () => undefined }, { title: 'Tertiary action', icon: PlusIcon, event: () => undefined }, { title: 'Another action', event: () => undefined }, ], lastUpdated: new Date(), }; watch( () => args.isOpen, () => { isOpen.value = args.isOpen; } ); watch( () => args.title, () => { title.value = args.title; } ); watch( () => args.activePage, () => { activePage.value = args.activePage; } ); return { args, isOpen, gridArgs, title, headerProps, activePage, openInspector, inspectorButtonTitle, }; }, }), args: DEFAULT_PROPS, parameters: storybookDocsParameter(defaultCode), }; export const MultiplePages: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoDataGrid, BentoHeader, BentoInspector, BentoInspectorPage, BentoStructuredList, BentoStructuredListItem, BentoTypography, SearchIcon, ShieldIcon, }, props: Object.keys(argTypes), template: ` <bento-inspector v-bind="args" :is-open="isOpen" :active-page="activePage" title="What if this title is really really long? So long that it should overflow the content on the right where the buttons are" @update:is-open="isOpen = $event" @update:active-page="onUpdateActivePage" expandable > <template #page> <div style="padding:16px"> <bento-header v-bind="headerProps"></bento-header> <bento-data-grid v-bind="gridArgs"> <template #item-paymentMethods="{item}"> <div> {{ item.paymentMethods }} <bento-button variant="tertiary" @click="openInspector(item)" style="float:right" > <template #iconLeft> <search-icon :svg-title="inspectorButtonTitle" /> </template> </bento-button> <bento-button v-if="item.paymentMethods === 'Visa'" variant="tertiary" @click="openInspector(item, true)" style="float:right; margin-right: 10px;" > <template #iconLeft> <shield-icon :svg-title="inspectorButtonTitle" /> </template> </bento-button> </div> </template> </bento-data-grid> </div> </template> <template #content> <bento-inspector-page page-id="visa-secure" title="Visa secure with a wrap text title" :actions="actions"> <p style="margin-top: 0;">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus accumsan dui sed nulla tincidunt fermentum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec tristique erat ut tincidunt aliquet. Nunc placerat aliquam consequat. Suspendisse non leo ut sem rutrum aliquet et at ex. Mauris facilisis arcu nec ipsum auctor, imperdiet fringilla tortor placerat. Aenean bibendum dui id velit ornare, eget porta tortor euismod.</p> <p>Phasellus posuere nec enim sed sodales. Mauris aliquet tristique nisl vitae sollicitudin. Aliquam nec diam lorem. Pellentesque eu ex condimentum, rhoncus ipsum et, varius elit. In lacinia elit eros, vel euismod nisl vestibulum et. Vivamus quis dui vel nibh facilisis venenatis nec molestie mi. Praesent porta suscipit elit, ut porta risus. Nunc gravida euismod convallis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris in ornare lorem.</p> <p>Ut nec nisl erat. Nunc arcu ante, lobortis eu lorem eget, maximus cursus nunc. Etiam tortor tellus, condimentum a libero in, pellentesque dignissim turpis. Phasellus ac sodales dui. Proin at auctor leo. Nunc vehicula ut lorem eget tempus. Quisque lectus dui, auctor vitae ipsum nec, dapibus mollis justo. Donec purus lectus, malesuada non laoreet eu, sodales ac turpis.</p> <p>Mauris venenatis, eros et fringilla vulputate, ante velit euismod nisi, blandit volutpat eros elit nec mi. Donec rutrum felis vel nibh aliquet finibus. Mauris gravida dapibus dui, ut consectetur risus. Sed sagittis dignissim lacus nec ornare. Fusce ullamcorper pretium metus id congue. Vestibulum pellentesque sem orci, eget bibendum magna maximus vel. Sed id lectus ullamcorper, imperdiet velit ac, porttitor est. Duis risus nisi, facilisis at pharetra pellentesque, hendrerit malesuada sapien. Aenean tellus nisl, rhoncus sed nisl quis, sagittis fermentum mauris. Etiam rutrum leo et ligula varius volutpat. Fusce semper hendrerit accumsan. Nam efficitur vitae velit nec condimentum. Vestibulum pellentesque nec purus nec tristique. In libero ante, dictum at urna in, interdum pretium felis. Ut scelerisque sed sem a malesuada.</p> <p>Vestibulum elementum malesuada arcu, id aliquet nisi blandit eu. Proin vel sagittis odio. Maecenas sollicitudin neque non urna malesuada, at convallis sem vulputate. Ut volutpat leo at tincidunt malesuada. Quisque pharetra turpis eget velit commodo, eu scelerisque risus ultrices. Etiam non faucibus libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Mauris in odio massa.</p> </bento-inspector-page> <bento-inspector-page page-id="visa-secure-sub" title="Sub Visa secure" previous-page="visa-secure" > I'm a sub page of visa </bento-inspector-page> <bento-inspector-page page-id="payment-details" :title="title"> <div style="display: flex; flex-direction: column; height: 100%"> <bento-structured-list style="flex-grow: 1"> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> </bento-structured-list> </div> </bento-inspector-page> </template> </bento-inspector> `, setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(args.isOpen); const title = ref(args.title); const activePage = ref(); const inspectorButtonTitle = computed(() => (isOpen.value ? 'Move to inspector' : 'Open inspector')); const gridData = [ { id: 1, paymentMethods: 'Visa', amount: '1,234 USD', numeric: '54,213' }, { id: 2, paymentMethods: 'MasterCard', amount: '1,234 USD', numeric: '54,213' }, { id: 3, paymentMethods: 'iDeal', amount: '1,234 USD', numeric: '54,213' }, { id: 4, paymentMethods: 'Klarna', amount: '1,234 USD', numeric: '54,213' }, { id: 5, paymentMethods: 'Apple Pay', amount: '1,234 USD', numeric: '54,213' }, ]; const onUpdateActivePage = (newActivePage: string) => { activePage.value = newActivePage; }; const openInspector = (value, isSecureView) => { if (isSecureView) { activePage.value = 'visa-secure'; } else { activePage.value = 'payment-details'; title.value = value.paymentMethods; } isOpen.value = true; }; const gridColumns = [ { field: 'paymentMethods', label: 'Payment methods', minWidth: 200, mandatory: true, }, { field: 'amount', label: 'Amount', numeric: true, minWidth: 300 }, { field: 'numeric', label: 'Numeric', numeric: true, minWidth: 300 }, ]; const gridArgs = { data: gridData, columns: gridColumns, pagination: {}, }; const actions = [ { title: 'Visa sub', event: () => { activePage.value = 'visa-secure-sub'; }, }, ]; const headerProps = { title: 'Payment methods list', actions: [ { title: 'Primary action', event: () => undefined }, { title: 'Secondary action', icon: PlusIcon, event: () => undefined }, { title: 'Tertiary action', icon: PlusIcon, event: () => undefined }, { title: 'Another action', event: () => undefined }, ], lastUpdated: new Date(), }; watch( () => args.isOpen, () => { isOpen.value = args.isOpen; } ); watch( () => args.title, () => { title.value = args.title; } ); return { args, isOpen, actions, gridArgs, title, headerProps, activePage, openInspector, onUpdateActivePage, inspectorButtonTitle, }; }, }), args: DEFAULT_PROPS, parameters: storybookDocsParameter(InspectorWithMultiplePagesExample), };
@@ -1 +1 @@
1
- export const BentoInspectorSizeOptions = ['small', 'large'] as const; export type BentoInspectorRelativeLayout = 'push-content' | 'overlay-content'; export type BentoInspectorSize = (typeof BentoInspectorSizeOptions)[number]; export interface BentoInspectorProps { /** * ID of the current active inspector page */ activePage?: string; /** * Shows the Expand icon in the inspector */ expandable?: boolean; /** * Opens and closes the inspector */ isOpen?: boolean; /** * Shows loading indicator for the content */ loading?: boolean; /** * Determines the size of the inspector */ size?: BentoInspectorSize; /** * The title of the component */ title: string; /** * The appearance of the inspector relative to the main content. Defaults to `push-content`. * * - `push-content`: The inspector pushes the main content to the side, resizing it to fit. * - `overlay-content`: The inspector opens on top of the main content, without affecting its layout. */ relativeLayout?: BentoInspectorRelativeLayout; /** * An optional HTMLElement to focus on when returning focus from the inspector * to the main page content (e.g. via the `Ctrl+Shift+S` keyboard shortcut). * * By default, the inspector attempts to return focus to the element that was active before it opened. * However, if that trigger element is removed from the DOM (e.g., via a `v-if`), focus cannot be returned. * * Providing a `pageFocusAnchor` (e.g. role="gridcell") ensures that the * focus context is preserved for accessibility. */ pageFocusAnchor?: HTMLElement; }
1
+ export const BentoInspectorSizeOptions = ['small', 'large'] as const; export type BentoInspectorRelativeLayout = 'push-content' | 'overlay-content'; export type BentoInspectorSize = (typeof BentoInspectorSizeOptions)[number]; export interface BentoInspectorProps { /** * ID of the current active inspector page */ activePage?: string; /** * Shows the Expand icon in the inspector */ expandable?: boolean; /** * Opens and closes the inspector */ isOpen?: boolean; /** * Shows loading indicator for the content */ loading?: boolean; /** * Determines the size of the inspector */ size?: BentoInspectorSize; /** * The title of the component. * Used for accessibility and the single page inspector. * When using `bento-inspector-page` this title is only used for accessibility purposes */ title: string; /** * The appearance of the inspector relative to the main content. Defaults to `push-content`. * * - `push-content`: The inspector pushes the main content to the side, resizing it to fit. * - `overlay-content`: The inspector opens on top of the main content, without affecting its layout. */ relativeLayout?: BentoInspectorRelativeLayout; /** * An optional HTMLElement to focus on when returning focus from the inspector * to the main page content (e.g. via the `Ctrl+Shift+S` keyboard shortcut). * * By default, the inspector attempts to return focus to the element that was active before it opened. * However, if that trigger element is removed from the DOM (e.g., via a `v-if`), focus cannot be returned. * * Providing a `pageFocusAnchor` (e.g. role="gridcell") ensures that the * focus context is preserved for accessibility. */ pageFocusAnchor?: HTMLElement; }
@@ -1 +1 @@
1
- <template> <div ref="wrapper" class="b-inspector" :class="conditionalClass" @keydown="handleSectionFocus"> <div ref="page" class="b-inspector__page-wrapper"> <div class="b-inspector__page"> <slot name="page" /> </div> </div> <Transition name="b-inspector__animation" mode="out-in" @before-enter="setFocus" @after-leave="setFocus"> <aside v-if="isOpen" ref="inspector" class="b-inspector__container-wrapper" :aria-label="inspectorAriaLabel" @keydown.tab="handleTabNavigation" > <bento-focus-trap :disabled="!isCompact"> <div class="b-inspector__container"> <div class="b-inspector__header"> <template v-if="!isCompact"> <bento-button class="b-inspector__return-content" variant="secondary" condensed :aria-describedby="returnToMainContentDescriptionId" @click="moveFocusBetweenSections" > {{ t('returnToMainContent') }} </bento-button> <span :id="returnToMainContentDescriptionId" class="b-inspector__return-content-description" >{{ t('returnToMainContentDescription') }}</span > </template> <bento-typography el="h2" variant="title" class="b-inspector__title"> {{ title }} </bento-typography> <div class="b-inspector__icons"> <bento-button v-if="expandable" variant="tertiary" @click="emit('expand')"> <template #iconLeft><expand-icon :svg-title="t('expand')"></expand-icon></template> </bento-button> <bento-button variant="tertiary" @click="emit('update:is-open', false)"> <template #iconLeft><cross-icon :svg-title="t('close')"></cross-icon></template> </bento-button> </div> </div> <div class="b-inspector__content"> <bento-loading-indicator v-if="loading" large class="b-inspector__loading-indicator" /> <slot v-else name="content" /> </div> </div> </bento-focus-trap> </aside> </Transition> </div> </template> <script setup lang="ts"> import { computed, nextTick, ref, watch } from 'vue'; import { BentoTypography } from '@/components/typography'; import { BentoButton } from '@/components/button'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoFocusTrap } from '@/components/focus-trap'; import ExpandIcon from '@adyen/ui-assets-icons-16/vue/expand'; import CrossIcon from '@adyen/ui-assets-icons-16/vue/cross'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { deprecate } from '@/utils/ts/deprecate'; import { getNextFocusableElement } from '@/utils/ts/focus-trap.utils'; import { useElementSize } from '@vueuse/core'; import moveFocusInside, { focusInside, getActiveElement } from 'focus-lock'; import { type BentoInspectorProps } from './inspector.types'; import { useFocusHistory } from './composables/use-focus-history/use-focus-history'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoInspectorProps>(), { activePage: '', expandable: false, isOpen: false, loading: false, size: 'small', relativeLayout: 'push-content', pageFocusAnchor: null, }); const emit = defineEmits<{ /** * Emits update to keep isOpen prop in sync */ (e: 'update:is-open', value: boolean): void; /** * Emits update to keep activePage prop in sync */ (e: 'update:active-page', value: string): void; /** * Emits event when user click expand button */ (e: 'expand'): void; }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const returnToMainContentDescriptionId = generateUid('returnToMainContentDescription'); const wrapper = ref<HTMLElement>(null); const inspector = ref<HTMLElement>(null); const page = ref<HTMLElement>(null); const inspectorLastFocusedElement = ref<HTMLElement>(null); const { width: wrapperWidth } = useElementSize(wrapper); const isCompact = computed(() => { const COMPACT_THRESHOLD_SMALL = 740; const COMPACT_THRESHOLD_LARGE = 915; // small inspector to occupy full screen if < 740px // large inspector to occupy full screen if < 915px const threshold = props.size === 'small' ? COMPACT_THRESHOLD_SMALL : COMPACT_THRESHOLD_LARGE; return wrapperWidth.value > 0 && wrapperWidth.value < threshold; }); const computedPageLastFocusedElement = computed(() => props.pageFocusAnchor); const conditionalClass = computed(() => ({ [`b-inspector--${props.size}`]: true, [`b-inspector--${props.relativeLayout}`]: true, [`b-inspector--is-compact`]: isCompact.value, })); const inspectorAriaLabel = computed(() => `${t('inspector')}, ${props.title}`); const { findValidFocusFallback } = useFocusHistory(page); const setFocus = async () => { if (props.isOpen) { await nextTick(); moveFocusInside(inspector.value, computedPageLastFocusedElement.value); } else { // This doesn't work with data grid cell actions, as the button gets destroyed once is not being hovered. // To be reviewed after LevelAccess revision focusElement(computedPageLastFocusedElement.value ?? findValidFocusFallback()); } }; const handleTabNavigation = event => { if (isCompact.value) { return; } const direction = event.shiftKey ? -1 : 1; const focusableEl = getNextFocusableElement(direction, inspector.value); // If the nextfocusableEl is the same as the activeElement that means it's the last focusable element // inside the inspector, so the focus is moved back to the last focused element on the page if (getActiveElement() === focusableEl) { event.preventDefault(); inspectorLastFocusedElement.value = document.activeElement as HTMLElement; focusElement(computedPageLastFocusedElement.value ?? findValidFocusFallback()); // Set active-page as undefined when focus is not on page emit('update:active-page', undefined); } }; const handleSectionFocus = event => { const sectionShortcut = event.shiftKey && event.ctrlKey && event.key === 'S'; if (props.isOpen && !isCompact.value && sectionShortcut) { moveFocusBetweenSections(); } }; const moveFocusBetweenSections = () => { if (focusInside(inspector.value)) { inspectorLastFocusedElement.value = document.activeElement as HTMLElement; focusElement(computedPageLastFocusedElement.value ?? findValidFocusFallback()); // Set active-page as undefined when focus is not on page emit('update:active-page', undefined); } else { focusElement(inspectorLastFocusedElement.value); } }; const focusElement = (element: HTMLElement) => { if (!element) { return; } element?.focus(); if (document.activeElement !== element) { focusElement(element.parentElement); } }; watch( () => props.activePage, () => { if (props.activePage) { setFocus(); } } ); if (!props.activePage) { deprecate( 'BentoInspector "active-page" as optional', `Make sure to set the "active-page" prop and keep it in sync with "update:active-page". "active-page" prop will be required in the next version`, '2.0.0' ); } </script> <script lang="ts"> /** * Inspector allows users to access additional information or details of an object/item. The inspector opens adjacent to the main content, pushing it aside to maintain visibility and accessibility to both the main content and inspector. * * @example * import { BentoInspector } from '@adyen/bento-vue2'; * * export default { * components: { BentoInspector }, * template: ` * <bento-inspector * title="Inspector" * size="large" * :is-open="isOpen" * :active-page="activePage" * expandable * @update:is-open="isOpen = $event" * @update:active-page="activePage = $event" * > * <template #page>Content of the page</template> * <template #content>Content of the inspector</template> * </bento-inspector> * ` * } */ export default { i18n: { messages } }; </script> <style lang="scss" scoped src="./inspector.scss" />
1
+ <template> <div ref="wrapper" class="b-inspector" :class="conditionalClass" @keydown="handleSectionFocus"> <div ref="page" class="b-inspector__page-wrapper"> <div class="b-inspector__page"> <slot name="page"></slot> </div> </div> <Transition name="b-inspector__animation" mode="out-in" @before-enter="setFocus" @after-leave="setFocus"> <aside v-if="isOpen" ref="inspector" class="b-inspector__container-wrapper" :aria-label="inspectorAriaLabel" @keydown.tab="handleTabNavigation" > <bento-focus-trap :disabled="!isCompact" class="b-inspector__focus-trap"> <div class="b-inspector__container" :class="computedContainerClasses"> <template v-if="!isCompact"> <bento-button class="b-inspector__return-content" variant="secondary" condensed :aria-describedby="returnToMainContentDescriptionId" @click="moveFocusBetweenSections" > {{ t('returnToMainContent') }} </bento-button> <span :id="returnToMainContentDescriptionId" class="b-inspector__return-content-description" > {{ t('returnToMainContentDescription') }} </span> </template> <div v-if="pages.length === 0" class="b-inspector__header"> <bento-typography el="h2" variant="title" class="b-inspector__title"> {{ title }} </bento-typography> </div> <div class="b-inspector__icons"> <bento-button v-if="expandable" variant="tertiary" @click="emit('expand')"> <template #iconLeft> <expand-icon :svg-title="t('expand')" /> </template> </bento-button> <bento-button variant="tertiary" @click="emit('update:is-open', false)"> <template #iconLeft> <cross-icon :svg-title="t('close')" /> </template> </bento-button> </div> <div class="b-inspector__content"> <bento-loading-indicator v-if="loading" large class="b-inspector__loading-indicator" /> <slot v-else name="content"></slot> </div> </div> </bento-focus-trap> </aside> </Transition> </div> </template> <script setup lang="ts"> import { computed, nextTick, provide, ref, toRef, watch } from 'vue'; import { BentoTypography } from '@/components/typography'; import { BentoButton } from '@/components/button'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoFocusTrap } from '@/components/focus-trap'; import ExpandIcon from '@adyen/ui-assets-icons-16/vue/expand'; import CrossIcon from '@adyen/ui-assets-icons-16/vue/cross'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { deprecate } from '@/utils/ts/deprecate'; import { getNextFocusableElement } from '@/utils/ts/focus-trap.utils'; import { useElementSize } from '@vueuse/core'; import moveFocusInside, { focusInside, getActiveElement } from 'focus-lock'; import { type BentoInspectorProps } from './inspector.types'; import { useFocusHistory } from './composables/use-focus-history/use-focus-history'; import { DIALOG_PAGE_CONFIG_INJECTION_KEY, DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY, } from '@/internal/dialog-page/dialog-page.keys'; import { BentoDialogPageAnimation, type BentoDialogPageConfigData, type BentoDialogPageRegisterPage, } from '@/internal/dialog-page/dialog-page.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoInspectorProps>(), { activePage: '', expandable: false, isOpen: false, loading: false, size: 'small', relativeLayout: 'push-content', pageFocusAnchor: null, }); const emit = defineEmits<{ /** * Emits update to keep isOpen prop in sync */ (e: 'update:is-open', value: boolean): void; /** * Emits update to keep activePage prop in sync */ (e: 'update:active-page', value: string): void; /** * Emits event when user click expand button */ (e: 'expand'): void; }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const returnToMainContentDescriptionId = generateUid('returnToMainContentDescription'); const wrapper = ref<HTMLElement>(null); const inspector = ref<HTMLElement>(null); const page = ref<HTMLElement>(null); const inspectorLastFocusedElement = ref<HTMLElement>(null); const internalActivePage = ref(props.activePage); // Multiple page navigation const pages = ref<Array<string>>([]); const previousPages = ref([]); const animation = ref(BentoDialogPageAnimation.TRANSITION_LEVEL_DEEPER); const isAnimationActive = ref(false); const { width: wrapperWidth } = useElementSize(wrapper); const computedContainerClasses = computed(() => { return { 'b-inspector__container--padded': pages.value.length === 0, // No pages are registered }; }); const isCompact = computed(() => { const COMPACT_THRESHOLD_SMALL = 740; const COMPACT_THRESHOLD_LARGE = 915; // small inspector to occupy full screen if < 740px // large inspector to occupy full screen if < 915px const threshold = props.size === 'small' ? COMPACT_THRESHOLD_SMALL : COMPACT_THRESHOLD_LARGE; return wrapperWidth.value > 0 && wrapperWidth.value < threshold; }); const computedPageLastFocusedElement = computed(() => props.pageFocusAnchor); const conditionalClass = computed(() => ({ [`b-inspector--${props.size}`]: true, [`b-inspector--${props.relativeLayout}`]: true, [`b-inspector--is-compact`]: isCompact.value, [`b-inspector--multi-paged`]: pages.value.length, })); const inspectorAriaLabel = computed(() => `${t('inspector')}, ${props.title}`); const { findValidFocusFallback } = useFocusHistory(page); const setFocus = async () => { if (props.isOpen) { await nextTick(); moveFocusInside(inspector.value, computedPageLastFocusedElement.value); } else { // This doesn't work with data grid cell actions, as the button gets destroyed once is not being hovered. // To be reviewed after LevelAccess revision focusElement(computedPageLastFocusedElement.value ?? findValidFocusFallback()); } }; const handleTabNavigation = event => { if (isCompact.value) { return; } const direction = event.shiftKey ? -1 : 1; const focusableEl = getNextFocusableElement(direction, inspector.value); // If the nextfocusableEl is the same as the activeElement that means it's the last focusable element // inside the inspector, so the focus is moved back to the last focused element on the page if (getActiveElement() === focusableEl) { event.preventDefault(); inspectorLastFocusedElement.value = document.activeElement as HTMLElement; focusElement(computedPageLastFocusedElement.value ?? findValidFocusFallback()); // Set active-page as undefined when focus is not on page emit('update:active-page', undefined); } }; const handleSectionFocus = event => { const sectionShortcut = event.shiftKey && event.ctrlKey && event.key === 'S'; if (props.isOpen && !isCompact.value && sectionShortcut) { moveFocusBetweenSections(); } }; const moveFocusBetweenSections = () => { if (focusInside(inspector.value)) { inspectorLastFocusedElement.value = document.activeElement as HTMLElement; focusElement(computedPageLastFocusedElement.value ?? findValidFocusFallback()); // Set active-page as undefined when focus is not on page emit('update:active-page', undefined); } else { focusElement(inspectorLastFocusedElement.value); } }; const focusElement = (element: HTMLElement) => { if (!element) { return; } element?.focus(); if (document.activeElement !== element) { focusElement(element.parentElement); } }; /** * Multiple page navigation and page registration */ const updatePage = page => emit('update:active-page', page); // unregister page if removed / destroyed const unregisterPage = (pageIdToRemove: string) => () => { pages.value = pages.value?.filter(pageId => pageId !== pageIdToRemove); }; const registerPage: BentoDialogPageRegisterPage = pageId => { pages.value.push(pageId); return { unregisterPage: unregisterPage(pageId) }; }; provide<BentoDialogPageConfigData>(DIALOG_PAGE_CONFIG_INJECTION_KEY, { activePage: internalActivePage, animation, isAnimationActive, isOpen: toRef(props, 'isOpen'), pages, previousPages, updatePage, }); provide(DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY, registerPage); watch( () => props.activePage, () => { if (props.activePage) { internalActivePage.value = props.activePage; setFocus(); } } ); if (!props.activePage) { deprecate( 'BentoInspector "active-page" as optional', `Make sure to set the "active-page" prop and keep it in sync with "update:active-page". "active-page" prop will be required in the next version`, '2.0.0' ); } </script> <script lang="ts"> /** * Inspector allows users to access additional information or details of an object/item. The inspector opens adjacent to the main content, pushing it aside to maintain visibility and accessibility to both the main content and inspector. * * @example * import { BentoInspector } from '@adyen/bento-vue2'; * * export default { * components: { BentoInspector }, * template: ` * <bento-inspector * title="Inspector" * size="large" * :is-open="isOpen" * :active-page="activePage" * expandable * @update:is-open="isOpen = $event" * @update:active-page="activePage = $event" * > * <template #page>Content of the page</template> * <template #content>Content of the inspector</template> * </bento-inspector> * ` * } */ export default { i18n: { messages } }; </script> <style lang="scss" scoped src="./inspector.scss" />
@@ -1 +1 @@
1
- <template> <Transition :name="animation" mode="out-in" @after-leave="updateAnimation" @leave="startAnimation"> <div v-if="isOpen && showPage" class="b-dialog-page"> <div v-if="hasSlot('header')" class="b-dialog-page__header" :class="headerConditionalClasses"> <div v-if="showBackButton" class="b-dialog-page__back-button" :style="backButtonStyling" data-testid="back-button" > <bento-button variant="tertiary" @click="goToPreviousPage(previousPage)"> <arrow-left-icon :svg-title="t('goToPreviousPage')" /> </bento-button> </div> <slot name="header"></slot> </div> <div v-if="hasSlot('content')" ref="contentElement" class="b-dialog-page__content" :tabindex="isScrollable ? 0 : null" @scroll="onScroll" > <slot name="content" /> </div> <div v-if="hasSlot('footer')" class="b-dialog-page__footer" :class="footerConditionalClasses"> <slot name="footer" /> </div> </div> </Transition> </template> <script setup lang="ts"> import { computed, inject, nextTick, onMounted, onUnmounted, type PropType, ref, useSlots, watch } from 'vue'; import { BentoButton } from '@/components/button'; import ArrowLeftIcon from '@adyen/ui-assets-icons-16/vue/arrow-left'; import { useHasSlot } from '@/composables/use-has-slot/use-has-slot'; import { useI18n } from '@/utils/ts/i18n'; import { isDevelopmentEnviroment } from '@/utils/ts/dev-environment'; import { BentoDialogPageAnimation, type BentoDialogPageConfigData, type BentoDialogPageRegisterPage, } from './dialog-page.types'; import type { CSSProperties } from 'vue/types/jsx.d.ts'; import { DIALOG_PAGE_CONFIG_INJECTION_KEY, DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY } from './dialog-page.keys'; import messages from './messages.json'; import { useScroll } from '@vueuse/core'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * Identifier of the page, used with activePage prop to handle navigation */ pageId: { type: String, default: undefined, }, /** * Show header and actions border */ alwaysShowBorder: { type: Boolean, default: false, }, /** * Pass custom styling to the back button to position it correctly with the header slot */ backButtonStyling: { type: Object as PropType<CSSProperties>, default: null }, /** * ID of the page for the back button to navigate to. Only use it to overwrite the default back behavior * Used when the back button should not take the user to the natural previous page * Set to `null` to hide back button in the page */ previousPage: { type: String, default: undefined, }, }); const emit = defineEmits<{ /** * Fires when the animation ends */ (e: 'animationend'): void; }>(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const { t } = useI18n<{ message: MessageSchema }>({ messages }); // Refs const contentElement = ref<HTMLDivElement>(null); const hasScroll = ref(false); const isMounted = ref(false); const { arrivedState } = useScroll(contentElement); // Logic to choose which page is active/shown const showPage = computed(() => (activePage.value ? activePage.value === props.pageId : true)); // Scrolling styling logic const headerConditionalClasses = computed(() => ({ 'b-dialog-page__header--with-border': props.alwaysShowBorder || (hasScroll.value && !arrivedState.top), })); const footerConditionalClasses = computed(() => ({ 'b-dialog-page__footer--with-border': props.alwaysShowBorder || (hasScroll.value && !arrivedState.bottom), })); const isScrollable = computed(() => !(arrivedState.bottom && arrivedState.top)); const onScroll = () => { if (!contentElement.value) { return; } }; const calculateScroll = async watchedValue => { if (!watchedValue) { return; } // Wait for content to be rendered to be able to calculate the height await nextTick(); if (!isMounted.value) { return; } hasScroll.value = contentElement.value && contentElement.value.scrollHeight > contentElement.value.clientHeight; }; const { activePage, animation, isAnimationActive, isOpen, pages, previousPages, updatePage } = inject<BentoDialogPageConfigData>(DIALOG_PAGE_CONFIG_INJECTION_KEY); watch(() => isOpen.value, calculateScroll, { immediate: true }); watch(() => showPage.value, calculateScroll, { immediate: true }); // Logic to determine whether the back button should be shown on the page const showBackButton = computed(() => { if (props.previousPage) { return true; } if (props.previousPage === null) { return false; } return hasPreviousPages.value && showPage.value; }); // Animation const updateAnimation = () => { animation.value = BentoDialogPageAnimation.TRANSITION_LEVEL_DEEPER; if (isAnimationActive) { isAnimationActive.value = false; } emit('animationend'); }; const startAnimation = () => { if (isAnimationActive) { isAnimationActive.value = true; } }; // Previous page logic const hasPreviousPages = computed(() => previousPages.value.length > 1); const lastPage = computed(() => previousPages.value[previousPages.value.length - 1]); const isActivePageValid = computed(() => pages.value.some(page => page === activePage.value)); const goToPreviousPage = async (page: string) => { if (page) { const index = previousPages.value.indexOf(page); if (index !== -1) { previousPages.value = previousPages.value.slice(0, index + 1); } else { previousPages.value = [page]; } } else { previousPages.value.pop(); } animation.value = BentoDialogPageAnimation.TRANSITION_LEVEL_UPPER; await nextTick(); if (!isMounted.value) { return; } updatePage(lastPage.value); }; // Add new page to the previousPage array to keep track of user navigation watch( () => activePage.value, async () => { await nextTick(); if (!isMounted.value || !isOpen.value) { return; } if (!!activePage.value && !isActivePageValid.value) { if (isDevelopmentEnviroment) { // eslint-disable-next-line no-console console.error( `The activePage passed "${activePage.value}" does not match the id of any of the pages` ); return; } } if (activePage.value !== lastPage.value) { previousPages.value.push(activePage.value); } }, { immediate: true } ); // Register page to parent component and inject data from parent component const registerPage = inject<BentoDialogPageRegisterPage>(DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY); const { unregisterPage } = registerPage(props.pageId); onMounted(() => { isMounted.value = true; }); // Unregister page on unmounting the page onUnmounted(() => { isMounted.value = false; unregisterPage(); }); </script> <script lang="ts"> /** * DialogPage is an internal component used to render different pages of dialog components (Modal, ModalFullscreen and Sidepanel) * * @example * import { DialogPage } from '@/internal'; * * export default { * components: { DialogPage }, * template: ` * <dialog-page pageId='step1' always-show-border> * <template #header>Header</template> * <template #content>Content</template> * <template #footer>Footer</template> * </dialog-page> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./dialog-page.scss" />
1
+ <template> <Transition :name="animation" mode="out-in" @after-leave="updateAnimation" @leave="startAnimation"> <div v-if="isOpen && showPage" class="b-dialog-page" :class="computedClasses"> <div v-if="hasSlot('header')" class="b-dialog-page__header" :class="headerConditionalClasses"> <div v-if="showBackButton" class="b-dialog-page__back-button" :style="backButtonStyling" data-testid="back-button" > <bento-button variant="tertiary" @click="goToPreviousPage(previousPage)"> <arrow-left-icon :svg-title="t('goToPreviousPage')" /> </bento-button> </div> <slot name="header"></slot> </div> <div v-if="hasSlot('content')" ref="contentElement" class="b-dialog-page__content" :tabindex="isScrollable ? 0 : null" @scroll="onScroll" > <slot name="content" /> </div> <div v-if="hasSlot('footer')" class="b-dialog-page__footer" :class="footerConditionalClasses"> <slot name="footer" /> </div> </div> </Transition> </template> <script setup lang="ts"> import { computed, inject, nextTick, onMounted, onUnmounted, type PropType, ref, useSlots, watch } from 'vue'; import { BentoButton } from '@/components/button'; import ArrowLeftIcon from '@adyen/ui-assets-icons-16/vue/arrow-left'; import { useHasSlot } from '@/composables/use-has-slot/use-has-slot'; import { useI18n } from '@/utils/ts/i18n'; import { isDevelopmentEnviroment } from '@/utils/ts/dev-environment'; import { BentoDialogPageAnimation, type BentoDialogPageConfigData, type BentoDialogPageRegisterPage, } from './dialog-page.types'; import type { CSSProperties } from 'vue/types/jsx.d.ts'; import { DIALOG_PAGE_CONFIG_INJECTION_KEY, DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY } from './dialog-page.keys'; import messages from './messages.json'; import { useScroll } from '@vueuse/core'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * Sets `position: absolute` on the dialog page root element. * Enabled by default as it is used by default in most places. * Inspector requires it to not be absolute positioned. * @default true */ absolutePosition: { type: Boolean, default: true, }, /** * Show header and actions border */ alwaysShowBorder: { type: Boolean, default: false, }, /** * Pass custom styling to the back button to position it correctly with the header slot */ backButtonStyling: { type: Object as PropType<CSSProperties>, default: null }, /** * Identifier of the page, used with activePage prop to handle navigation */ pageId: { type: String, default: undefined, }, /** * ID of the page for the back button to navigate to. Only use it to overwrite the default back behavior * Used when the back button should not take the user to the natural previous page * Set to `null` to hide back button in the page */ previousPage: { type: String, default: undefined, }, }); const emit = defineEmits<{ /** * Fires when the animation ends */ (e: 'animationend'): void; }>(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const { t } = useI18n<{ message: MessageSchema }>({ messages }); // Refs const contentElement = ref<HTMLDivElement>(null); const hasScroll = ref(false); const isMounted = ref(false); const { arrivedState } = useScroll(contentElement); // Logic to choose which page is active/shown const showPage = computed(() => (activePage.value ? activePage.value === props.pageId : true)); const computedClasses = computed(() => ({ 'b-dialog-page--absolute-position': props.absolutePosition, })); // Scrolling styling logic const headerConditionalClasses = computed(() => ({ 'b-dialog-page__header--with-border': props.alwaysShowBorder || (hasScroll.value && !arrivedState.top), })); const footerConditionalClasses = computed(() => ({ 'b-dialog-page__footer--with-border': props.alwaysShowBorder || (hasScroll.value && !arrivedState.bottom), })); const isScrollable = computed(() => !(arrivedState.bottom && arrivedState.top)); const onScroll = () => { if (!contentElement.value) { return; } }; const calculateScroll = async watchedValue => { if (!watchedValue) { return; } // Wait for content to be rendered to be able to calculate the height await nextTick(); if (!isMounted.value) { return; } hasScroll.value = contentElement.value && contentElement.value.scrollHeight > contentElement.value.clientHeight; }; const { activePage, animation, isAnimationActive, isOpen, pages, previousPages, updatePage } = inject<BentoDialogPageConfigData>(DIALOG_PAGE_CONFIG_INJECTION_KEY); watch(() => isOpen.value, calculateScroll, { immediate: true }); watch(() => showPage.value, calculateScroll, { immediate: true }); // Logic to determine whether the back button should be shown on the page const showBackButton = computed(() => { if (props.previousPage) { return true; } if (props.previousPage === null) { return false; } return hasPreviousPages.value && showPage.value; }); // Animation const updateAnimation = () => { animation.value = BentoDialogPageAnimation.TRANSITION_LEVEL_DEEPER; if (isAnimationActive) { isAnimationActive.value = false; } emit('animationend'); }; const startAnimation = () => { if (isAnimationActive) { isAnimationActive.value = true; } }; // Previous page logic const hasPreviousPages = computed(() => previousPages.value.length > 1); const lastPage = computed(() => previousPages.value[previousPages.value.length - 1]); const isActivePageValid = computed(() => pages.value.some(page => page === activePage.value)); const goToPreviousPage = async (page: string) => { if (page) { const index = previousPages.value.indexOf(page); if (index !== -1) { previousPages.value = previousPages.value.slice(0, index + 1); } else { previousPages.value = [page]; } } else { previousPages.value.pop(); } animation.value = BentoDialogPageAnimation.TRANSITION_LEVEL_UPPER; await nextTick(); if (!isMounted.value) { return; } updatePage(lastPage.value); }; // Add new page to the previousPage array to keep track of user navigation watch( () => activePage.value, async () => { await nextTick(); if (!isMounted.value || !isOpen.value) { return; } if (!!activePage.value && !isActivePageValid.value) { if (isDevelopmentEnviroment) { // eslint-disable-next-line no-console console.error( `The activePage passed "${activePage.value}" does not match the id of any of the pages` ); return; } } if (activePage.value !== lastPage.value) { previousPages.value.push(activePage.value); } }, { immediate: true } ); // Register page to parent component and inject data from parent component const registerPage = inject<BentoDialogPageRegisterPage>(DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY); const { unregisterPage } = registerPage(props.pageId); onMounted(() => { isMounted.value = true; }); // Unregister page on unmounting the page onUnmounted(() => { isMounted.value = false; unregisterPage(); }); </script> <script lang="ts"> /** * DialogPage is an internal component used to render different pages of dialog components (Modal, ModalFullscreen and Sidepanel) * * @example * import { DialogPage } from '@/internal'; * * export default { * components: { DialogPage }, * template: ` * <dialog-page pageId='step1' always-show-border> * <template #header>Header</template> * <template #content>Content</template> * <template #footer>Footer</template> * </dialog-page> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./dialog-page.scss" />
@@ -1 +1 @@
1
- export interface BentoTagProps { /** * The label text content of the Tag. */ label?: string; /** * The number of characters to limit the number of displayed characters for the tag text, truncating it with an ellipsis of three dots "...". */ truncate?: number; /** * The variant variant of tag */ variant?: BentoTagVariant | `${BentoTagVariant}`; } /** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoTagVariant { GREY = 'grey', BLUE = 'blue', GREEN = 'green', ORANGE = 'orange', RED = 'red', WHITE = 'white', }
1
+ export type BentoTagVariant = 'grey' | 'blue' | 'green' | 'orange' | 'red' | 'white'; /** * @deprecated Since v2.0.0. Use string literal instead. */ export const BentoTagVariant: Record<string, BentoTagVariant> = { GREY: 'grey', BLUE: 'blue', GREEN: 'green', ORANGE: 'orange', RED: 'red', WHITE: 'white', }; export interface BentoTagProps { /** * The label text content of the Tag. */ label?: string; /** * The number of characters to limit the number of displayed characters for the tag text, truncating it with an ellipsis of three dots "...". */ truncate?: number; /** * The variant variant of tag */ variant?: BentoTagVariant | `${BentoTagVariant}`; }
@@ -55,6 +55,7 @@
55
55
  "input-field-phone-number",
56
56
  "input-time",
57
57
  "inspector",
58
+ "inspector-page",
58
59
  "layout",
59
60
  "layout-row",
60
61
  "link",
@@ -7,8 +7,8 @@
7
7
  "anchor-scroller": [""],
8
8
  "avatar": ["packages/vue2/src/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.visual.spec.tsx", "packages/vue2/src/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.stories.ts", "packages/vue2/src/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.test.ts", "packages/vue2/src/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.types.ts", "packages/vue2/src/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.vue", "packages/vue2/src/components/filter-bar/components/select-filter/variants/select-avatar-filter/select-avatar-filter.vue", "packages/vue2/src/components/filter-bar/filter-bar.types.ts"],
9
9
  "base-filter": ["packages/vue2/src/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.test.ts", "packages/vue2/src/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.vue", "packages/vue2/src/components/filter-bar/components/range-filter/range-filter.vue", "packages/vue2/src/components/filter-bar/components/range-filter/range-filter.test.ts", "packages/vue2/src/components/filter-bar/components/checkbox-group-filter/checkbox-group-filter.vue", "packages/vue2/src/components/filter-bar/components/date-filter/date-filter.vue", "packages/vue2/src/components/filter-bar/components/index.ts", "packages/vue2/src/components/filter-bar/components/select-filter/components/select-filter-button/select-filter-button.vue", "packages/vue2/src/components/filter-bar/components/date-range-filter/date-range-filter.vue", "packages/vue2/src/components/filter-bar/components/boolean-filter/boolean-filter.test.ts", "packages/vue2/src/components/filter-bar/components/boolean-filter/boolean-filter.vue", "packages/vue2/src/components/filter-bar/components/input-filter/input-filter.deprecated.test.ts", "packages/vue2/src/components/filter-bar/components/input-filter/input-filter.test.ts", "packages/vue2/src/components/filter-bar/components/input-filter/input-filter.vue", "packages/vue2/src/components/filter-bar/components/radio-group-filter/radio-group-filter.vue", "packages/vue2/src/components/filter-bar/filter-bar.docs.mdx", "packages/vue2/src/components/filter-bar/index.ts", "packages/vue2/src/components/filter-bar/__tests__/filter-bar-custom-filter-sub-component-multiple-value-example.vue", "packages/vue2/src/components/filter-bar/__tests__/filter-bar-custom-filter-sub-component-multiple-value.vue", "packages/vue2/src/components/filter-bar/__tests__/filter-bar-custom-filter-sub-component-single-value.vue", "packages/vue2/src/components/filter-bar/__tests__/filter-bar-custom-filter-sub-component-single-value-example.vue", "packages/vue2/src/components/filter-bar/__tests__/filter-bar-custom-filter-sub-component-with-popover-example.vue"],
10
- "button": ["packages/vue2/src/components/anchor-scroller/components/anchor-scroller-list/anchor-scroller-list.vue", "packages/vue2/src/components/date-range-picker/date-range-picker.vue", "packages/vue2/src/components/alert/alert.stories.ts", "packages/vue2/src/components/alert/alert.visual.spec.tsx", "packages/vue2/src/components/alert/alert.vue", "packages/vue2/src/components/file-uploader/components/file-uploader-file-card/file-uploader-file-card.vue", "packages/vue2/src/components/navigation-menu/navigation-menu.vue", "packages/vue2/src/components/navigation-menu/navigation-menu.docs.mdx", "packages/vue2/src/components/navigation-menu/navigation-menu.stories.ts", "packages/vue2/src/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.stories.ts", "packages/vue2/src/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.stories.ts", "packages/vue2/src/components/navigation-menu/__tests__/navigation-menu-everything-bagel-example.vue", "packages/vue2/src/components/popover/popover.test.ts", "packages/vue2/src/components/popover/components/popover-dismiss-button/popover-dismiss-button.vue", "packages/vue2/src/components/popover/popover.visual.spec.tsx", "packages/vue2/src/components/popover/popover.stories.ts", "packages/vue2/src/components/popover/popover.docs.mdx", "packages/vue2/src/components/popover/popover.types.ts", "packages/vue2/src/components/popover/popover.vue", "packages/vue2/src/components/popover/__tests__/popover-stub.vue", "packages/vue2/src/components/toast/components/toast-item/toast-item.vue", "packages/vue2/src/components/toast/toast.stories.ts", "packages/vue2/src/components/drawer/drawer.stories.ts", "packages/vue2/src/components/drawer/__tests__/drawer-default-example.vue", "packages/vue2/src/components/empty-state/empty-state.types.ts", "packages/vue2/src/components/empty-state/empty-state.vue", "packages/vue2/src/components/action-bar/action-bar.stories.ts", "packages/vue2/src/components/action-bar/action-bar.vue", "packages/vue2/src/components/action-bar/__tests__/action-bar-stub.vue", "packages/vue2/src/components/action-bar/__tests__/action-bar-default.vue", "packages/vue2/src/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue", "packages/vue2/src/components/header-with-views/components/header-with-views-item-edit-form/header-with-views-edit-form.vue", "packages/vue2/src/components/header-with-views/components/header-with-views-all-views/header-with-views-all-views.vue", "packages/vue2/src/components/header-with-views/header-with-views.docs.mdx", "packages/vue2/src/components/header-with-views/header-with-views.types.ts", "packages/vue2/src/components/header-with-views/header-with-views.vue", "packages/vue2/src/components/accordion/__tests__/accordion-stub.vue", "packages/vue2/src/components/accordion/accordion.stories.ts", "packages/vue2/src/components/pagination/pagination.docs.mdx", "packages/vue2/src/components/pagination/components/pagination-controls/pagination-controls.vue", "packages/vue2/src/components/link/link.types.ts", "packages/vue2/src/components/link/__tests__/hot-swap-link-stub.vue", "packages/vue2/src/components/focus-trap/focus-trap.vue", "packages/vue2/src/components/focus-trap/__tests__/focus-trap-mock.vue", "packages/vue2/src/components/focus-trap/focus-trap.stories.ts", "packages/vue2/src/components/list/components/list-item/list-item.stories.ts", "packages/vue2/src/components/list/components/list-item/list-item.vue", "packages/vue2/src/components/list/components/list-item/list-item.visual.spec.tsx", "packages/vue2/src/components/list/list.stories.ts", "packages/vue2/src/components/list/__tests__/list-default.vue", "packages/vue2/src/components/list/__tests__/list-everything-bagel.vue", "packages/vue2/src/components/form-layout/form-layout.docs.mdx", "packages/vue2/src/components/form-layout/form-layout.visual.spec.tsx", "packages/vue2/src/components/form-layout/form-layout.stories.ts", "packages/vue2/src/components/form-layout/__tests__/form-layout-default-example.vue", "packages/vue2/src/components/inspector/inspector.vue", "packages/vue2/src/components/inspector/inspector.stories.ts", "packages/vue2/src/components/inspector/__tests__/inspector-mock.vue", "packages/vue2/src/components/inspector/__tests__/inspector-with-ephemeral-trigger-mock.vue", "packages/vue2/src/components/modal-fullscreen/modal-fullscreen.stories.ts", "packages/vue2/src/components/modal-fullscreen/components/modal-fullscreen-page.vue", "packages/vue2/src/components/modal-fullscreen/modal-fullscreen.docs.mdx", "packages/vue2/src/components/modal-fullscreen/modal-fullscreen.vue", "packages/vue2/src/components/modal-fullscreen/__tests__/toast-modal-fullscreen-stub.vue", "packages/vue2/src/components/modal/modal.test.ts", "packages/vue2/src/components/modal/components/modal-page/modal-page.types.ts", "packages/vue2/src/components/modal/components/modal-dialog/modal-dialog.test.ts", "packages/vue2/src/components/modal/components/modal-dialog/modal-dialog.vue", "packages/vue2/src/components/modal/components/modal-default/modal-default.test.ts", "packages/vue2/src/components/modal/components/modal-default/modal-default.vue", "packages/vue2/src/components/modal/components/base-modal/base-modal.test.ts", "packages/vue2/src/components/modal/components/base-modal/base-modal.vue", "packages/vue2/src/components/modal/modal.stories.ts", "packages/vue2/src/components/modal/modal.vue", "packages/vue2/src/components/modal/__tests__/modal-with-date-picker-stub.vue", "packages/vue2/src/components/modal/__tests__/modal-with-data-grid-stub.vue", "packages/vue2/src/components/modal/__tests__/modal-with-multiple-pages.vue", "packages/vue2/src/components/promo-banner/promo-banner.types.ts", "packages/vue2/src/components/promo-banner/promo-banner.docs.mdx", "packages/vue2/src/components/promo-banner/promo-banner.vue", "packages/vue2/src/components/header/header.vue", "packages/vue2/src/components/header/header.test.ts", "packages/vue2/src/components/header/header.docs.mdx", "packages/vue2/src/components/header/header.stories.ts", "packages/vue2/src/components/header/header.visual.spec.tsx", "packages/vue2/src/components/header/__tests__/header-change-button-actions-label-stub.vue", "packages/vue2/src/components/input-field-password/input-field-password.docs.mdx", "packages/vue2/src/components/input-field-password/input-field-password.vue", "packages/vue2/src/components/structured-list/components/structered-list-item/structured-list-item.vue", "packages/vue2/src/components/stepper/stepper.stories.ts", "packages/vue2/src/components/stepper/__tests__/reset-stepper-example.vue", "packages/vue2/src/components/card/card.vue", "packages/vue2/src/components/card/card.test.ts", "packages/vue2/src/components/card/card.stories.ts", "packages/vue2/src/components/card/__tests__/card-stub.vue", "packages/vue2/src/components/ai-tag/ai-tag.vue", "packages/vue2/src/components/ai-tag/ai-tag.types.ts", "packages/vue2/src/components/ai-tag/__tests__/ai-tag-default.vue", "packages/vue2/src/components/radio-group/components/radio-button/radio-button.stories.ts", "packages/vue2/src/components/internal/fixed-scroller/fixed-scroller.stories.ts", "packages/vue2/src/components/internal/fixed-scroller/fixed-scroller.vue", "packages/vue2/src/components/internal/fixed-scroller/__tests__/fixed-scroller-stub.vue", "packages/vue2/src/components/internal/calendar/components/calendar-month/calendar-month.vue", "packages/vue2/src/components/internal/calendar/components/calendar-year/calendar-year.vue", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.visual.spec.tsx", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.types.ts", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.vue", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.test.ts", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.stories.ts", "packages/vue2/src/components/internal/footer-actions/footer-actions.test.ts", "packages/vue2/src/components/internal/footer-actions/footer-actions.visual.spec.tsx", "packages/vue2/src/components/internal/footer-actions/footer-actions.vue", "packages/vue2/src/components/internal/base-button/base-button.vue", "packages/vue2/src/components/internal/dialog-page/dialog-page.vue", "packages/vue2/src/components/internal/copy/copy.vue", "packages/vue2/src/components/internal/copy/copy.types.ts", "packages/vue2/src/components/draggable/draggable.stories.ts", "packages/vue2/src/components/draggable/__tests__/bento-draggable-grid-example.vue", "packages/vue2/src/components/sidepanel/components/sidepanel-page.vue", "packages/vue2/src/components/sidepanel/components/sidepanel-page.stories.ts", "packages/vue2/src/components/sidepanel/sidepanel.stories.ts", "packages/vue2/src/components/sidepanel/sidepanel.vue", "packages/vue2/src/components/sidepanel/__tests__/sidepanel-mock.vue", "packages/vue2/src/components/timeline/timeline.vue", "packages/vue2/src/components/timeline/timeline.stories.ts", "packages/vue2/src/components/timeline/components/timeline-item/timeline-item.vue", "packages/vue2/src/components/timeline/timeline.test.ts", "packages/vue2/src/components/timeline/__tests__/timeline-dynamic-add-stub.vue", "packages/vue2/src/components/tutorial/components/tutorial-popover/components/tutorial-indicator/__tests__/tutorial-indicator-stub.vue", "packages/vue2/src/components/tutorial/components/tutorial-popover/tutorial-popover.vue", "packages/vue2/src/components/tutorial/components/tutorial-popover/__tests__/tutorial-popover-stub.vue", "packages/vue2/src/components/tutorial/tutorial.types.ts", "packages/vue2/src/components/tutorial/tutorial.stories.ts", "packages/vue2/src/components/tutorial/tutorial.visual.spec.tsx", "packages/vue2/src/components/tutorial/tutorial.docs.mdx", "packages/vue2/src/components/tutorial/__tests__/tutorial-default-example.vue", "packages/vue2/src/components/tutorial/__tests__/tutorial-stub.vue", "packages/vue2/src/components/tutorial/__tests__/tutorial-with-inspector-stub.vue", "packages/vue2/src/components/code-snippet/code-snippet.vue", "packages/vue2/src/components/textarea/textarea.vue", "packages/vue2/src/components/filter-bar/components/all-filters-modal/all-filters-modal.vue", "packages/vue2/src/components/filter-bar/components/date-range-filter/date-range-filter.vue", "packages/vue2/src/components/data-grid/components/data-grid-static-icon/data-grid-static-icon.stories.ts", "packages/vue2/src/components/data-grid/components/data-grid-config-settings/data-grid-config-settings.vue", "packages/vue2/src/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.vue", "packages/vue2/src/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.test.ts", "packages/vue2/src/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.types.ts", "packages/vue2/src/components/data-grid/data-grid.docs.mdx", "packages/vue2/src/components/data-grid/data-grid.vue", "packages/vue2/src/components/data-grid/__tests__/data-grid-fit-content-in-modal-example.vue", "packages/vue2/src/components/data-grid/data-grid.stories.ts", "packages/vue2/src/components/menu/menu.test.ts", "packages/vue2/src/components/menu/menu.types.ts", "packages/vue2/src/components/menu/menu.docs.mdx"],
11
- "button-actions": ["packages/vue2/src/components/date-range-picker/date-range-picker.vue", "packages/vue2/src/components/popover/popover.test.ts", "packages/vue2/src/components/popover/popover.visual.spec.tsx", "packages/vue2/src/components/popover/popover.stories.ts", "packages/vue2/src/components/popover/popover.types.ts", "packages/vue2/src/components/popover/popover.vue", "packages/vue2/src/components/action-bar/action-bar.vue", "packages/vue2/src/components/header-with-views/components/header-with-views-item-edit-form/header-with-views-edit-form.vue", "packages/vue2/src/components/header-with-views/header-with-views.docs.mdx", "packages/vue2/src/components/header-with-views/header-with-views.types.ts", "packages/vue2/src/components/modal-fullscreen/modal-fullscreen.stories.ts", "packages/vue2/src/components/modal-fullscreen/components/modal-fullscreen-page.vue", "packages/vue2/src/components/modal-fullscreen/modal-fullscreen.vue", "packages/vue2/src/components/modal/modal.test.ts", "packages/vue2/src/components/modal/components/modal-page/modal-page.types.ts", "packages/vue2/src/components/modal/components/modal-dialog/modal-dialog.test.ts", "packages/vue2/src/components/modal/components/modal-dialog/modal-dialog.vue", "packages/vue2/src/components/modal/components/modal-default/modal-default.test.ts", "packages/vue2/src/components/modal/components/modal-default/modal-default.vue", "packages/vue2/src/components/modal/components/base-modal/base-modal.test.ts", "packages/vue2/src/components/modal/components/base-modal/base-modal.vue", "packages/vue2/src/components/modal/modal.stories.ts", "packages/vue2/src/components/modal/modal.vue", "packages/vue2/src/components/header/header.vue", "packages/vue2/src/components/header/header.test.ts", "packages/vue2/src/components/header/header.docs.mdx", "packages/vue2/src/components/header/header.stories.ts", "packages/vue2/src/components/header/header.visual.spec.tsx", "packages/vue2/src/components/header/__tests__/header-change-button-actions-label-stub.vue", "packages/vue2/src/components/card/card.vue", "packages/vue2/src/components/card/card.test.ts", "packages/vue2/src/components/card/card.stories.ts", "packages/vue2/src/components/ai-tag/ai-tag.vue", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.visual.spec.tsx", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.types.ts", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.vue", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.test.ts", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.stories.ts", "packages/vue2/src/components/internal/footer-actions/footer-actions.test.ts", "packages/vue2/src/components/internal/footer-actions/footer-actions.visual.spec.tsx", "packages/vue2/src/components/internal/footer-actions/footer-actions.vue", "packages/vue2/src/components/sidepanel/components/sidepanel-page.vue", "packages/vue2/src/components/timeline/timeline.vue", "packages/vue2/src/components/timeline/timeline.test.ts", "packages/vue2/src/components/tutorial/components/tutorial-popover/tutorial-popover.vue", "packages/vue2/src/components/filter-bar/components/date-range-filter/date-range-filter.vue", "packages/vue2/src/components/data-grid/data-grid.vue", "packages/vue2/src/components/data-grid/__tests__/data-grid-fit-content-in-modal-example.vue"],
10
+ "button": ["packages/vue2/src/components/anchor-scroller/components/anchor-scroller-list/anchor-scroller-list.vue", "packages/vue2/src/components/date-range-picker/date-range-picker.vue", "packages/vue2/src/components/alert/alert.stories.ts", "packages/vue2/src/components/alert/alert.visual.spec.tsx", "packages/vue2/src/components/alert/alert.vue", "packages/vue2/src/components/file-uploader/components/file-uploader-file-card/file-uploader-file-card.vue", "packages/vue2/src/components/navigation-menu/navigation-menu.vue", "packages/vue2/src/components/navigation-menu/navigation-menu.docs.mdx", "packages/vue2/src/components/navigation-menu/navigation-menu.stories.ts", "packages/vue2/src/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.stories.ts", "packages/vue2/src/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.stories.ts", "packages/vue2/src/components/navigation-menu/__tests__/navigation-menu-everything-bagel-example.vue", "packages/vue2/src/components/popover/popover.test.ts", "packages/vue2/src/components/popover/components/popover-dismiss-button/popover-dismiss-button.vue", "packages/vue2/src/components/popover/popover.visual.spec.tsx", "packages/vue2/src/components/popover/popover.stories.ts", "packages/vue2/src/components/popover/popover.docs.mdx", "packages/vue2/src/components/popover/popover.types.ts", "packages/vue2/src/components/popover/popover.vue", "packages/vue2/src/components/popover/__tests__/popover-stub.vue", "packages/vue2/src/components/toast/components/toast-item/toast-item.vue", "packages/vue2/src/components/toast/toast.stories.ts", "packages/vue2/src/components/drawer/drawer.stories.ts", "packages/vue2/src/components/drawer/__tests__/drawer-default-example.vue", "packages/vue2/src/components/empty-state/empty-state.types.ts", "packages/vue2/src/components/empty-state/empty-state.vue", "packages/vue2/src/components/action-bar/action-bar.stories.ts", "packages/vue2/src/components/action-bar/action-bar.vue", "packages/vue2/src/components/action-bar/__tests__/action-bar-stub.vue", "packages/vue2/src/components/action-bar/__tests__/action-bar-default.vue", "packages/vue2/src/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue", "packages/vue2/src/components/header-with-views/components/header-with-views-item-edit-form/header-with-views-edit-form.vue", "packages/vue2/src/components/header-with-views/components/header-with-views-all-views/header-with-views-all-views.vue", "packages/vue2/src/components/header-with-views/header-with-views.docs.mdx", "packages/vue2/src/components/header-with-views/header-with-views.types.ts", "packages/vue2/src/components/header-with-views/header-with-views.vue", "packages/vue2/src/components/accordion/__tests__/accordion-stub.vue", "packages/vue2/src/components/accordion/accordion.stories.ts", "packages/vue2/src/components/pagination/pagination.docs.mdx", "packages/vue2/src/components/pagination/components/pagination-controls/pagination-controls.vue", "packages/vue2/src/components/link/link.types.ts", "packages/vue2/src/components/link/__tests__/hot-swap-link-stub.vue", "packages/vue2/src/components/focus-trap/focus-trap.vue", "packages/vue2/src/components/focus-trap/__tests__/focus-trap-mock.vue", "packages/vue2/src/components/focus-trap/focus-trap.stories.ts", "packages/vue2/src/components/list/components/list-item/list-item.stories.ts", "packages/vue2/src/components/list/components/list-item/list-item.vue", "packages/vue2/src/components/list/components/list-item/list-item.visual.spec.tsx", "packages/vue2/src/components/list/list.stories.ts", "packages/vue2/src/components/list/__tests__/list-default.vue", "packages/vue2/src/components/list/__tests__/list-everything-bagel.vue", "packages/vue2/src/components/form-layout/form-layout.docs.mdx", "packages/vue2/src/components/form-layout/form-layout.visual.spec.tsx", "packages/vue2/src/components/form-layout/form-layout.stories.ts", "packages/vue2/src/components/form-layout/__tests__/form-layout-default-example.vue", "packages/vue2/src/components/inspector/inspector.vue", "packages/vue2/src/components/inspector/inspector.stories.ts", "packages/vue2/src/components/inspector/components/inspector-page/inspector-page.types.ts", "packages/vue2/src/components/inspector/__tests__/inspector-mock.vue", "packages/vue2/src/components/inspector/__tests__/inspector-with-ephemeral-trigger-mock.vue", "packages/vue2/src/components/inspector/__tests__/inspector-with-multiple-pages-example.vue", "packages/vue2/src/components/modal-fullscreen/modal-fullscreen.stories.ts", "packages/vue2/src/components/modal-fullscreen/components/modal-fullscreen-page.vue", "packages/vue2/src/components/modal-fullscreen/modal-fullscreen.docs.mdx", "packages/vue2/src/components/modal-fullscreen/modal-fullscreen.vue", "packages/vue2/src/components/modal-fullscreen/__tests__/toast-modal-fullscreen-stub.vue", "packages/vue2/src/components/modal/modal.test.ts", "packages/vue2/src/components/modal/components/modal-page/modal-page.types.ts", "packages/vue2/src/components/modal/components/modal-dialog/modal-dialog.test.ts", "packages/vue2/src/components/modal/components/modal-dialog/modal-dialog.vue", "packages/vue2/src/components/modal/components/modal-default/modal-default.test.ts", "packages/vue2/src/components/modal/components/modal-default/modal-default.vue", "packages/vue2/src/components/modal/components/base-modal/base-modal.test.ts", "packages/vue2/src/components/modal/components/base-modal/base-modal.vue", "packages/vue2/src/components/modal/modal.stories.ts", "packages/vue2/src/components/modal/modal.vue", "packages/vue2/src/components/modal/__tests__/modal-with-date-picker-stub.vue", "packages/vue2/src/components/modal/__tests__/modal-with-data-grid-stub.vue", "packages/vue2/src/components/modal/__tests__/modal-with-multiple-pages.vue", "packages/vue2/src/components/promo-banner/promo-banner.types.ts", "packages/vue2/src/components/promo-banner/promo-banner.docs.mdx", "packages/vue2/src/components/promo-banner/promo-banner.vue", "packages/vue2/src/components/header/header.vue", "packages/vue2/src/components/header/header.test.ts", "packages/vue2/src/components/header/header.docs.mdx", "packages/vue2/src/components/header/header.stories.ts", "packages/vue2/src/components/header/header.visual.spec.tsx", "packages/vue2/src/components/header/__tests__/header-change-button-actions-label-stub.vue", "packages/vue2/src/components/input-field-password/input-field-password.docs.mdx", "packages/vue2/src/components/input-field-password/input-field-password.vue", "packages/vue2/src/components/structured-list/components/structered-list-item/structured-list-item.vue", "packages/vue2/src/components/stepper/stepper.stories.ts", "packages/vue2/src/components/stepper/__tests__/reset-stepper-example.vue", "packages/vue2/src/components/card/card.vue", "packages/vue2/src/components/card/card.test.ts", "packages/vue2/src/components/card/card.stories.ts", "packages/vue2/src/components/card/__tests__/card-stub.vue", "packages/vue2/src/components/ai-tag/ai-tag.vue", "packages/vue2/src/components/ai-tag/ai-tag.types.ts", "packages/vue2/src/components/ai-tag/__tests__/ai-tag-default.vue", "packages/vue2/src/components/radio-group/components/radio-button/radio-button.stories.ts", "packages/vue2/src/components/internal/fixed-scroller/fixed-scroller.stories.ts", "packages/vue2/src/components/internal/fixed-scroller/fixed-scroller.vue", "packages/vue2/src/components/internal/fixed-scroller/__tests__/fixed-scroller-stub.vue", "packages/vue2/src/components/internal/calendar/components/calendar-month/calendar-month.vue", "packages/vue2/src/components/internal/calendar/components/calendar-year/calendar-year.vue", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.visual.spec.tsx", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.types.ts", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.vue", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.test.ts", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.stories.ts", "packages/vue2/src/components/internal/footer-actions/footer-actions.test.ts", "packages/vue2/src/components/internal/footer-actions/footer-actions.visual.spec.tsx", "packages/vue2/src/components/internal/footer-actions/footer-actions.vue", "packages/vue2/src/components/internal/base-button/base-button.vue", "packages/vue2/src/components/internal/dialog-page/dialog-page.vue", "packages/vue2/src/components/internal/copy/copy.vue", "packages/vue2/src/components/internal/copy/copy.types.ts", "packages/vue2/src/components/draggable/draggable.stories.ts", "packages/vue2/src/components/draggable/__tests__/bento-draggable-grid-example.vue", "packages/vue2/src/components/sidepanel/components/sidepanel-page.vue", "packages/vue2/src/components/sidepanel/components/sidepanel-page.stories.ts", "packages/vue2/src/components/sidepanel/sidepanel.stories.ts", "packages/vue2/src/components/sidepanel/sidepanel.vue", "packages/vue2/src/components/sidepanel/__tests__/sidepanel-mock.vue", "packages/vue2/src/components/timeline/timeline.vue", "packages/vue2/src/components/timeline/timeline.stories.ts", "packages/vue2/src/components/timeline/components/timeline-item/timeline-item.vue", "packages/vue2/src/components/timeline/timeline.test.ts", "packages/vue2/src/components/timeline/__tests__/timeline-dynamic-add-stub.vue", "packages/vue2/src/components/tutorial/components/tutorial-popover/components/tutorial-indicator/__tests__/tutorial-indicator-stub.vue", "packages/vue2/src/components/tutorial/components/tutorial-popover/tutorial-popover.vue", "packages/vue2/src/components/tutorial/components/tutorial-popover/__tests__/tutorial-popover-stub.vue", "packages/vue2/src/components/tutorial/tutorial.types.ts", "packages/vue2/src/components/tutorial/tutorial.stories.ts", "packages/vue2/src/components/tutorial/tutorial.visual.spec.tsx", "packages/vue2/src/components/tutorial/tutorial.docs.mdx", "packages/vue2/src/components/tutorial/__tests__/tutorial-default-example.vue", "packages/vue2/src/components/tutorial/__tests__/tutorial-stub.vue", "packages/vue2/src/components/tutorial/__tests__/tutorial-with-inspector-stub.vue", "packages/vue2/src/components/code-snippet/code-snippet.vue", "packages/vue2/src/components/textarea/textarea.vue", "packages/vue2/src/components/filter-bar/components/all-filters-modal/all-filters-modal.vue", "packages/vue2/src/components/filter-bar/components/date-range-filter/date-range-filter.vue", "packages/vue2/src/components/data-grid/components/data-grid-static-icon/data-grid-static-icon.stories.ts", "packages/vue2/src/components/data-grid/components/data-grid-config-settings/data-grid-config-settings.vue", "packages/vue2/src/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.vue", "packages/vue2/src/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.test.ts", "packages/vue2/src/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.types.ts", "packages/vue2/src/components/data-grid/data-grid.docs.mdx", "packages/vue2/src/components/data-grid/data-grid.vue", "packages/vue2/src/components/data-grid/__tests__/data-grid-fit-content-in-modal-example.vue", "packages/vue2/src/components/data-grid/data-grid.stories.ts", "packages/vue2/src/components/menu/menu.test.ts", "packages/vue2/src/components/menu/menu.types.ts", "packages/vue2/src/components/menu/menu.docs.mdx"],
11
+ "button-actions": ["packages/vue2/src/components/date-range-picker/date-range-picker.vue", "packages/vue2/src/components/popover/popover.test.ts", "packages/vue2/src/components/popover/popover.visual.spec.tsx", "packages/vue2/src/components/popover/popover.stories.ts", "packages/vue2/src/components/popover/popover.types.ts", "packages/vue2/src/components/popover/popover.vue", "packages/vue2/src/components/action-bar/action-bar.vue", "packages/vue2/src/components/header-with-views/components/header-with-views-item-edit-form/header-with-views-edit-form.vue", "packages/vue2/src/components/header-with-views/header-with-views.docs.mdx", "packages/vue2/src/components/header-with-views/header-with-views.types.ts", "packages/vue2/src/components/inspector/components/inspector-page/inspector-page.types.ts", "packages/vue2/src/components/modal-fullscreen/modal-fullscreen.stories.ts", "packages/vue2/src/components/modal-fullscreen/components/modal-fullscreen-page.vue", "packages/vue2/src/components/modal-fullscreen/modal-fullscreen.vue", "packages/vue2/src/components/modal/modal.test.ts", "packages/vue2/src/components/modal/components/modal-page/modal-page.types.ts", "packages/vue2/src/components/modal/components/modal-dialog/modal-dialog.test.ts", "packages/vue2/src/components/modal/components/modal-dialog/modal-dialog.vue", "packages/vue2/src/components/modal/components/modal-default/modal-default.test.ts", "packages/vue2/src/components/modal/components/modal-default/modal-default.vue", "packages/vue2/src/components/modal/components/base-modal/base-modal.test.ts", "packages/vue2/src/components/modal/components/base-modal/base-modal.vue", "packages/vue2/src/components/modal/modal.stories.ts", "packages/vue2/src/components/modal/modal.vue", "packages/vue2/src/components/header/header.vue", "packages/vue2/src/components/header/header.test.ts", "packages/vue2/src/components/header/header.docs.mdx", "packages/vue2/src/components/header/header.stories.ts", "packages/vue2/src/components/header/header.visual.spec.tsx", "packages/vue2/src/components/header/__tests__/header-change-button-actions-label-stub.vue", "packages/vue2/src/components/card/card.vue", "packages/vue2/src/components/card/card.test.ts", "packages/vue2/src/components/card/card.stories.ts", "packages/vue2/src/components/ai-tag/ai-tag.vue", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.visual.spec.tsx", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.types.ts", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.vue", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.test.ts", "packages/vue2/src/components/internal/button-actions-with-menu/button-actions-with-menu.stories.ts", "packages/vue2/src/components/internal/footer-actions/footer-actions.test.ts", "packages/vue2/src/components/internal/footer-actions/footer-actions.visual.spec.tsx", "packages/vue2/src/components/internal/footer-actions/footer-actions.vue", "packages/vue2/src/components/sidepanel/components/sidepanel-page.vue", "packages/vue2/src/components/timeline/timeline.vue", "packages/vue2/src/components/timeline/timeline.test.ts", "packages/vue2/src/components/tutorial/components/tutorial-popover/tutorial-popover.vue", "packages/vue2/src/components/filter-bar/components/date-range-filter/date-range-filter.vue", "packages/vue2/src/components/data-grid/data-grid.vue", "packages/vue2/src/components/data-grid/__tests__/data-grid-fit-content-in-modal-example.vue"],
12
12
  "card": ["packages/vue2/src/components/click-outside/click-outside.stories.ts", "packages/vue2/src/components/click-outside/click-outside.visual.spec.tsx", "packages/vue2/src/components/focus-trap/__tests__/focus-trap-mock.vue", "packages/vue2/src/components/focus-trap/focus-trap.stories.ts", "packages/vue2/src/components/ai-tag/ai-tag.visual.spec.tsx", "packages/vue2/src/components/data-grid/data-grid.visual.spec.tsx", "packages/vue2/src/components/data-grid/data-grid.docs.mdx", "packages/vue2/src/components/data-grid/__tests__/data-grid-fit-content-in-card-example.vue", "packages/vue2/src/components/data-grid/data-grid.stories.ts"],
13
13
  "checkbox": ["packages/vue2/src/components/toast/toast.stories.ts", "packages/vue2/src/components/rich-text-editor/components/rich-text-editor-renderer/components/rich-text-editor-list-item/rich-text-editor-list-item.vue", "packages/vue2/src/components/header-with-views/components/header-with-views-item-edit-form/header-with-views-edit-form.vue", "packages/vue2/src/components/header-with-views/header-with-views.vue", "packages/vue2/src/components/list/components/list-item/list-item.stories.ts", "packages/vue2/src/components/list/components/list-item/list-item.visual.spec.tsx", "packages/vue2/src/components/form-layout/form-layout.visual.spec.tsx", "packages/vue2/src/components/form-layout/form-layout.stories.ts", "packages/vue2/src/components/form-layout/__tests__/form-layout-default-example.vue", "packages/vue2/src/components/tag/tag.vue", "packages/vue2/src/components/modal/__tests__/modal-with-data-grid-stub.vue", "packages/vue2/src/components/selection-card/selection-card.types.ts", "packages/vue2/src/components/selection-card/components/selection-card-group/selection-card-group.vue", "packages/vue2/src/components/selection-card/selection-card.vue", "packages/vue2/src/components/radio-group/components/radio-button/radio-button.docs.mdx", "packages/vue2/src/components/internal/checkbox-input/checkbox-input.stories.ts", "packages/vue2/src/components/internal/checkbox-input/checkbox-input.vue", "packages/vue2/src/components/internal/checkbox-input/checkbox-input.visual.spec.tsx", "packages/vue2/src/components/internal/checkbox-input/index.ts", "packages/vue2/src/components/internal/checkbox-input/checkbox-input.test.ts", "packages/vue2/src/components/internal/controls-group/controls-group.vue", "packages/vue2/src/components/internal/radio-input/radio-input.vue", "packages/vue2/src/components/filter-bar/components/all-filters-modal/all-filters-modal.vue", "packages/vue2/src/components/filter-bar/components/checkbox-group-filter/checkbox-group-filter.visual.spec.tsx", "packages/vue2/src/components/filter-bar/components/checkbox-group-filter/checkbox-group-filter.vue", "packages/vue2/src/components/filter-bar/components/checkbox-group-filter/checkbox-group-filter.types.ts", "packages/vue2/src/components/filter-bar/components/checkbox-group-filter/index.ts", "packages/vue2/src/components/filter-bar/components/checkbox-group-filter/checkbox-group-filter.stories.ts", "packages/vue2/src/components/filter-bar/components/index.ts", "packages/vue2/src/components/filter-bar/filter-bar.docs.mdx", "packages/vue2/src/components/filter-bar/filter-bar.types.ts", "packages/vue2/src/components/filter-bar/filter-bar.vue", "packages/vue2/src/components/data-grid/components/data-grid-cell/data-grid-cell.vue", "packages/vue2/src/components/data-grid/components/data-grid-columns/data-grid-columns.vue"],
14
14
  "checkbox-group": ["packages/vue2/src/components/checkbox/checkbox.vue", "packages/vue2/src/components/checkbox/checkbox.stories.ts", "packages/vue2/src/components/form-layout/form-layout.visual.spec.tsx", "packages/vue2/src/components/form-layout/form-layout.stories.ts", "packages/vue2/src/components/form-layout/__tests__/form-layout-default-example.vue", "packages/vue2/src/components/tag/tag.vue", "packages/vue2/src/components/selection-card/components/selection-card-group/selection-card-group.vue", "packages/vue2/src/components/filter-bar/components/all-filters-modal/all-filters-modal.vue", "packages/vue2/src/components/filter-bar/components/checkbox-group-filter/checkbox-group-filter.visual.spec.tsx", "packages/vue2/src/components/filter-bar/components/checkbox-group-filter/checkbox-group-filter.vue", "packages/vue2/src/components/filter-bar/components/checkbox-group-filter/checkbox-group-filter.types.ts", "packages/vue2/src/components/filter-bar/components/checkbox-group-filter/index.ts", "packages/vue2/src/components/filter-bar/components/checkbox-group-filter/checkbox-group-filter.stories.ts", "packages/vue2/src/components/filter-bar/components/index.ts", "packages/vue2/src/components/filter-bar/filter-bar.docs.mdx", "packages/vue2/src/components/filter-bar/filter-bar.types.ts", "packages/vue2/src/components/filter-bar/filter-bar.vue"],
@@ -20,7 +20,7 @@
20
20
  "dashboard-section": [""],
21
21
  "dashboard-template": [""],
22
22
  "dashed-underline": ["packages/vue2/src/components/summary-grid/components/summary-grid-item-custom/summary-grid-item-custom.types.ts", "packages/vue2/src/components/summary-grid/components/summary-grid-item/summary-grid-item.vue", "packages/vue2/src/components/summary-grid/components/summary-grid-item/summary-grid-item.types.ts", "packages/vue2/src/components/summary-grid/components/summary-grid-item-text/summary-grid-item-text.types.ts", "packages/vue2/src/components/timeline/components/timeline-item/timeline-item.vue", "packages/vue2/src/components/timeline/timeline.types.ts", "packages/vue2/src/components/data-grid/components/data-grid-summary-cell/data-grid-summary-cell.vue", "packages/vue2/src/components/data-grid/components/data-grid-summary-cell/data-grid-summary-cell.types.ts", "packages/vue2/src/components/data-grid/data-grid.stories.ts"],
23
- "data-grid": ["packages/vue2/src/components/dashed-underline/__tests__/dashed-underline-inside-grid-stub.vue", "packages/vue2/src/components/link/link.docs.mdx", "packages/vue2/src/components/inspector/inspector.stories.ts", "packages/vue2/src/components/modal/modal.visual.spec.tsx", "packages/vue2/src/components/modal/__tests__/modal-with-data-grid-stub.vue", "packages/vue2/src/components/filter-bar/filter-bar.vue", "packages/vue2/src/components/menu/components/menu-list-container/menu-list-container.test.ts"],
23
+ "data-grid": ["packages/vue2/src/components/dashed-underline/__tests__/dashed-underline-inside-grid-stub.vue", "packages/vue2/src/components/link/link.docs.mdx", "packages/vue2/src/components/inspector/inspector.stories.ts", "packages/vue2/src/components/inspector/__tests__/inspector-with-multiple-pages-example.vue", "packages/vue2/src/components/modal/modal.visual.spec.tsx", "packages/vue2/src/components/modal/__tests__/modal-with-data-grid-stub.vue", "packages/vue2/src/components/filter-bar/filter-bar.vue", "packages/vue2/src/components/menu/components/menu-list-container/menu-list-container.test.ts"],
24
24
  "data-grid-static-icon": ["packages/vue2/src/components/data-grid/components/index.ts", "packages/vue2/src/components/data-grid/index.ts"],
25
25
  "data-grid-tag-with-text-cell": ["packages/vue2/src/components/data-grid/data-grid.test.ts", "packages/vue2/src/components/data-grid/data-grid.visual.spec.tsx", "packages/vue2/src/components/data-grid/components/index.ts", "packages/vue2/src/components/data-grid/index.ts", "packages/vue2/src/components/data-grid/data-grid.docs.mdx", "packages/vue2/src/components/data-grid/__tests__/data-grid-tag-with-text-cell-example.vue", "packages/vue2/src/components/data-grid/data-grid.stories.ts"],
26
26
  "data-grid-template": ["packages/vue2/src/components/data-grid/data-grid.vue"],
@@ -46,7 +46,7 @@
46
46
  "form-layout-field-tip": ["packages/vue2/src/components/form-layout/form-layout.docs.mdx", "packages/vue2/src/components/form-layout/form-layout.vue", "packages/vue2/src/components/form-layout/components/index.ts", "packages/vue2/src/components/form-layout/index.ts", "packages/vue2/src/components/form-layout/form-layout.stories.ts", "packages/vue2/src/components/form-layout/__tests__/form-layout-default-example.vue", "packages/vue2/src/components/form-layout/__tests__/form-layout-field-tip-example.vue"],
47
47
  "form-layout-group": ["packages/vue2/src/components/date-range-picker/components/date-time-input/date-time-input.vue", "packages/vue2/src/components/form-layout/form-layout.docs.mdx", "packages/vue2/src/components/form-layout/components/index.ts", "packages/vue2/src/components/form-layout/index.ts", "packages/vue2/src/components/form-layout/form-layout.visual.spec.tsx", "packages/vue2/src/components/form-layout/form-layout.stories.ts", "packages/vue2/src/components/form-layout/__tests__/form-layout-default-example.vue", "packages/vue2/src/components/form-layout/__tests__/form-layout-validate-example.vue", "packages/vue2/src/components/internal/listbox/composables/use-multi-level-items/use-multi-level-items.ts", "packages/vue2/src/components/date-time-picker/date-time-picker.vue"],
48
48
  "form-layout-title": ["packages/vue2/src/components/form-layout/form-layout.docs.mdx", "packages/vue2/src/components/form-layout/form-layout.vue", "packages/vue2/src/components/form-layout/components/index.ts", "packages/vue2/src/components/form-layout/index.ts", "packages/vue2/src/components/form-layout/form-layout.visual.spec.tsx", "packages/vue2/src/components/form-layout/form-layout.stories.ts", "packages/vue2/src/components/form-layout/__tests__/form-layout-default-example.vue", "packages/vue2/src/components/form-layout/__tests__/form-layout-validate-example.vue"],
49
- "header": ["packages/vue2/src/components/button/components/button-actions/button-actions.docs.mdx", "packages/vue2/src/components/header-with-views/composables/use-edit-views/use-edit-views.test.ts", "packages/vue2/src/components/header-with-views/composables/use-edit-views/use-edit-views.ts", "packages/vue2/src/components/header-with-views/composables/use-drag-and-drop-views/use-drag-and-drop-views.ts", "packages/vue2/src/components/header-with-views/header-with-views.test.ts", "packages/vue2/src/components/header-with-views/components/header-with-views-item-edit-form/header-with-views-edit-form.vue", "packages/vue2/src/components/header-with-views/components/header-with-views-item-edit-form/header-with-views-edit-form.types.ts", "packages/vue2/src/components/header-with-views/components/header-with-views-all-views/header-with-views-all-views.vue", "packages/vue2/src/components/header-with-views/components/header-with-views-all-views/header-with-views-all-views.types.ts", "packages/vue2/src/components/header-with-views/header-with-views.stories.ts", "packages/vue2/src/components/header-with-views/header-with-views.docs.mdx", "packages/vue2/src/components/header-with-views/header-with-views.types.ts", "packages/vue2/src/components/header-with-views/index.ts", "packages/vue2/src/components/header-with-views/header-with-views.visual.spec.tsx", "packages/vue2/src/components/header-with-views/__tests__/header-with-views-and-toast-stub.vue", "packages/vue2/src/components/header-with-views/__tests__/header-with-views-controlled-active-view-example.vue", "packages/vue2/src/components/header-with-views/header-with-views.vue", "packages/vue2/src/components/inspector/inspector.stories.ts", "packages/vue2/src/components/internal/header-meta/header-meta.types.ts", "packages/vue2/src/components/internal/header-meta/header-meta.vue"],
49
+ "header": ["packages/vue2/src/components/button/components/button-actions/button-actions.docs.mdx", "packages/vue2/src/components/header-with-views/composables/use-edit-views/use-edit-views.test.ts", "packages/vue2/src/components/header-with-views/composables/use-edit-views/use-edit-views.ts", "packages/vue2/src/components/header-with-views/composables/use-drag-and-drop-views/use-drag-and-drop-views.ts", "packages/vue2/src/components/header-with-views/header-with-views.test.ts", "packages/vue2/src/components/header-with-views/components/header-with-views-item-edit-form/header-with-views-edit-form.vue", "packages/vue2/src/components/header-with-views/components/header-with-views-item-edit-form/header-with-views-edit-form.types.ts", "packages/vue2/src/components/header-with-views/components/header-with-views-all-views/header-with-views-all-views.vue", "packages/vue2/src/components/header-with-views/components/header-with-views-all-views/header-with-views-all-views.types.ts", "packages/vue2/src/components/header-with-views/header-with-views.stories.ts", "packages/vue2/src/components/header-with-views/header-with-views.docs.mdx", "packages/vue2/src/components/header-with-views/header-with-views.types.ts", "packages/vue2/src/components/header-with-views/index.ts", "packages/vue2/src/components/header-with-views/header-with-views.visual.spec.tsx", "packages/vue2/src/components/header-with-views/__tests__/header-with-views-and-toast-stub.vue", "packages/vue2/src/components/header-with-views/__tests__/header-with-views-controlled-active-view-example.vue", "packages/vue2/src/components/header-with-views/header-with-views.vue", "packages/vue2/src/components/inspector/inspector.stories.ts", "packages/vue2/src/components/inspector/__tests__/inspector-with-multiple-pages-example.vue", "packages/vue2/src/components/internal/header-meta/header-meta.types.ts", "packages/vue2/src/components/internal/header-meta/header-meta.vue"],
50
50
  "header-with-views": ["packages/vue2/src/components/button/components/button-actions/button-actions.docs.mdx", "packages/vue2/src/components/internal/header-meta/header-meta.vue"],
51
51
  "image": ["packages/vue2/src/components/promo-banner/promo-banner.stories.ts", "packages/vue2/src/components/promo-banner/promo-banner.types.ts", "packages/vue2/src/components/promo-banner/promo-banner.docs.mdx", "packages/vue2/src/components/promo-banner/promo-banner.vue", "packages/vue2/src/components/input-field-password/input-field-password.vue", "packages/vue2/src/components/card/card.visual.spec.tsx", "packages/vue2/src/components/card/card.stories.ts", "packages/vue2/src/components/tutorial/components/tutorial-popover/tutorial-popover.vue", "packages/vue2/src/components/tutorial/tutorial.types.ts", "packages/vue2/src/components/tutorial/tutorial.stories.ts", "packages/vue2/src/components/tutorial/__tests__/tutorial-default-example.vue"],
52
52
  "info-icon": ["packages/vue2/src/components/structured-list/components/structered-list-item/structured-list-item.vue", "packages/vue2/src/components/summary-grid/components/summary-grid-item/summary-grid-item.vue", "packages/vue2/src/components/internal/field-label/field-label.vue", "packages/vue2/src/components/internal/info-icon-with-popover/info-icon-with-popover.visual.spec.tsx", "packages/vue2/src/components/internal/info-icon-with-popover/info-icon-with-popover.vue", "packages/vue2/src/components/internal/info-icon-with-popover/info-icon-with-popover.test.ts", "packages/vue2/src/components/data-grid/components/data-grid-columns/data-grid-columns.vue"],
@@ -55,6 +55,7 @@
55
55
  "input-field-phone-number": ["packages/vue2/src/components/input-field/input-field.vue"],
56
56
  "input-time": ["packages/vue2/src/components/date-range-picker/components/date-time-input/date-time-input.types.ts", "packages/vue2/src/components/date-range-picker/components/date-time-input/date-time-input.vue", "packages/vue2/src/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.vue", "packages/vue2/src/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.test.ts", "packages/vue2/src/components/date-time-picker/date-time-picker.docs.mdx", "packages/vue2/src/components/date-time-picker/date-time-picker.vue", "packages/vue2/src/components/date-time-picker/date-time-picker.types.ts"],
57
57
  "inspector": ["packages/vue2/src/components/tutorial/__tests__/tutorial-with-inspector-stub.vue"],
58
+ "inspector-page": ["packages/vue2/src/components/inspector/inspector.stories.ts", "packages/vue2/src/components/inspector/inspector.types.ts", "packages/vue2/src/components/inspector/inspector.docs.mdx", "packages/vue2/src/components/inspector/__tests__/inspector-with-multiple-pages-example.vue"],
58
59
  "layout": ["packages/vue2/src/components/form-layout/components/form-layout-group/form-layout-group.vue", "packages/vue2/src/components/form-layout/components/form-layout-group/form-layout-group.types.ts", "packages/vue2/src/components/internal/listbox/composables/use-multi-level-items/use-multi-level-items.ts"],
59
60
  "layout-row": ["packages/vue2/src/components/layout/layout.stories.ts", "packages/vue2/src/components/layout/layout.types.ts", "packages/vue2/src/components/layout/layout.visual.spec.tsx", "packages/vue2/src/components/layout/layout.test.ts", "packages/vue2/src/components/layout/index.ts", "packages/vue2/src/components/layout/layout.vue"],
60
61
  "link": ["packages/vue2/src/components/dashed-underline/dashed-underline.visual.spec.tsx", "packages/vue2/src/components/dashed-underline/dashed-underline.stories.ts", "packages/vue2/src/components/dropdown/dropdown.visual.spec.tsx", "packages/vue2/src/components/file-uploader/components/file-uploader-file-card/file-uploader-file-card.vue", "packages/vue2/src/components/navigation-menu/navigation-menu.docs.mdx", "packages/vue2/src/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.types.ts", "packages/vue2/src/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.vue", "packages/vue2/src/components/popover/popover.stories.ts", "packages/vue2/src/components/button/components/ai-button/ai-button.types.ts", "packages/vue2/src/components/button/button.vue", "packages/vue2/src/components/button/button.docs.mdx", "packages/vue2/src/components/rich-text-editor/components/rich-text-editor-renderer/components/rich-text-editor-link/rich-text-editor-link.vue", "packages/vue2/src/components/checkbox/checkbox.test.ts", "packages/vue2/src/components/toggle/toggle.test.ts", "packages/vue2/src/components/form-layout/form-layout.visual.spec.tsx", "packages/vue2/src/components/form-layout/form-layout.stories.ts", "packages/vue2/src/components/form-layout/__tests__/form-layout-validate-example.vue", "packages/vue2/src/components/header/header.stories.ts", "packages/vue2/src/components/header/header.visual.spec.tsx", "packages/vue2/src/components/structured-list/structured-list.visual.spec.tsx", "packages/vue2/src/components/structured-list/__tests__/structured-list-default.vue", "packages/vue2/src/components/structured-list/structured-list.stories.ts", "packages/vue2/src/components/card/card.stories.ts", "packages/vue2/src/components/radio-group/components/radio-button/radio-button.test.ts", "packages/vue2/src/components/internal/info-icon-with-popover/info-icon-with-popover.visual.spec.tsx", "packages/vue2/src/components/internal/info-icon-with-popover/info-icon-with-popover.test.ts", "packages/vue2/src/components/timeline/timeline.stories.ts", "packages/vue2/src/components/timeline/components/timeline-item/timeline-item.vue", "packages/vue2/src/components/timeline/components/timeline-item/timeline-item.visual.spec.tsx", "packages/vue2/src/components/timeline/timeline.docs.mdx", "packages/vue2/src/components/data-grid/data-grid.visual.spec.tsx", "packages/vue2/src/components/data-grid/data-grid.docs.mdx", "packages/vue2/src/components/data-grid/__tests__/data-grid-with-slots-example.vue", "packages/vue2/src/components/data-grid/__tests__/data-grid-interactive-elements-in-cells-and-headers.vue", "packages/vue2/src/components/data-grid/__tests__/data-grid-everything-bagel.vue", "packages/vue2/src/components/data-grid/data-grid.stories.ts"],
@@ -87,8 +88,8 @@
87
88
  "status": ["packages/vue2/src/components/list/components/list-item/list-item.types.ts", "packages/vue2/src/components/list/components/list-item/list-item.stories.ts", "packages/vue2/src/components/list/components/list-item/list-item.vue", "packages/vue2/src/components/structured-list/components/structered-list-item/structured-list-item.vue", "packages/vue2/src/components/structured-list/structured-list.visual.spec.tsx", "packages/vue2/src/components/structured-list/__tests__/structured-list-default.vue", "packages/vue2/src/components/structured-list/structured-list.stories.ts", "packages/vue2/src/components/summary-grid/components/summary-grid-item-custom/summary-grid-item-custom.visual.spec.tsx", "packages/vue2/src/components/summary-grid/summary-grid.docs.mdx", "packages/vue2/src/components/summary-grid/summary-grid.stories.ts", "packages/vue2/src/components/summary-grid/summary-grid.visual.spec.tsx", "packages/vue2/src/components/timeline/timeline.docs.mdx", "packages/vue2/src/components/data-grid/data-grid.visual.spec.tsx", "packages/vue2/src/components/data-grid/data-grid.docs.mdx", "packages/vue2/src/components/data-grid/__tests__/data-grid-everything-bagel.vue", "packages/vue2/src/components/data-grid/data-grid.stories.ts"],
88
89
  "step": ["packages/vue2/src/components/modal-fullscreen/modal-fullscreen.docs.mdx", "packages/vue2/src/components/stepper/stepper.types.ts", "packages/vue2/src/components/stepper/composables/use-keyboard-navigation/use-keyboard-navigation.ts", "packages/vue2/src/components/stepper/composables/use-step-register/use-step-register.ts", "packages/vue2/src/components/stepper/stepper.vue", "packages/vue2/src/components/stepper/stepper.test.ts", "packages/vue2/src/components/stepper/index.ts", "packages/vue2/src/components/stepper/stepper.stories.ts", "packages/vue2/src/components/stepper/stepper.docs.mdx", "packages/vue2/src/components/stepper/stepper.visual.spec.tsx", "packages/vue2/src/components/stepper/__tests__/reset-stepper-example.vue", "packages/vue2/src/components/stepper/__tests__/non-linear-stepper-example.vue", "packages/vue2/src/components/stepper/__tests__/conditional-stepper-stub.vue"],
89
90
  "stepper": ["packages/vue2/src/components/modal-fullscreen/modal-fullscreen.docs.mdx"],
90
- "structured-list": ["packages/vue2/src/components/inspector/inspector.stories.ts", "packages/vue2/src/components/timeline/components/timeline-item/timeline-item.vue"],
91
- "structured-list-item": ["packages/vue2/src/components/inspector/inspector.stories.ts", "packages/vue2/src/components/structured-list/components/structered-list-item/structured-list-item.test.ts", "packages/vue2/src/components/structured-list/components/structered-list-item/structured-list-item.stories.ts", "packages/vue2/src/components/structured-list/components/structered-list-item/structured-list-item.vue", "packages/vue2/src/components/structured-list/structured-list.visual.spec.tsx", "packages/vue2/src/components/structured-list/index.ts", "packages/vue2/src/components/structured-list/structured-list.test.ts", "packages/vue2/src/components/structured-list/__tests__/structured-list-default.vue", "packages/vue2/src/components/structured-list/structured-list.stories.ts", "packages/vue2/src/components/structured-list/structured-list.vue", "packages/vue2/src/components/timeline/components/timeline-item/timeline-item.vue"],
91
+ "structured-list": ["packages/vue2/src/components/inspector/inspector.stories.ts", "packages/vue2/src/components/inspector/__tests__/inspector-with-multiple-pages-example.vue", "packages/vue2/src/components/timeline/components/timeline-item/timeline-item.vue"],
92
+ "structured-list-item": ["packages/vue2/src/components/inspector/inspector.stories.ts", "packages/vue2/src/components/inspector/__tests__/inspector-with-multiple-pages-example.vue", "packages/vue2/src/components/structured-list/components/structered-list-item/structured-list-item.test.ts", "packages/vue2/src/components/structured-list/components/structered-list-item/structured-list-item.stories.ts", "packages/vue2/src/components/structured-list/components/structered-list-item/structured-list-item.vue", "packages/vue2/src/components/structured-list/structured-list.visual.spec.tsx", "packages/vue2/src/components/structured-list/index.ts", "packages/vue2/src/components/structured-list/structured-list.test.ts", "packages/vue2/src/components/structured-list/__tests__/structured-list-default.vue", "packages/vue2/src/components/structured-list/structured-list.stories.ts", "packages/vue2/src/components/structured-list/structured-list.vue", "packages/vue2/src/components/timeline/components/timeline-item/timeline-item.vue"],
92
93
  "summary-grid": ["packages/vue2/src/components/country/country.vue", "packages/vue2/src/components/payment-method/payment-method.vue", "packages/vue2/src/components/internal/grid-layout/grid-layout.vue"],
93
94
  "summary-grid-item-amount": ["packages/vue2/src/components/summary-grid/summary-grid.stories.ts", "packages/vue2/src/components/summary-grid/summary-grid.vue", "packages/vue2/src/components/summary-grid/index.ts", "packages/vue2/src/components/summary-grid/summary-grid.visual.spec.tsx"],
94
95
  "summary-grid-item-custom": ["packages/vue2/src/components/summary-grid/summary-grid.stories.ts", "packages/vue2/src/components/summary-grid/index.ts", "packages/vue2/src/components/summary-grid/summary-grid.visual.spec.tsx"],
@@ -106,5 +107,5 @@
106
107
  "toggle": ["packages/vue2/src/components/button/components/toggle-button/toggle-button.visual.spec.tsx", "packages/vue2/src/components/button/components/toggle-button/toggle-button.stories.ts", "packages/vue2/src/components/button/components/toggle-button/toggle-button.vue", "packages/vue2/src/components/button/components/toggle-button/toggle-button.docs.mdx", "packages/vue2/src/components/button/components/toggle-button/index.ts", "packages/vue2/src/components/button/components/toggle-button/toggle.types.ts", "packages/vue2/src/components/header-with-views/components/header-with-views-item-edit-form/header-with-views-edit-form.vue", "packages/vue2/src/components/header-with-views/components/header-with-views-all-views/header-with-views-all-views.vue", "packages/vue2/src/components/header-with-views/header-with-views.vue", "packages/vue2/src/components/list/components/list-item/list-item.stories.ts", "packages/vue2/src/components/list/components/list-item/list-item.vue", "packages/vue2/src/components/list/components/list-item/list-item.visual.spec.tsx", "packages/vue2/src/components/list/list.stories.ts", "packages/vue2/src/components/list/__tests__/list-default.vue", "packages/vue2/src/components/list/__tests__/list-everything-bagel.vue", "packages/vue2/src/components/form-layout/form-layout.visual.spec.tsx", "packages/vue2/src/components/ai-tag/ai-tag.vue", "packages/vue2/src/components/radio-group/components/radio-button/radio-button.docs.mdx", "packages/vue2/src/components/filter-bar/components/all-filters-modal/all-filters-modal.vue", "packages/vue2/src/components/filter-bar/components/base-filter/base-filter.docs.mdx", "packages/vue2/src/components/filter-bar/components/toggle-filter/toggle-filter.visual.spec.tsx", "packages/vue2/src/components/filter-bar/components/toggle-filter/toggle-filter.vue", "packages/vue2/src/components/filter-bar/components/toggle-filter/toggle-filter.stories.ts", "packages/vue2/src/components/filter-bar/components/toggle-filter/toggle-filter.types.ts", "packages/vue2/src/components/filter-bar/components/toggle-filter/index.ts", "packages/vue2/src/components/filter-bar/components/toggle-filter/toggle-filter.test.ts", "packages/vue2/src/components/filter-bar/components/index.ts", "packages/vue2/src/components/filter-bar/filter-bar.stories.ts", "packages/vue2/src/components/filter-bar/filter-bar.docs.mdx", "packages/vue2/src/components/filter-bar/filter-bar.types.ts", "packages/vue2/src/components/filter-bar/filter-bar.vue", "packages/vue2/src/components/filter-bar/__tests__/filter-bar-custom-filter-sub-component-multiple-value-example.vue", "packages/vue2/src/components/filter-bar/__tests__/filter-bar-custom-filter-sub-component-multiple-value.vue", "packages/vue2/src/components/filter-bar/__tests__/filter-bar-custom-filter-sub-component-single-value.vue", "packages/vue2/src/components/filter-bar/__tests__/filter-bar-custom-filter-sub-component-single-value-example.vue", "packages/vue2/src/components/data-grid/components/data-grid-cell/data-grid-cell.vue", "packages/vue2/src/components/data-grid/components/data-grid-columns/data-grid-columns.vue", "packages/vue2/src/components/data-grid/components/data-grid-config-settings/data-grid-config-settings.vue", "packages/vue2/src/components/data-grid/components/data-grid-config-settings/components/data-grid-config-settings-column-row/data-grid-config-settings-column-row.vue", "packages/vue2/src/components/menu/menu.vue"],
107
108
  "toggle-button": ["packages/vue2/src/components/header-with-views/components/header-with-views-item-edit-form/header-with-views-edit-form.vue", "packages/vue2/src/components/header-with-views/components/header-with-views-all-views/header-with-views-all-views.vue", "packages/vue2/src/components/header-with-views/header-with-views.vue", "packages/vue2/src/components/ai-tag/ai-tag.vue", "packages/vue2/src/components/data-grid/components/data-grid-cell/data-grid-cell.vue", "packages/vue2/src/components/data-grid/components/data-grid-columns/data-grid-columns.vue", "packages/vue2/src/components/data-grid/components/data-grid-config-settings/data-grid-config-settings.vue", "packages/vue2/src/components/menu/menu.vue"],
108
109
  "tutorial": [""],
109
- "typography": ["packages/vue2/src/components/dashed-underline/dashed-underline.visual.spec.tsx", "packages/vue2/src/components/dashed-underline/dashed-underline.vue", "packages/vue2/src/components/dashed-underline/dashed-underline.stories.ts", "packages/vue2/src/components/dashed-underline/__tests__/dashed-underline-inside-grid-stub.vue", "packages/vue2/src/components/status/status.vue", "packages/vue2/src/components/status/status.visual.spec.tsx", "packages/vue2/src/components/country/country.types.ts", "packages/vue2/src/components/country/country.vue", "packages/vue2/src/components/payment-method/payment-method.vue", "packages/vue2/src/components/anchor-scroller/anchor-scroller.vue", "packages/vue2/src/components/anchor-scroller/anchor-scroller.stories.ts", "packages/vue2/src/components/anchor-scroller/__tests__/anchor-scroller-default-example.vue", "packages/vue2/src/components/dropdown/dropdown.vue", "packages/vue2/src/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue", "packages/vue2/src/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.vue", "packages/vue2/src/components/dropdown/components/variants/dropdown-country/dropdown-country.vue", "packages/vue2/src/components/dropdown/__tests__/dropdown-with-custom-slots-example.vue", "packages/vue2/src/components/dropdown/__tests__/dropdown-scoped-slots-stub.vue", "packages/vue2/src/components/avatar/components/avatar-image/avatar-image.vue", "packages/vue2/src/components/avatar/avatar.vue", "packages/vue2/src/components/segmented-control/components/segmented-control-button/segmented-control-button.vue", "packages/vue2/src/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.vue", "packages/vue2/src/components/date-range-picker/date-range-picker.vue", "packages/vue2/src/components/currency/currency.vue", "packages/vue2/src/components/alert/alert.vue", "packages/vue2/src/components/date-picker/date-picker.vue", "packages/vue2/src/components/file-uploader/components/file-uploader-file-card/file-uploader-file-card.vue", "packages/vue2/src/components/file-uploader/components/file-uploader-restrictions/file-uploader-restrictions.vue", "packages/vue2/src/components/file-uploader/file-uploader.vue", "packages/vue2/src/components/chip/chip.vue", "packages/vue2/src/components/navigation-menu/navigation-menu.vue", "packages/vue2/src/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.vue", "packages/vue2/src/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.vue", "packages/vue2/src/components/popover/components/popover-title/popover-title.test.ts", "packages/vue2/src/components/popover/components/popover-title/popover-title.vue", "packages/vue2/src/components/popover/popover.visual.spec.tsx", "packages/vue2/src/components/popover/popover.stories.ts", "packages/vue2/src/components/button/button.vue", "packages/vue2/src/components/toast/components/toast-item/toast-item.vue", "packages/vue2/src/components/rich-text-editor/rich-text-editor.vue", "packages/vue2/src/components/drawer/drawer.stories.ts", "packages/vue2/src/components/drawer/__tests__/drawer-default-example.vue", "packages/vue2/src/components/checkbox/checkbox.vue", "packages/vue2/src/components/click-outside/click-outside.stories.ts", "packages/vue2/src/components/table-of-contents/components/table-of-contents-list/table-of-contents-list.vue", "packages/vue2/src/components/table-of-contents/table-of-contents.vue", "packages/vue2/src/components/table-of-contents/table-of-contents.stories.ts", "packages/vue2/src/components/toggle/toggle.vue", "packages/vue2/src/components/empty-state/empty-state.types.ts", "packages/vue2/src/components/empty-state/empty-state.vue", "packages/vue2/src/components/action-bar/action-bar.vue", "packages/vue2/src/components/layout/components/layout-row/layout-row.vue", "packages/vue2/src/components/secondary-nav/secondary-nav.vue", "packages/vue2/src/components/secondary-nav/components/secondary-nav-category/secondary-nav-category.vue", "packages/vue2/src/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue", "packages/vue2/src/components/header-with-views/components/header-with-views-all-views/header-with-views-all-views.vue", "packages/vue2/src/components/header-with-views/header-with-views.vue", "packages/vue2/src/components/accordion/components/accordion-item.vue", "packages/vue2/src/components/accordion/components/accordion-item.stories.ts", "packages/vue2/src/components/accordion/accordion.stories.ts", "packages/vue2/src/components/input-field/input-field.vue", "packages/vue2/src/components/input-field/input-field.test.ts", "packages/vue2/src/components/pagination/components/pagination-context/pagination-context.vue", "packages/vue2/src/components/pagination/components/pagination-results-per-page/pagination-results-per-page.vue", "packages/vue2/src/components/pagination/pagination.vue", "packages/vue2/src/components/link/link.stories.ts", "packages/vue2/src/components/link/link.vue", "packages/vue2/src/components/list/components/list-item/list-item.stories.ts", "packages/vue2/src/components/list/components/list-item/list-item.vue", "packages/vue2/src/components/form-layout/form-layout.docs.mdx", "packages/vue2/src/components/form-layout/components/form-layout-title/form-layout-title.vue", "packages/vue2/src/components/form-layout/form-layout.visual.spec.tsx", "packages/vue2/src/components/inspector/inspector.vue", "packages/vue2/src/components/inspector/inspector.stories.ts", "packages/vue2/src/components/modal-fullscreen/modal-fullscreen.stories.ts", "packages/vue2/src/components/modal-fullscreen/components/modal-fullscreen-page.vue", "packages/vue2/src/components/tag/tag.vue", "packages/vue2/src/components/modal/components/modal-page/modal-page.vue", "packages/vue2/src/components/promo-banner/promo-banner.stories.ts", "packages/vue2/src/components/promo-banner/promo-banner.vue", "packages/vue2/src/components/header/header.vue", "packages/vue2/src/components/date/date.vue", "packages/vue2/src/components/input-field-password/input-field-password.vue", "packages/vue2/src/components/structured-list/components/structered-list-item/structured-list-item.stories.ts", "packages/vue2/src/components/structured-list/components/structered-list-item/structured-list-item.vue", "packages/vue2/src/components/structured-list/structured-list.visual.spec.tsx", "packages/vue2/src/components/structured-list/__tests__/structured-list-default.vue", "packages/vue2/src/components/structured-list/structured-list.stories.ts", "packages/vue2/src/components/summary-grid/components/summary-grid-item-amount/summary-grid-item-amount.vue", "packages/vue2/src/components/summary-grid/components/summary-grid-item/summary-grid-item.test.ts", "packages/vue2/src/components/summary-grid/components/summary-grid-item/summary-grid-item.vue", "packages/vue2/src/components/stepper/components/step/step.vue", "packages/vue2/src/components/stepper/stepper.vue", "packages/vue2/src/components/selection-card/selection-card.vue", "packages/vue2/src/components/card/card.vue", "packages/vue2/src/components/card/card.docs.mdx", "packages/vue2/src/components/card/card.stories.ts", "packages/vue2/src/components/ai-tag/ai-tag.vue", "packages/vue2/src/components/radio-group/components/radio-button/radio-button.vue", "packages/vue2/src/components/tabs/tabs.vue", "packages/vue2/src/components/tabs/tabs.test.ts", "packages/vue2/src/components/internal/error-message/error-message.vue", "packages/vue2/src/components/internal/header-meta/header-meta.vue", "packages/vue2/src/components/internal/field-label/field-label.vue", "packages/vue2/src/components/internal/calendar/components/calendar-month/components/calendar-month-day/calendar-month-day.vue", "packages/vue2/src/components/internal/calendar/components/calendar-month/calendar-month.vue", "packages/vue2/src/components/internal/listbox/components/listbox-single-select-option/listbox-single-select-option.vue", "packages/vue2/src/components/internal/listbox/components/listbox-option-tag/listbox-option-tag.vue", "packages/vue2/src/components/internal/listbox/components/listbox-single-select/components/listbox-single-select-category/listbox-single-select-category.vue", "packages/vue2/src/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-category/listbox-multi-select-category.vue", "packages/vue2/src/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-category/listbox-multi-select-category.visual.spec.tsx", "packages/vue2/src/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/listbox-multi-select-options.vue", "packages/vue2/src/components/internal/listbox/components/listbox-multi-select/listbox-multi-select.vue", "packages/vue2/src/components/internal/listbox/listbox.vue", "packages/vue2/src/components/internal/footer-actions/footer-actions.vue", "packages/vue2/src/components/internal/info-icon-with-popover/info-icon-with-popover.vue", "packages/vue2/src/components/internal/controls-group/controls-group.vue", "packages/vue2/src/components/internal/base-button/base-button.vue", "packages/vue2/src/components/internal/tooltip/tooltip.vue", "packages/vue2/src/components/internal/copy/copy.stories.ts", "packages/vue2/src/components/draggable/draggable.stories.ts", "packages/vue2/src/components/draggable/__tests__/bento-draggable-default-example.vue", "packages/vue2/src/components/draggable/__tests__/bento-draggable-board-example.vue", "packages/vue2/src/components/draggable/__tests__/bento-draggable-nested-example.vue", "packages/vue2/src/components/draggable/__tests__/bento-draggable-grid-example.vue", "packages/vue2/src/components/sidepanel/components/sidepanel-page.vue", "packages/vue2/src/components/timeline/components/timeline-item/timeline-item.vue", "packages/vue2/src/components/tutorial/components/tutorial-popover/tutorial-popover.vue", "packages/vue2/src/components/tutorial/tutorial.stories.ts", "packages/vue2/src/components/code-snippet/code-snippet.vue", "packages/vue2/src/components/code-snippet/code-snippet.stories.ts", "packages/vue2/src/components/textarea/textarea.vue", "packages/vue2/src/components/filter-bar/components/all-filters-modal/all-filters-modal.vue", "packages/vue2/src/components/filter-bar/components/filter-bar-button/filter-bar-button.vue", "packages/vue2/src/components/filter-bar/components/base-filter-button/base-filter-button.vue", "packages/vue2/src/components/filter-bar/components/toggle-filter/toggle-filter.vue", "packages/vue2/src/components/filter-bar/components/select-filter/select-filter.stories.ts", "packages/vue2/src/components/filter-bar/components/select-filter/variants/select-country-filter/select-country-filter.vue", "packages/vue2/src/components/filter-bar/components/date-range-filter/date-range-filter.vue", "packages/vue2/src/components/filter-bar/components/date-range-filter/date-range-filter.visual.spec.tsx", "packages/vue2/src/components/data-grid/data-grid.visual.spec.tsx", "packages/vue2/src/components/data-grid/components/data-grid-trend-cell/data-grid-trend-cell.vue", "packages/vue2/src/components/data-grid/components/data-grid-summary-cell/data-grid-summary-cell.vue", "packages/vue2/src/components/data-grid/components/data-grid-cell/data-grid-cell.vue", "packages/vue2/src/components/data-grid/components/data-grid-columns/data-grid-columns.vue", "packages/vue2/src/components/data-grid/components/data-grid-config-settings/data-grid-config-settings.vue", "packages/vue2/src/components/data-grid/components/data-grid-tag-with-text-cell/data-grid-tag-with-text-cell.vue", "packages/vue2/src/components/data-grid/data-grid.vue", "packages/vue2/src/components/data-grid/__tests__/data-grid-with-slots-example.vue", "packages/vue2/src/components/data-grid/data-grid.stories.ts", "packages/vue2/src/components/menu/components/menu-item/menu-item.vue"]
110
+ "typography": ["packages/vue2/src/components/dashed-underline/dashed-underline.visual.spec.tsx", "packages/vue2/src/components/dashed-underline/dashed-underline.vue", "packages/vue2/src/components/dashed-underline/dashed-underline.stories.ts", "packages/vue2/src/components/dashed-underline/__tests__/dashed-underline-inside-grid-stub.vue", "packages/vue2/src/components/status/status.vue", "packages/vue2/src/components/status/status.visual.spec.tsx", "packages/vue2/src/components/country/country.types.ts", "packages/vue2/src/components/country/country.vue", "packages/vue2/src/components/payment-method/payment-method.vue", "packages/vue2/src/components/anchor-scroller/anchor-scroller.vue", "packages/vue2/src/components/anchor-scroller/anchor-scroller.stories.ts", "packages/vue2/src/components/anchor-scroller/__tests__/anchor-scroller-default-example.vue", "packages/vue2/src/components/dropdown/dropdown.vue", "packages/vue2/src/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue", "packages/vue2/src/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.vue", "packages/vue2/src/components/dropdown/components/variants/dropdown-country/dropdown-country.vue", "packages/vue2/src/components/dropdown/__tests__/dropdown-with-custom-slots-example.vue", "packages/vue2/src/components/dropdown/__tests__/dropdown-scoped-slots-stub.vue", "packages/vue2/src/components/avatar/components/avatar-image/avatar-image.vue", "packages/vue2/src/components/avatar/avatar.vue", "packages/vue2/src/components/segmented-control/components/segmented-control-button/segmented-control-button.vue", "packages/vue2/src/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.vue", "packages/vue2/src/components/date-range-picker/date-range-picker.vue", "packages/vue2/src/components/currency/currency.vue", "packages/vue2/src/components/alert/alert.vue", "packages/vue2/src/components/date-picker/date-picker.vue", "packages/vue2/src/components/file-uploader/components/file-uploader-file-card/file-uploader-file-card.vue", "packages/vue2/src/components/file-uploader/components/file-uploader-restrictions/file-uploader-restrictions.vue", "packages/vue2/src/components/file-uploader/file-uploader.vue", "packages/vue2/src/components/chip/chip.vue", "packages/vue2/src/components/navigation-menu/navigation-menu.vue", "packages/vue2/src/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.vue", "packages/vue2/src/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.vue", "packages/vue2/src/components/popover/components/popover-title/popover-title.test.ts", "packages/vue2/src/components/popover/components/popover-title/popover-title.vue", "packages/vue2/src/components/popover/popover.visual.spec.tsx", "packages/vue2/src/components/popover/popover.stories.ts", "packages/vue2/src/components/button/button.vue", "packages/vue2/src/components/toast/components/toast-item/toast-item.vue", "packages/vue2/src/components/rich-text-editor/rich-text-editor.vue", "packages/vue2/src/components/drawer/drawer.stories.ts", "packages/vue2/src/components/drawer/__tests__/drawer-default-example.vue", "packages/vue2/src/components/checkbox/checkbox.vue", "packages/vue2/src/components/click-outside/click-outside.stories.ts", "packages/vue2/src/components/table-of-contents/components/table-of-contents-list/table-of-contents-list.vue", "packages/vue2/src/components/table-of-contents/table-of-contents.vue", "packages/vue2/src/components/table-of-contents/table-of-contents.stories.ts", "packages/vue2/src/components/toggle/toggle.vue", "packages/vue2/src/components/empty-state/empty-state.types.ts", "packages/vue2/src/components/empty-state/empty-state.vue", "packages/vue2/src/components/action-bar/action-bar.vue", "packages/vue2/src/components/layout/components/layout-row/layout-row.vue", "packages/vue2/src/components/secondary-nav/secondary-nav.vue", "packages/vue2/src/components/secondary-nav/components/secondary-nav-category/secondary-nav-category.vue", "packages/vue2/src/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue", "packages/vue2/src/components/header-with-views/components/header-with-views-all-views/header-with-views-all-views.vue", "packages/vue2/src/components/header-with-views/header-with-views.vue", "packages/vue2/src/components/accordion/components/accordion-item.vue", "packages/vue2/src/components/accordion/components/accordion-item.stories.ts", "packages/vue2/src/components/accordion/accordion.stories.ts", "packages/vue2/src/components/input-field/input-field.vue", "packages/vue2/src/components/input-field/input-field.test.ts", "packages/vue2/src/components/pagination/components/pagination-context/pagination-context.vue", "packages/vue2/src/components/pagination/components/pagination-results-per-page/pagination-results-per-page.vue", "packages/vue2/src/components/pagination/pagination.vue", "packages/vue2/src/components/link/link.stories.ts", "packages/vue2/src/components/link/link.vue", "packages/vue2/src/components/list/components/list-item/list-item.stories.ts", "packages/vue2/src/components/list/components/list-item/list-item.vue", "packages/vue2/src/components/form-layout/form-layout.docs.mdx", "packages/vue2/src/components/form-layout/components/form-layout-title/form-layout-title.vue", "packages/vue2/src/components/form-layout/form-layout.visual.spec.tsx", "packages/vue2/src/components/inspector/inspector.vue", "packages/vue2/src/components/inspector/inspector.stories.ts", "packages/vue2/src/components/inspector/components/inspector-page/inspector-page.vue", "packages/vue2/src/components/inspector/__tests__/inspector-with-multiple-pages-example.vue", "packages/vue2/src/components/modal-fullscreen/modal-fullscreen.stories.ts", "packages/vue2/src/components/modal-fullscreen/components/modal-fullscreen-page.vue", "packages/vue2/src/components/tag/tag.vue", "packages/vue2/src/components/modal/components/modal-page/modal-page.vue", "packages/vue2/src/components/promo-banner/promo-banner.stories.ts", "packages/vue2/src/components/promo-banner/promo-banner.vue", "packages/vue2/src/components/header/header.vue", "packages/vue2/src/components/date/date.vue", "packages/vue2/src/components/input-field-password/input-field-password.vue", "packages/vue2/src/components/structured-list/components/structered-list-item/structured-list-item.stories.ts", "packages/vue2/src/components/structured-list/components/structered-list-item/structured-list-item.vue", "packages/vue2/src/components/structured-list/structured-list.visual.spec.tsx", "packages/vue2/src/components/structured-list/__tests__/structured-list-default.vue", "packages/vue2/src/components/structured-list/structured-list.stories.ts", "packages/vue2/src/components/summary-grid/components/summary-grid-item-amount/summary-grid-item-amount.vue", "packages/vue2/src/components/summary-grid/components/summary-grid-item/summary-grid-item.test.ts", "packages/vue2/src/components/summary-grid/components/summary-grid-item/summary-grid-item.vue", "packages/vue2/src/components/stepper/components/step/step.vue", "packages/vue2/src/components/stepper/stepper.vue", "packages/vue2/src/components/selection-card/selection-card.vue", "packages/vue2/src/components/card/card.vue", "packages/vue2/src/components/card/card.docs.mdx", "packages/vue2/src/components/card/card.stories.ts", "packages/vue2/src/components/ai-tag/ai-tag.vue", "packages/vue2/src/components/radio-group/components/radio-button/radio-button.vue", "packages/vue2/src/components/tabs/tabs.vue", "packages/vue2/src/components/tabs/tabs.test.ts", "packages/vue2/src/components/internal/error-message/error-message.vue", "packages/vue2/src/components/internal/header-meta/header-meta.vue", "packages/vue2/src/components/internal/field-label/field-label.vue", "packages/vue2/src/components/internal/calendar/components/calendar-month/components/calendar-month-day/calendar-month-day.vue", "packages/vue2/src/components/internal/calendar/components/calendar-month/calendar-month.vue", "packages/vue2/src/components/internal/listbox/components/listbox-single-select-option/listbox-single-select-option.vue", "packages/vue2/src/components/internal/listbox/components/listbox-option-tag/listbox-option-tag.vue", "packages/vue2/src/components/internal/listbox/components/listbox-single-select/components/listbox-single-select-category/listbox-single-select-category.vue", "packages/vue2/src/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-category/listbox-multi-select-category.vue", "packages/vue2/src/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-category/listbox-multi-select-category.visual.spec.tsx", "packages/vue2/src/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/listbox-multi-select-options.vue", "packages/vue2/src/components/internal/listbox/components/listbox-multi-select/listbox-multi-select.vue", "packages/vue2/src/components/internal/listbox/listbox.vue", "packages/vue2/src/components/internal/footer-actions/footer-actions.vue", "packages/vue2/src/components/internal/info-icon-with-popover/info-icon-with-popover.vue", "packages/vue2/src/components/internal/controls-group/controls-group.vue", "packages/vue2/src/components/internal/base-button/base-button.vue", "packages/vue2/src/components/internal/tooltip/tooltip.vue", "packages/vue2/src/components/internal/copy/copy.stories.ts", "packages/vue2/src/components/draggable/draggable.stories.ts", "packages/vue2/src/components/draggable/__tests__/bento-draggable-default-example.vue", "packages/vue2/src/components/draggable/__tests__/bento-draggable-board-example.vue", "packages/vue2/src/components/draggable/__tests__/bento-draggable-nested-example.vue", "packages/vue2/src/components/draggable/__tests__/bento-draggable-grid-example.vue", "packages/vue2/src/components/sidepanel/components/sidepanel-page.vue", "packages/vue2/src/components/timeline/components/timeline-item/timeline-item.vue", "packages/vue2/src/components/tutorial/components/tutorial-popover/tutorial-popover.vue", "packages/vue2/src/components/tutorial/tutorial.stories.ts", "packages/vue2/src/components/code-snippet/code-snippet.vue", "packages/vue2/src/components/code-snippet/code-snippet.stories.ts", "packages/vue2/src/components/textarea/textarea.vue", "packages/vue2/src/components/filter-bar/components/all-filters-modal/all-filters-modal.vue", "packages/vue2/src/components/filter-bar/components/filter-bar-button/filter-bar-button.vue", "packages/vue2/src/components/filter-bar/components/base-filter-button/base-filter-button.vue", "packages/vue2/src/components/filter-bar/components/toggle-filter/toggle-filter.vue", "packages/vue2/src/components/filter-bar/components/select-filter/select-filter.stories.ts", "packages/vue2/src/components/filter-bar/components/select-filter/variants/select-country-filter/select-country-filter.vue", "packages/vue2/src/components/filter-bar/components/date-range-filter/date-range-filter.vue", "packages/vue2/src/components/filter-bar/components/date-range-filter/date-range-filter.visual.spec.tsx", "packages/vue2/src/components/data-grid/data-grid.visual.spec.tsx", "packages/vue2/src/components/data-grid/components/data-grid-trend-cell/data-grid-trend-cell.vue", "packages/vue2/src/components/data-grid/components/data-grid-summary-cell/data-grid-summary-cell.vue", "packages/vue2/src/components/data-grid/components/data-grid-cell/data-grid-cell.vue", "packages/vue2/src/components/data-grid/components/data-grid-columns/data-grid-columns.vue", "packages/vue2/src/components/data-grid/components/data-grid-config-settings/data-grid-config-settings.vue", "packages/vue2/src/components/data-grid/components/data-grid-tag-with-text-cell/data-grid-tag-with-text-cell.vue", "packages/vue2/src/components/data-grid/data-grid.vue", "packages/vue2/src/components/data-grid/__tests__/data-grid-with-slots-example.vue", "packages/vue2/src/components/data-grid/data-grid.stories.ts", "packages/vue2/src/components/menu/components/menu-item/menu-item.vue"]
110
111
  }
package/dist/main.js CHANGED
@@ -30537,7 +30537,7 @@ ${usage.map((f) => `- ${f}`).join("\n")}`
30537
30537
  var package_default = {
30538
30538
  name: "@adyen/bento-mcp",
30539
30539
  mcpName: "io.github.adyen/bento-mcp",
30540
- version: "0.7.0",
30540
+ version: "0.8.0",
30541
30541
  type: "module",
30542
30542
  description: "A Model Context Protocol server implementation for Bento",
30543
30543
  license: "MIT",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adyen/bento-mcp",
3
3
  "mcpName": "io.github.adyen/bento-mcp",
4
- "version": "0.7.0",
4
+ "version": "0.8.0",
5
5
  "type": "module",
6
6
  "description": "A Model Context Protocol server implementation for Bento",
7
7
  "license": "MIT",