@adyen/bento-mcp 0.5.1 → 0.5.2

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/assets/components/accordion/components/accordion-item.vue +1 -1
  3. package/dist/assets/components/card/card.vue +1 -1
  4. package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.vue +1 -1
  5. package/dist/assets/components/draggable/draggable.docs.mdx +76 -135
  6. package/dist/assets/components/draggable/draggable.stories.ts +1 -1
  7. package/dist/assets/components/draggable/draggable.types.ts +1 -1
  8. package/dist/assets/components/draggable/draggable.vue +1 -1
  9. package/dist/assets/components/filter-bar/components/base-filter/base-filter.vue +1 -1
  10. package/dist/assets/components/internal/calendar/calendar.types.ts +1 -1
  11. package/dist/assets/components/internal/calendar/calendar.vue +1 -1
  12. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.stories.ts +1 -1
  13. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.types.ts +1 -1
  14. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.vue +1 -1
  15. package/dist/assets/components/internal/calendar/components/calendar-month/composables/granular-min-max-date.types.ts +1 -1
  16. package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.types.ts +1 -1
  17. package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.vue +1 -1
  18. package/dist/assets/components/internal/teleport/teleport.vue +1 -1
  19. package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.vue +1 -1
  20. package/dist/assets/components/secondary-nav/components/secondary-nav-category/secondary-nav-category.vue +1 -1
  21. package/dist/assets/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue +1 -1
  22. package/dist/assets/components/secondary-nav/secondary-nav.stories.ts +1 -1
  23. package/dist/assets/components/secondary-nav/secondary-nav.types.ts +1 -1
  24. package/dist/assets/components/secondary-nav/secondary-nav.vue +1 -1
  25. package/dist/assets/components/tabs/components/tab.types.ts +1 -1
  26. package/dist/assets/components/tabs/components/tab.vue +1 -1
  27. package/dist/assets/components/tabs/tabs.stories.ts +1 -1
  28. package/dist/assets/components/tabs/tabs.types.ts +1 -1
  29. package/dist/assets/components/tabs/tabs.vue +1 -1
  30. package/dist/assets/components.json +1 -0
  31. package/dist/assets/index.ts +1 -1
  32. package/dist/assets/usage.json +5 -4
  33. package/dist/main.js +1 -1
  34. package/package.json +1 -1
@@ -1 +1 @@
1
- <template> <div v-if="onlySlot"> <slot></slot> </div> <div v-else class="b-base-filter"> <filter-bar-button-group v-bento-tooltip-directive:[tooltipPosition]="tooltipText" :is-active="isFilterOpen" :skip-grouping="!isValueChanged" > <filter-bar-button ref="filterButtonRef" :aria-label="computedAriaLabel" :aria-controls="isFilterOpen ? popoverId : null" :applied-filters-count="appliedFiltersCount" :disabled="disabled" :is-persistent-filter="isPersistentFilter" :is-filter-open="isFilterOpen" :secondary-label="buttonSecondaryLabel" @click="onClickFilter" @keydown.esc="closePopover" > {{ computedButtonText }} </filter-bar-button> <filter-bar-button v-if="isValueChanged" :applied-filters-count="appliedFiltersCount" :disabled="disabled" :is-persistent-filter="isPersistentFilter" :is-filter-open="isFilterOpen" :secondary-label="buttonSecondaryLabel" @click="resetFilter" > <template #iconLeft> <cross-small-icon :svg-title="computedClearButtonLabel"></cross-small-icon> </template> </filter-bar-button> </filter-bar-button-group> <bento-teleport v-if="filterButtonRef"> <bento-popover v-if="filterButtonRef" :id="popoverId" ref="popoverRef" class="b-base-filter__popover" divider :aria-label="label" :open="isFilterOpen" :actions="controlled ? null : actions" actions-layout="space-between" :target-element="filterButtonRef" :title="controlled ? null : label" :fit-content="fitContent" position="bottom-start" :without-space="controlled" :fallback-position="['bottom-end']" overflow-visible > <div @keydown.esc="onCancel"> <slot></slot> </div> </bento-popover> </bento-teleport> </div> </template> <script setup lang="ts"> import { computed, nextTick, ref, watch } from 'vue'; import BentoPopover from '@/components/popover/popover.vue'; import { BentoTooltipDirective as vBentoTooltipDirective } from '@/directives/tooltip/tooltip'; import { FilterBarButton } from '../filter-bar-button'; import FilterBarButtonGroup from '../filter-bar-button-group/filter-bar-button-group.vue'; import { useI18n } from '@/utils/ts/i18n'; import { BaseFilterEvent, type BentoBaseFilterProps } from './base-filter.types'; import messages from './messages.json'; import { generateUid } from '@/core/utils/ts/generate-uid'; import { useClickOutside } from '@/composables/use-click-outside'; import BentoTeleport from '@/internal/teleport/teleport.vue'; import type { BentoFilterBarValue } from '../../filter-bar.types'; import CrossSmallIcon from '@adyen/ui-assets-icons-16/vue/cross-small'; type MessageSchema = (typeof messages)['en-US']; const emit = defineEmits([ BaseFilterEvent.APPLY, BaseFilterEvent.CLEAR, BaseFilterEvent.OPEN, BaseFilterEvent.CLOSE, BaseFilterEvent.RESET, BaseFilterEvent.CANCEL, ]); const props = withDefaults(defineProps<BentoBaseFilterProps<BentoFilterBarValue, string>>(), { appliedFiltersCount: null, buttonLabel: undefined, buttonSecondaryLabel: undefined, disabled: false, defaultValue: null, onlySlot: false, controlled: false, disableApplyButton: false, disableSecondaryButton: false, fitContent: undefined, tooltipText: undefined, tooltipPosition: undefined, value: undefined, }); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const popoverId = generateUid('base-filter-popover'); const isFilterOpen = ref(false); const filterButtonRef = ref(null); const popoverRef = ref(null); const isPersistentFilter = computed(() => props.defaultValue !== null); const isValueChanged = computed(() => { // If it's not a persistent filter, its value can't have changed. if (!isPersistentFilter.value) { return props.appliedFiltersCount || props.buttonSecondaryLabel; } // Use JSON.stringify for a simple but effective deep comparison // of objects, arrays, and primitives. return JSON.stringify(props.value) !== JSON.stringify(props.defaultValue); }); const computedAriaLabel = computed(() => { let ariaLabelValuePart = props.buttonSecondaryLabel ? `: ${props.buttonSecondaryLabel}` : ''; if (props.appliedFiltersCount) { ariaLabelValuePart = `: ${t('numberSelected', { numberOfItemsSelected: props.appliedFiltersCount })}`; } return (isPersistentFilter.value ? props.label : computedButtonText.value) + ariaLabelValuePart; }); const computedClearButtonLabel = computed(() => isPersistentFilter.value ? t('resetFilter', { function: computedButtonText.value, }) : t('clearFilter', { function: computedButtonText.value, }) ); const computedButtonText = computed(() => props.buttonLabel || props.label); const resetFocus = async () => { await nextTick(); filterButtonRef.value?.$el?.focus(); }; const closePopover = () => { if (!isFilterOpen.value) { return; } isFilterOpen.value = false; resetFocus(); emit(BaseFilterEvent.CLOSE); }; const resetFilter = () => { if (isPersistentFilter.value) { emit(BaseFilterEvent.RESET); } else { emit(BaseFilterEvent.CLEAR); } resetFocus(); }; const onClickSecondaryAction = () => { closePopover(); resetFilter(); }; const onClickFilter = () => { if (isFilterOpen.value) { closePopover(); } else { isFilterOpen.value = true; emit(BaseFilterEvent.OPEN); } }; const onClickApply = () => { closePopover(); emit(BaseFilterEvent.APPLY); }; const onCancel = () => { closePopover(); emit(BaseFilterEvent.CANCEL); }; const onClickOutside = () => onCancel(); const actions = computed(() => [ { title: t('apply') as string, event: onClickApply, disabled: props.disableApplyButton, }, { title: isPersistentFilter.value ? (t('reset') as string) : (t('clear') as string), event: onClickSecondaryAction, disabled: props.disableSecondaryButton, }, ]); watch( () => isFilterOpen.value, (value, prevValue) => { if (!value && prevValue) { if (filterButtonRef.value?.filterBarButtonRef?.bentoBaseButtonRef) { filterButtonRef.value.filterBarButtonRef.bentoBaseButtonRef.focus(); } } } ); // Ignore the events inside the Button and the Popover useClickOutside(popoverRef, onClickOutside, { ignore: [filterButtonRef] }); defineExpose({ closePopover, }); </script> <script lang="ts"> export default { i18n: { messages }, name: 'bento-base-filter', }; </script> <style lang="scss" scoped src="./base-filter.scss"></style>
1
+ <template> <div v-if="onlySlot"> <slot></slot> </div> <div v-else class="b-base-filter"> <filter-bar-button-group v-bento-tooltip-directive:[tooltipPosition]="tooltipText" :is-active="isFilterOpen" :skip-grouping="!isValueChanged" > <filter-bar-button ref="filterButtonRef" :aria-label="computedAriaLabel" :aria-controls="isFilterOpen ? popoverId : null" :applied-filters-count="appliedFiltersCount" :disabled="disabled" :is-persistent-filter="isPersistentFilter" :is-filter-open="isFilterOpen" :secondary-label="buttonSecondaryLabel" @click="onClickFilter" @keydown.esc="closePopover" > {{ computedButtonText }} </filter-bar-button> <filter-bar-button v-if="isValueChanged" :applied-filters-count="appliedFiltersCount" :disabled="disabled" :is-persistent-filter="isPersistentFilter" :is-filter-open="isFilterOpen" :secondary-label="buttonSecondaryLabel" @click="resetFilter" > <template #iconLeft> <cross-small-icon :svg-title="computedClearButtonLabel"></cross-small-icon> </template> </filter-bar-button> </filter-bar-button-group> <bento-teleport v-if="filterButtonEl"> <bento-popover :id="popoverId" :ref="setPopoverRef" class="b-base-filter__popover" divider :aria-label="label" :open="isFilterOpen" :actions="controlled ? null : actions" actions-layout="space-between" :target-element="filterButtonEl" :title="controlled ? null : label" :fit-content="fitContent" position="bottom-start" :without-space="controlled" :fallback-position="['bottom-end']" overflow-visible > <div @keydown.esc="onCancel"> <slot></slot> </div> </bento-popover> </bento-teleport> </div> </template> <script setup lang="ts"> import { computed, nextTick, ref, watch } from 'vue'; import BentoPopover from '@/components/popover/popover.vue'; import { BentoTooltipDirective as vBentoTooltipDirective } from '@/directives/tooltip/tooltip'; import { FilterBarButton } from '../filter-bar-button'; import FilterBarButtonGroup from '../filter-bar-button-group/filter-bar-button-group.vue'; import { useI18n } from '@/utils/ts/i18n'; import { BaseFilterEvent, type BentoBaseFilterProps } from './base-filter.types'; import messages from './messages.json'; import { generateUid } from '@/core/utils/ts/generate-uid'; import { useClickOutside } from '@/composables/use-click-outside'; import BentoTeleport from '@/internal/teleport/teleport.vue'; import type { BentoFilterBarValue } from '../../filter-bar.types'; import CrossSmallIcon from '@adyen/ui-assets-icons-16/vue/cross-small'; type MessageSchema = (typeof messages)['en-US']; const emit = defineEmits([ BaseFilterEvent.APPLY, BaseFilterEvent.CLEAR, BaseFilterEvent.OPEN, BaseFilterEvent.CLOSE, BaseFilterEvent.RESET, BaseFilterEvent.CANCEL, ]); const props = withDefaults(defineProps<BentoBaseFilterProps<BentoFilterBarValue, string>>(), { appliedFiltersCount: null, buttonLabel: undefined, buttonSecondaryLabel: undefined, disabled: false, defaultValue: null, onlySlot: false, controlled: false, disableApplyButton: false, disableSecondaryButton: false, fitContent: undefined, tooltipText: undefined, tooltipPosition: undefined, value: undefined, }); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const popoverId = generateUid('base-filter-popover'); const isFilterOpen = ref(false); const filterButtonRef = ref(null); const popoverRef = ref(null); const filterButtonEl = computed(() => filterButtonRef.value?.$el ?? null); const setPopoverRef = (instance: unknown) => { popoverRef.value = instance ?? null; }; const isPersistentFilter = computed(() => props.defaultValue !== null); const isValueChanged = computed(() => { // If it's not a persistent filter, its value can't have changed. if (!isPersistentFilter.value) { return props.appliedFiltersCount || props.buttonSecondaryLabel; } // Use JSON.stringify for a simple but effective deep comparison // of objects, arrays, and primitives. return JSON.stringify(props.value) !== JSON.stringify(props.defaultValue); }); const computedAriaLabel = computed(() => { let ariaLabelValuePart = props.buttonSecondaryLabel ? `: ${props.buttonSecondaryLabel}` : ''; if (props.appliedFiltersCount) { ariaLabelValuePart = `: ${t('numberSelected', { numberOfItemsSelected: props.appliedFiltersCount })}`; } return (isPersistentFilter.value ? props.label : computedButtonText.value) + ariaLabelValuePart; }); const computedClearButtonLabel = computed(() => isPersistentFilter.value ? t('resetFilter', { function: computedButtonText.value, }) : t('clearFilter', { function: computedButtonText.value, }) ); const computedButtonText = computed(() => props.buttonLabel || props.label); const resetFocus = async () => { await nextTick(); filterButtonRef.value?.$el?.focus(); }; const closePopover = () => { if (!isFilterOpen.value) { return; } isFilterOpen.value = false; resetFocus(); emit(BaseFilterEvent.CLOSE); }; const resetFilter = () => { if (isPersistentFilter.value) { emit(BaseFilterEvent.RESET); } else { emit(BaseFilterEvent.CLEAR); } resetFocus(); }; const onClickSecondaryAction = () => { closePopover(); resetFilter(); }; const onClickFilter = () => { if (isFilterOpen.value) { closePopover(); } else { isFilterOpen.value = true; emit(BaseFilterEvent.OPEN); } }; const onClickApply = () => { closePopover(); emit(BaseFilterEvent.APPLY); }; const onCancel = () => { closePopover(); emit(BaseFilterEvent.CANCEL); }; const onClickOutside = () => onCancel(); const actions = computed(() => [ { title: t('apply') as string, event: onClickApply, disabled: props.disableApplyButton, }, { title: isPersistentFilter.value ? (t('reset') as string) : (t('clear') as string), event: onClickSecondaryAction, disabled: props.disableSecondaryButton, }, ]); watch( () => isFilterOpen.value, (value, prevValue) => { if (!value && prevValue) { if (filterButtonRef.value?.filterBarButtonRef?.bentoBaseButtonRef) { filterButtonRef.value.filterBarButtonRef.bentoBaseButtonRef.focus(); } } } ); // Ignore the events inside the Button and the Popover useClickOutside(popoverRef, onClickOutside, { ignore: [filterButtonRef] }); defineExpose({ closePopover, }); </script> <script lang="ts"> export default { i18n: { messages }, name: 'bento-base-filter', }; </script> <style lang="scss" scoped src="./base-filter.scss"></style>
@@ -1 +1 @@
1
- import { type Ref, type UnwrapNestedRefs } from 'vue'; import type { Day } from 'date-fns'; export { CalendarMonthFirstDayOfWeek as CalendarFirstDayOfWeek } from './components/calendar-month/calendar-month.types'; export type { CalendarMonthIsDateDisabled as CalendarIsDateDisabled } from './components/calendar-month/calendar-month.types'; export { CalendarYearPosition as CalendarPosition } from './components/calendar-year/calendar-year.types'; export type CalendarGranularityType = 'daily' | 'weekly' | 'monthly' | 'quarterly'; export interface CalendarGranularityConfig<T extends CalendarGranularityType = CalendarGranularityType> { type: T; earliest?: Date; latest?: Date; disabled?: boolean; maxRange?: number; } export interface CalendarRangeDateValue { startDate: Date; endDate: Date; granularity?: CalendarGranularityType; range?: string; } export type CalendarSingleDateValue = Date; export type CalendarValue = CalendarSingleDateValue | CalendarRangeDateValue; interface UseCalendarPaneProps { isDateDisabled?: (date: Date) => boolean; maxRange?: Ref<number>; selectedDate: Ref<CalendarValue>; granularity?: Ref<CalendarGranularityConfig>; firstDayOfWeek: Day; min?: Ref<Date>; max?: Ref<Date>; } export type UseCalendarPane = ( propRefs: UseCalendarPaneProps, emit ) => { internalValue: UnwrapNestedRefs<CalendarRangeDateValue>; onDateInput: (date: Date) => void; onMouseOver: (date: Date) => void; }; export enum CalendarEvent { INPUT = 'input', START_DATE_SELECTED = 'start-date-selected', END_DATE_SELECTED = 'end-date-selected', } /** * Private types */ export enum CalendarVariant { DAY = 'day', MONTH = 'month', }
1
+ import { type Ref, type UnwrapNestedRefs } from 'vue'; import type { Day } from 'date-fns'; /** * Defines the first day of the week */ export enum CalendarFirstDayOfWeek { MONDAY = 1, SATURDAY = 6, SUNDAY = 0, } export type CalendarIsDateDisabled = (date: Date) => boolean; /** * Defines the position in which the calendar will be displayed */ export enum CalendarPosition { FIRST = 'first', LAST = 'last', MIDDLE = 'middle', UNIQUE = 'unique', } export type CalendarGranularityType = 'daily' | 'weekly' | 'monthly' | 'quarterly'; export interface CalendarGranularityConfig<T extends CalendarGranularityType = CalendarGranularityType> { type: T; earliest?: Date; latest?: Date; disabled?: boolean; maxRange?: number; } export interface CalendarRangeDateValue { startDate: Date; endDate: Date; granularity?: CalendarGranularityType; range?: string; } export type CalendarSingleDateValue = Date; export type CalendarValue = CalendarSingleDateValue | CalendarRangeDateValue; interface UseCalendarPaneProps { isDateDisabled?: (date: Date) => boolean; maxRange?: Ref<number>; selectedDate: Ref<CalendarValue>; granularity?: Ref<CalendarGranularityConfig>; firstDayOfWeek: Day; min?: Ref<Date>; max?: Ref<Date>; } export enum CalendarEvent { INPUT = 'input', START_DATE_SELECTED = 'start-date-selected', END_DATE_SELECTED = 'end-date-selected', } /** * Private types */ export enum CalendarVariant { DAY = 'day', MONTH = 'month', } export type UseCalendarPane = ( propRefs: UseCalendarPaneProps, emit ) => { internalValue: UnwrapNestedRefs<CalendarRangeDateValue>; onDateInput: (date: Date) => void; onMouseOver: (date: Date) => void; }; export interface CalendarEmit { /** * Triggered when a date is selected */ (e: 'input', date: Date): void; /** * Triggered when the active day changes */ (e: 'changeDay', day: Date): void; /** * Triggered when navigating to the next month */ (e: 'nextMonth', month: number): void; /** * Triggered when navigating to the previous month */ (e: 'previousMonth', month: number): void; /** * Triggered when the mouse hovers over a date, used for range selection */ (e: 'mouseover', date: Date): void; }
@@ -1 +1 @@
1
- <template> <div class="b-calendar" :class="conditionalClasses"> <component :is="calendarComponent" v-for="(_, index) in numberOfMonths" :ref="numberOfMonths === 1 ? 'calendarRef' : null" :key="`calendar-${index}`" :first-day-of-week="firstDayOfWeek" :is-date-disabled="isDateDisabled" :month="calculateOffsetMonth(index)" :year="calculateOffsetYear(index)" :position="calendarPosition(index)" :min="min" :max="max" :is-range="isRange" :start-date="internalValue.startDate" :end-date="internalValue.endDate" :granularity="granularity" @input="onDateInput" @mouseover="onMouseOver" @changeDay="onMouseOver" @nextMonth="calculateNextMonths" @previousMonth="calculatePreviousMonths" /> </div> </template> <script lang="ts"> import { computed, defineComponent, type PropType, ref, toRef, watch } from 'vue'; import CalendarMonth from './components/calendar-month/calendar-month.vue'; import CalendarYear from './components/calendar-year/calendar-year.vue'; import { CalendarMonthFirstDayOfWeek } from './components/calendar-month'; import type { CalendarMonthIsDateDisabled } from './components/calendar-month'; import { CalendarEvent, type CalendarGranularityConfig, CalendarPosition, type CalendarRangeDateValue, type CalendarValue, CalendarVariant, } from './calendar.types'; import { useSinglePane } from './composables/use-single-pane'; import { useRangePane } from './composables/use-range-pane'; import { addMonths } from 'date-fns/addMonths'; import { subMonths } from 'date-fns/subMonths'; import { addYears } from 'date-fns/addYears'; import { subYears } from 'date-fns/subYears'; /** * Handles the calendar logic to change visible months for Single calendar * and Range calendar, as well as the selection logic for range. * All the processing necessary for the calendar to select dates * * @example * import { Calendar } from '@/internal/bento-vue2'; * * export default { * components: { Calendar }, * template: ` * <calendar * :value="value" * :first-day-of-week="CalendarFirstDayOfWeek.Monday" * :is-date-disabled="(day: Date) => boolean" * :number-of-months="2" * is-range * @input="(selectedDate) => void" * @start-date-selected="(startDate: Date) => void" * @end-date-selected="(endDate: Date) => void" * /> * ` * } */ export default defineComponent({ name: 'calendar', components: { CalendarMonth, CalendarYear }, props: { /** * Allows you to set the first day of the week. 0 is sunday, 1 is monday, 6 is saturday */ firstDayOfWeek: { type: Number as PropType<CalendarMonthFirstDayOfWeek>, default: CalendarMonthFirstDayOfWeek.MONDAY, validator: (value: CalendarMonthFirstDayOfWeek) => Object.values(CalendarMonthFirstDayOfWeek).includes(value), }, /** * The selected granularity configuration */ granularity: { type: Object as PropType<CalendarGranularityConfig>, default: null, }, /** * Indicate if a date should be disabled or not */ isDateDisabled: { type: Function as PropType<CalendarMonthIsDateDisabled>, default: undefined }, /** * Enables the Date Range calendar */ isRange: { type: Boolean, default: false }, /** * Set a maximum number of dates to be selectable by the range. */ maxRange: { type: Number, default: null, validator: (value: number) => !value || value > 0 }, /** * Sets a minimum limit for selectable dates - dates before `min` will not be selectable */ min: { type: Date, default: null }, /** * Sets a maximum limit for selectable dates - dates after `max` will not be selectable */ max: { type: Date, default: null }, /** * Number of months rendered on pane */ numberOfMonths: { type: Number, default: 3, validator: (n: number) => n >= 1, }, /** * Upon opening date range picker, end date's month will be pre-selected. */ showEndDateOnOpen: { type: Boolean, default: false }, /** * Selected date. Can be a single Date or a CalendarRangeDate * instance that contains startDate and endDate attributes. */ value: { type: [Date, Object] as PropType<CalendarValue>, default: undefined }, /** * Default start date for the calendar pane */ defaultDisplayMonth: { type: String, default: null, validator: (value: string) => { // Checks for YYYY-MM-DD format const dateRegex = /^\d{4}-(0?[1-9]|1[012])(-(0?[1-9]|[12][0-9]|3[01]))?$/; const isValidDate = dateRegex.test(value); return isValidDate; }, }, /** * The type of calendar to display. Defaults to showing days. */ variant: { type: String as PropType<`${CalendarVariant}`>, default: 'day', validator: (value: string) => Object.values(CalendarVariant).includes(value as CalendarVariant), }, }, emit: [CalendarEvent.INPUT, CalendarEvent.START_DATE_SELECTED, CalendarEvent.END_DATE_SELECTED], setup(props, { emit }) { // Single date calendar ref const calendarRef = ref(null); // Use a duplicate of "month" to move back and // forth with the arrows. Maintained in sync with the prop "value" const baseDate = ref(); watch( () => props.value, () => { if (props.isRange) { baseDate.value = props?.showEndDateOnOpen && (props.value as CalendarRangeDateValue)?.endDate ? (props.value as CalendarRangeDateValue)?.endDate : (props.value as CalendarRangeDateValue)?.startDate || new Date(); } else { const defaultDate = props.defaultDisplayMonth && new Date(props.defaultDisplayMonth).getTime() ? new Date(props.defaultDisplayMonth) : new Date(); baseDate.value = (props.value as Date) || defaultDate; } }, { immediate: true, deep: true } ); const calendarComponent = computed(() => props.variant === CalendarVariant.MONTH ? CalendarYear : CalendarMonth ); const conditionalClasses = computed(() => ({ 'b-calendar__range': props.isRange, })); const { internalValue, onDateInput, onMouseOver } = props.isRange ? useRangePane( { selectedDate: toRef(props, 'value'), isDateDisabled: props.isDateDisabled as (date: Date) => boolean, maxRange: toRef(props, 'maxRange'), granularity: toRef(props, 'granularity'), firstDayOfWeek: props.firstDayOfWeek, min: toRef(props, 'min'), max: toRef(props, 'max'), }, emit ) : useSinglePane( { selectedDate: toRef(props, 'value'), firstDayOfWeek: props.firstDayOfWeek, }, emit ); /** * Calculate the current month calendar. * This is used to render/hide the Previous & Next buttons on each calendar. * @param {number} calendarIndex - Number to offset the calendar position */ const calendarPosition = (calendarIndex: number): `${CalendarPosition}` => { if (props.numberOfMonths === 1) { return CalendarPosition.UNIQUE; } if (calendarIndex === 0) { return CalendarPosition.FIRST; } return calendarIndex === props.numberOfMonths - 1 ? CalendarPosition.LAST : CalendarPosition.MIDDLE; }; /** * Calculates in relation to the amount of months defined in "numberOfMonths" * which calendar month should be render starting from the first (leftmost) * rendered calendar. * If showEndDateOnOpen is set to true, the last pane should be the baseDate month i.e. the end date * @param offsetMonth - Amount of months to add after the first month was rendered */ const calculateOffsetMonth = computed(() => (offsetMonth: number) => { const monthOffset = props?.showEndDateOnOpen && props.isRange ? subMonths(baseDate.value, props.numberOfMonths - 1) : baseDate.value; return addMonths(monthOffset, offsetMonth); }); const calculateOffsetYear = computed(() => (offsetYear: number) => { const yearOffset = props?.showEndDateOnOpen ? subYears(baseDate.value, props.numberOfMonths - 1) : baseDate.value; return addYears(yearOffset, offsetYear); }); /** * By default shows the following month * Can skip multiple months by passing an additional param * Used for keyboard nav to be able to skip a year */ const calculateNextMonths = (amountOfMonths = 1) => { baseDate.value = addMonths(baseDate.value, amountOfMonths); }; /** * By default shows the previous month * Can skip multiple months by passing an additional param * Used for keyboard nav to be able to go back a year */ const calculatePreviousMonths = (amountOfMonths = 1) => { baseDate.value = subMonths(baseDate.value, amountOfMonths); }; return { // Refs calendarRef, // Values baseDate, calendarComponent, conditionalClasses, internalValue, // Methods calculateOffsetMonth, calculateOffsetYear, calendarPosition, // Events onDateInput, onMouseOver, calculateNextMonths, calculatePreviousMonths, }; }, }); </script> <style lang="scss" scoped src="./calendar.scss" />
1
+ <template> <div class="b-calendar" :class="conditionalClasses"> <component :is="calendarComponent" v-for="(_, index) in numberOfMonths" :ref="numberOfMonths === 1 ? 'calendarRef' : null" :key="`calendar-${index}`" :first-day-of-week="firstDayOfWeek" :is-date-disabled="isDateDisabled" :month="calculateOffsetMonth(index)" :year="calculateOffsetYear(index)" :position="calendarPosition(index)" :min="min" :max="max" :is-range="isRange" :start-date="internalValue.startDate" :end-date="internalValue.endDate" :granularity="granularity" @input="onDateInput" @mouseover="onMouseOver" @changeDay="onMouseOver" @nextMonth="calculateNextMonths" @previousMonth="calculatePreviousMonths" /> </div> </template> <script lang="ts"> import { computed, defineComponent, type PropType, ref, toRef, watch } from 'vue'; import CalendarMonth from './components/calendar-month/calendar-month.vue'; import CalendarYear from './components/calendar-year/calendar-year.vue'; import { CalendarEvent, CalendarFirstDayOfWeek, type CalendarGranularityConfig, type CalendarIsDateDisabled, CalendarPosition, type CalendarRangeDateValue, type CalendarValue, CalendarVariant, } from './calendar.types'; import { useSinglePane } from './composables/use-single-pane'; import { useRangePane } from './composables/use-range-pane'; import { addMonths } from 'date-fns/addMonths'; import { subMonths } from 'date-fns/subMonths'; import { addYears } from 'date-fns/addYears'; import { subYears } from 'date-fns/subYears'; /** * Handles the calendar logic to change visible months for Single calendar * and Range calendar, as well as the selection logic for range. * All the processing necessary for the calendar to select dates * * @example * import { Calendar } from '@/internal/bento-vue2'; * * export default { * components: { Calendar }, * template: ` * <calendar * :value="value" * :first-day-of-week="CalendarFirstDayOfWeek.Monday" * :is-date-disabled="(day: Date) => boolean" * :number-of-months="2" * is-range * @input="(selectedDate) => void" * @start-date-selected="(startDate: Date) => void" * @end-date-selected="(endDate: Date) => void" * /> * ` * } */ export default defineComponent({ name: 'calendar', components: { CalendarMonth, CalendarYear }, props: { /** * Allows you to set the first day of the week. 0 is sunday, 1 is monday, 6 is saturday */ firstDayOfWeek: { type: Number as PropType<CalendarFirstDayOfWeek>, default: CalendarFirstDayOfWeek.MONDAY, validator: (value: CalendarFirstDayOfWeek) => Object.values(CalendarFirstDayOfWeek).includes(value), }, /** * The selected granularity configuration */ granularity: { type: Object as PropType<CalendarGranularityConfig>, default: null, }, /** * Indicate if a date should be disabled or not */ isDateDisabled: { type: Function as PropType<CalendarIsDateDisabled>, default: undefined }, /** * Enables the Date Range calendar */ isRange: { type: Boolean, default: false }, /** * Set a maximum number of dates to be selectable by the range. */ maxRange: { type: Number, default: null, validator: (value: number) => !value || value > 0 }, /** * Sets a minimum limit for selectable dates - dates before `min` will not be selectable */ min: { type: Date, default: null }, /** * Sets a maximum limit for selectable dates - dates after `max` will not be selectable */ max: { type: Date, default: null }, /** * Number of months rendered on pane */ numberOfMonths: { type: Number, default: 3, validator: (n: number) => n >= 1, }, /** * Upon opening date range picker, end date's month will be pre-selected. */ showEndDateOnOpen: { type: Boolean, default: false }, /** * Selected date. Can be a single Date or a CalendarRangeDate * instance that contains startDate and endDate attributes. */ value: { type: [Date, Object] as PropType<CalendarValue>, default: undefined }, /** * Default start date for the calendar pane */ defaultDisplayMonth: { type: String, default: null, validator: (value: string) => { // Checks for YYYY-MM-DD format const dateRegex = /^\d{4}-(0?[1-9]|1[012])(-(0?[1-9]|[12][0-9]|3[01]))?$/; const isValidDate = dateRegex.test(value); return isValidDate; }, }, /** * The type of calendar to display. Defaults to showing days. */ variant: { type: String as PropType<`${CalendarVariant}`>, default: 'day', validator: (value: string) => Object.values(CalendarVariant).includes(value as CalendarVariant), }, }, emit: [CalendarEvent.INPUT, CalendarEvent.START_DATE_SELECTED, CalendarEvent.END_DATE_SELECTED], setup(props, { emit }) { // Single date calendar ref const calendarRef = ref(null); // Use a duplicate of "month" to move back and // forth with the arrows. Maintained in sync with the prop "value" const baseDate = ref(); watch( () => props.value, () => { if (props.isRange) { baseDate.value = props?.showEndDateOnOpen && (props.value as CalendarRangeDateValue)?.endDate ? (props.value as CalendarRangeDateValue)?.endDate : (props.value as CalendarRangeDateValue)?.startDate || new Date(); } else { const defaultDate = props.defaultDisplayMonth && new Date(props.defaultDisplayMonth).getTime() ? new Date(props.defaultDisplayMonth) : new Date(); baseDate.value = (props.value as Date) || defaultDate; } }, { immediate: true, deep: true } ); const calendarComponent = computed(() => props.variant === CalendarVariant.MONTH ? CalendarYear : CalendarMonth ); const conditionalClasses = computed(() => ({ 'b-calendar__range': props.isRange, })); const { internalValue, onDateInput, onMouseOver } = props.isRange ? useRangePane( { selectedDate: toRef(props, 'value'), isDateDisabled: props.isDateDisabled as (date: Date) => boolean, maxRange: toRef(props, 'maxRange'), granularity: toRef(props, 'granularity'), firstDayOfWeek: props.firstDayOfWeek, min: toRef(props, 'min'), max: toRef(props, 'max'), }, emit ) : useSinglePane( { selectedDate: toRef(props, 'value'), firstDayOfWeek: props.firstDayOfWeek, }, emit ); /** * Calculate the current month calendar. * This is used to render/hide the Previous & Next buttons on each calendar. * @param calendarIndex - Number to offset the calendar position */ const calendarPosition = (calendarIndex: number): CalendarPosition => { if (props.numberOfMonths === 1) { return CalendarPosition.UNIQUE; } if (calendarIndex === 0) { return CalendarPosition.FIRST; } return calendarIndex === props.numberOfMonths - 1 ? CalendarPosition.LAST : CalendarPosition.MIDDLE; }; /** * Calculates in relation to the amount of months defined in "numberOfMonths" * which calendar month should be render starting from the first (leftmost) * rendered calendar. * If showEndDateOnOpen is set to true, the last pane should be the baseDate month i.e. the end date * @param offsetMonth - Amount of months to add after the first month was rendered */ const calculateOffsetMonth = computed(() => (offsetMonth: number) => { const monthOffset = props?.showEndDateOnOpen && props.isRange ? subMonths(baseDate.value, props.numberOfMonths - 1) : baseDate.value; return addMonths(monthOffset, offsetMonth); }); const calculateOffsetYear = computed(() => (offsetYear: number) => { const yearOffset = props?.showEndDateOnOpen ? subYears(baseDate.value, props.numberOfMonths - 1) : baseDate.value; return addYears(yearOffset, offsetYear); }); /** * By default shows the following month * Can skip multiple months by passing an additional param * Used for keyboard nav to be able to skip a year */ const calculateNextMonths = (amountOfMonths = 1) => { baseDate.value = addMonths(baseDate.value, amountOfMonths); }; /** * By default shows the previous month * Can skip multiple months by passing an additional param * Used for keyboard nav to be able to go back a year */ const calculatePreviousMonths = (amountOfMonths = 1) => { baseDate.value = subMonths(baseDate.value, amountOfMonths); }; return { // Refs calendarRef, // Values baseDate, calendarComponent, conditionalClasses, internalValue, // Methods calculateOffsetMonth, calculateOffsetYear, calendarPosition, // Events onDateInput, onMouseOver, calculateNextMonths, calculatePreviousMonths, }; }, }); </script> <style lang="scss" scoped src="./calendar.scss" />
@@ -1 +1 @@
1
- import { type Meta, type StoryObj } from '@storybook/vue'; import { action } from '@storybook/addon-actions'; import { computed, ref } from 'vue'; import { CalendarMonthFirstDayOfWeek } from './calendar-month.types'; import CalendarMonth from './calendar-month.vue'; import { isVue2 } from 'vue-demi'; const meta: Meta = { title: 'Calendar/Month', component: CalendarMonth, argTypes: { // Props month: { name: 'month', defaultValue: undefined, description: 'Month of the year to be displayed in the calendar', control: 'date', }, value: { name: 'value', defaultValue: undefined, description: 'Date selected', control: { disable: true, }, }, // Funcs isDateDisabled: { name: 'isDateDisabled', defaultValue: '', description: 'Disables the dates that meet the criteria defined in the function.', table: { type: { summary: '(currentDate: Date) => boolean' } }, control: { disable: true }, }, // Enum props firstDayOfWeek: { options: Object.keys(CalendarMonthFirstDayOfWeek) .map(key => CalendarMonthFirstDayOfWeek[key]) .filter(value => typeof value !== 'string') as Array<number>, control: { type: 'select', }, }, // Events input: { name: 'input', defaultValue: '', description: 'Triggered when a date is selected', table: { type: { summary: '(selectedDate: Date) => void' } }, control: { disable: true }, }, }, }; export default meta; type Story = StoryObj<typeof CalendarMonth>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { CalendarMonth }, props: Object.keys(argTypes), template: ` <calendar-month v-bind="args" /> `, setup(props) { const currentMonth = computed(() => new Date(props.month)); const currentValue = ref(new Date(props.value)); const onInputEvent = value => { action('input')(value); currentValue.value = value; }; return { args: isVue2 ? props : _args, // Values currentMonth, currentValue, // Events onInputEvent, }; }, }), args: { isRange: true, month: new Date(), startDate: (() => { const date = new Date(); date.setDate(date.getDate() - 6); return date; })(), endDate: (() => { const date = new Date(); date.setDate(date.getDate()); return date; })(), }, };
1
+ import { type Meta, type StoryObj } from '@storybook/vue'; import { action } from '@storybook/addon-actions'; import { computed, ref } from 'vue'; import { CalendarFirstDayOfWeek } from '../../calendar.types'; import { isVue2 } from 'vue-demi'; import CalendarMonth from './calendar-month.vue'; const meta: Meta = { title: 'Calendar/Month', component: CalendarMonth, argTypes: { // Props month: { name: 'month', defaultValue: undefined, description: 'Month of the year to be displayed in the calendar', control: 'date', }, value: { name: 'value', defaultValue: undefined, description: 'Date selected', control: { disable: true, }, }, // Funcs isDateDisabled: { name: 'isDateDisabled', defaultValue: '', description: 'Disables the dates that meet the criteria defined in the function.', table: { type: { summary: '(currentDate: Date) => boolean' } }, control: { disable: true }, }, // Enum props firstDayOfWeek: { options: Object.keys(CalendarFirstDayOfWeek) .map(key => CalendarFirstDayOfWeek[key]) .filter(value => typeof value !== 'string') as Array<number>, control: { type: 'select', }, }, // Events input: { name: 'input', defaultValue: '', description: 'Triggered when a date is selected', table: { type: { summary: '(selectedDate: Date) => void' } }, control: { disable: true }, }, }, }; export default meta; type Story = StoryObj<typeof CalendarMonth>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { CalendarMonth }, props: Object.keys(argTypes), template: ` <calendar-month v-bind="args" /> `, setup(props) { const currentMonth = computed(() => new Date(props.month)); const currentValue = ref(new Date(props.value)); const onInputEvent = value => { action('input')(value); currentValue.value = value; }; return { args: isVue2 ? props : _args, // Values currentMonth, currentValue, // Events onInputEvent, }; }, }), args: { isRange: true, month: new Date(), startDate: (() => { const date = new Date(); date.setDate(date.getDate() - 6); return date; })(), endDate: (() => { const date = new Date(); date.setDate(date.getDate()); return date; })(), }, };
@@ -1 +1 @@
1
- export enum CalendarMonthEvent { INPUT = 'input', MOUSE_OVER = 'mouseover', CHANGE_DAY = 'changeDay', NEXT_MONTH = 'nextMonth', PREVIOUS_MONTH = 'previousMonth', } export enum CalendarMonthFirstDayOfWeek { MONDAY = 1, SATURDAY = 6, SUNDAY = 0, } export type CalendarMonthIsDateDisabled = (date: Date) => boolean;
1
+ import { type CalendarFirstDayOfWeek, type CalendarIsDateDisabled, type CalendarPosition } from '../../calendar.types'; export interface CalendarMonthProps { /** * end of the selected range * @default null */ endDate?: Date; /** * Allows you to set the first day of the week. 0 is sunday, 1 is monday, 6 is saturday * @default CalendarMonthFirstDayOfWeek.MONDAY, */ firstDayOfWeek?: CalendarFirstDayOfWeek; /** * Indicate if a date should be disabled or not * @default undefined */ isDateDisabled?: CalendarIsDateDisabled; /** * Indicates if the calendar is part of a range date picker or a sigle date picker * @default false */ isRange?: boolean; /** * Sets a maximum limit for selectable dates - dates after `max` will not be selectable * @default null */ max?: Date; /** * Sets a minimum limit for selectable dates - dates before `min` will not be selectable * @default null */ min?: Date; /** * Month of the year to be displayed in the calendar */ month: Date; /** * When rendering multiple calendars, this property allows to render the * Next and Previous calendar buttons to the right, left or not at all * depending on the position: Unique (only 1), First, Middle or Last * @default 'unique' */ position?: `${CalendarPosition}`; /** * start of the selected range * @default undefined */ startDate?: Date; }
@@ -1 +1 @@
1
- <template> <div v-bento-keyboard-navigation-directive class="b-calendar-month" v-on="listeners"> <div class="b-calendar-month__month-selector"> <!-- go to previous month --> <div class="b-calendar-month__previous"> <bento-button v-if="shouldShowPreviousMonthButton" variant="tertiary" @click="moveToPreviousMonth($event, true)" > <template #iconLeft> <chevron-left-icon :svg-title="t('previousMonth')" /> </template> </bento-button> </div> <!-- month's name --> <bento-typography :id="monthLabelId" el="h2" variant="title" aria-live="polite"> {{ formattedMonthDate }} </bento-typography> <!-- go to next month --> <div class="b-calendar-month__next"> <bento-button v-if="shouldShowNextMonthButton" variant="tertiary" @click="moveToNextMonth($event, true)" > <template #iconLeft> <chevron-right-icon :svg-title="t('nextMonth')" /> </template> </bento-button> </div> </div> <table class="b-calendar-month__month-container" role="grid" :aria-labelledby="monthLabelId"> <thead> <tr class="b-calendar-month__week-header"> <th v-for="(dayOfWeekFormatted, index) in daysOfWeek" :key="index" class="b-calendar-month__day-of-week" scope="col" > <bento-typography class="b-calendar-month__day-of-week-font" el="span" variant="caption"> {{ dayOfWeekFormatted }} </bento-typography> </th> </tr> </thead> <tbody> <tr v-for="(week, index) in weeksOfMonth" :key="index"> <td v-if="index === 0 && week.length < 7" :colspan="7 - week.length"> </td> <calendar-month-day v-for="(day, dayIndex) in week" :ref="addDayRef(day)" :key="`calendar-day-${dayIndex}`" :day="day" :disabled="computedIsDateDisabled(day)" :selected="isSelectedDate(day)" :variant="dayVariant(day)" @click="onDayClicked" @mouseover="$emit(CalendarMonthEvent.MOUSE_OVER, day)" @keyup.up.native="$emit(CalendarMonthEvent.CHANGE_DAY, day)" @keyup.down.native="$emit(CalendarMonthEvent.CHANGE_DAY, day)" @keyup.left.native="$emit(CalendarMonthEvent.CHANGE_DAY, day)" @keyup.right.native="$emit(CalendarMonthEvent.CHANGE_DAY, day)" /> </tr> <template v-if="weeksOfMonth?.length < 6"> <tr v-for="n in 6 - weeksOfMonth?.length" :key="`calendar month filler row ${n}`"> <td aria-hidden="true" class="b-calendar-month__empty-cell-filler" :colspan="7"></td> </tr> </template> </tbody> </table> </div> </template> <script lang="ts"> import { computed, defineComponent, type PropType, toRef } from 'vue'; import BentoButton from '@/components/button/button.vue'; import { CalendarMonthDay, CalendarMonthDayVariant } from './components/calendar-month-day'; import { BentoTypography } from '@/components/typography'; import { useI18n } from '@/utils/ts/i18n'; import { CalendarMonthEvent, CalendarMonthFirstDayOfWeek, type CalendarMonthIsDateDisabled, } from './calendar-month.types'; import { CalendarPosition } from '@/internal/calendar/calendar.types'; import { generateUid } from '@/core/utils/ts'; import { BentoKeyboardNavigationDirective } from '@/directives'; import { useMoveCalendarDate } from './composables/move-calendar-date'; import ChevronLeftIcon from '@adyen/ui-assets-icons-16/vue/chevron-left'; import ChevronRightIcon from '@adyen/ui-assets-icons-16/vue/chevron-right'; import { setDay } from 'date-fns/setDay'; import { startOfMonth } from 'date-fns/startOfMonth'; import { startOfDay } from 'date-fns/startOfDay'; import { isSameDay } from 'date-fns/isSameDay'; import { isWithinInterval } from 'date-fns/isWithinInterval'; import { getDateTimeFormatter } from '@/utils/ts/format-date'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; /** * CalendarMonth that displays all the days of a given month and allows * to move back and forth the visible month with the Next and Previous buttons * * @example * import { CalendarMonthMonth, CalendarMonthFirstDayOfWeek, CalendarPosition } from '@/internal/calendar'; * * export default { * components: { CalendarMonthMonth }, * template: ` * <calendar * :first-day-of-week="CalendarMonthFirstDayOfWeek.SUNDAY" * :is-date-disabled="(date: Date) => boolean" * :month="new Date()" * :position="CalendarPosition.LAST" * is-range * :start-date="new Date()" * :end-date="new Date()" * @input="(selectedDate: Date) => void" * @mouseover="(event: Event) => void" * @nextMonth="() => void" * @previousMonth="() => void" * /> * ` * } */ export default defineComponent({ i18n: { messages }, name: 'calendar-month', components: { BentoButton, BentoTypography, CalendarMonthDay, ChevronLeftIcon, ChevronRightIcon, }, directives: { BentoKeyboardNavigationDirective }, props: { /** * end of the selected range */ endDate: { type: Date, default: null }, /** * Allows you to set the first day of the week. 0 is sunday, 1 is monday, 6 is saturday */ firstDayOfWeek: { type: Number as PropType<CalendarMonthFirstDayOfWeek>, default: CalendarMonthFirstDayOfWeek.MONDAY, validator: (value: CalendarMonthFirstDayOfWeek) => Object.values(CalendarMonthFirstDayOfWeek).includes(value), }, /** * Indicate if a date should be disabled or not */ isDateDisabled: { type: Function as PropType<CalendarMonthIsDateDisabled>, default: undefined }, /** * Indicates if the calendar is part of a range date picker or a sigle date picker */ isRange: { type: Boolean, default: false }, /** * Sets a minimum limit for selectable dates - dates before `min` will not be selectable */ min: { type: Date, default: null }, /** * Sets a maximum limit for selectable dates - dates after `max` will not be selectable */ max: { type: Date, default: null }, /** * Month of the year to be displayed in the calendar */ month: { type: Date, required: true }, /** * When rendering multiple calendars, this property allows to render the * Next and Previous calendar buttons to the right, left or not at all * depending on the position: Unique (only 1), First, Middle or Last */ position: { type: String as PropType<`${CalendarPosition}`>, default: 'unique', validator: (value: CalendarPosition) => Object.values(CalendarPosition).includes(value), }, /** * start of the selected range */ startDate: { type: Date, default: null }, // can be undefined, eslint complains without default }, emits: [ CalendarMonthEvent.CHANGE_DAY, CalendarMonthEvent.INPUT, CalendarMonthEvent.NEXT_MONTH, CalendarMonthEvent.PREVIOUS_MONTH, CalendarMonthEvent.MOUSE_OVER, ], setup(props, { emit }) { // Date formatter locale options const { locale, t } = useI18n<{ message: MessageSchema }>({ messages }); const monthLabelId = generateUid('bento-date-picker-month-label'); const computedIsDateDisabled = computed( () => (date: Date) => (props.min && date < props.min) || (props.max && date > props.max) || props.isDateDisabled?.(date) ); const { internalDate, baseDate, // Calendar refs addDayRef, // Month navigation showNextMonth, showPreviousMonth, // Day navigation moveToNextDay, moveToPreviousDay, moveToPreviousWeek, moveToNextWeek, moveToFirstDayOfWeek, moveToLastDayOfWeek, moveToPreviousMonth, moveToPreviousYear, moveToNextMonth, moveToNextYear, focusOnDate, } = useMoveCalendarDate( { currentSelectedDate: toRef(props, 'startDate'), month: toRef(props, 'month'), isDateDisabled: computedIsDateDisabled.value, firstDayOfWeek: props.firstDayOfWeek, }, emit ); /** * Default to Monday if the value passed in is not part of the defined Enum */ const fallbackFirstDayOfWeek = computed(() => Object.values(CalendarMonthFirstDayOfWeek).includes(props.firstDayOfWeek) ? props.firstDayOfWeek : CalendarMonthFirstDayOfWeek.MONDAY ); /** * Renders the previous month button if it's Unique or First calendar */ const shouldShowPreviousMonthButton = computed(() => ['first', 'unique'].includes(props.position)); /** * Renders the next month button if it's Unique or Last calendar */ const shouldShowNextMonthButton = computed(() => ['last', 'unique'].includes(props.position)); /** * Formats the month date to the name of the month and the year */ const formattedMonthDate = computed(() => getDateTimeFormatter(locale.value, { month: 'long', year: 'numeric' }).format(baseDate.value) ); /** * Formats all the days of the week to their day name */ const daysOfWeek = computed(() => new Array(7).fill(new Date()).map((date, index) => { const dayOfWeekDate = setDay(date, fallbackFirstDayOfWeek.value + index); return getDateTimeFormatter(locale.value, { weekday: 'short' }).format(dayOfWeekDate); }) ); /** * Calculates all the weeks of the current "props.month" * and adds the respective day number to each column and week */ const weeksOfMonth = computed(() => { const weeks = []; const date = startOfMonth(props.month); let week = weeks[weeks.push([]) - 1]; while (date.getMonth() === props.month.getMonth()) { if (date.getDay() - fallbackFirstDayOfWeek.value === 0 && week.length > 0) { week = weeks[weeks.push([]) - 1]; } week.push(startOfDay(date)); date.setDate(date.getDate() + 1); } return weeks; }); const onDayClicked = (selectedDate: Date) => { emit(CalendarMonthEvent.INPUT, selectedDate); }; /** * Calculates the variation of the days selected. * For single select it's always SINGLE * For ranges: * - Start & End is the same: SINGLE * - Start: BEGINNING * - End: END * - Within the range: MIDDLE * @param {Date} day Current day */ // eslint-disable-next-line consistent-return const dayVariant = (day: Date) => { if (!props.isRange || isSameDay(props.startDate, props.endDate)) { return CalendarMonthDayVariant.SINGLE; } if (props.startDate && isSameDay(props.startDate, day)) { return CalendarMonthDayVariant.BEGINNING; } if (props.endDate && isSameDay(props.endDate, day)) { return CalendarMonthDayVariant.END; } if ( props.startDate && props.endDate && isWithinInterval(day, { start: props.startDate, end: props.endDate }) ) { return CalendarMonthDayVariant.MIDDLE; } }; /** * Checks every date in the calendar to see if it's selected. * @param day Current day in the calendar */ const isSelectedDate = (day: Date) => { // Single selection if (internalDate.value && isSameDay(internalDate.value, day) && !props.endDate) { return true; } // Range return ( props.startDate && props.endDate && isWithinInterval(day, { start: props.startDate, end: props.endDate }) ); }; const confirmNavigationDate = () => { emit(CalendarMonthEvent.INPUT, internalDate); }; const listeners = { 'keydown:arrow-right': moveToNextDay, 'keydown:arrow-left': moveToPreviousDay, 'keydown:arrow-up': moveToPreviousWeek, 'keydown:arrow-down': moveToNextWeek, 'keydown:home': moveToFirstDayOfWeek, 'keydown:end': moveToLastDayOfWeek, 'keydown:page-up': moveToPreviousMonth, 'keydown:shift-page-up': moveToPreviousYear, 'keydown:page-down': moveToNextMonth, 'keydown:shift-page-down': moveToNextYear, }; return { // Refs addDayRef, // Values computedIsDateDisabled, daysOfWeek, weeksOfMonth, formattedMonthDate, shouldShowNextMonthButton, shouldShowPreviousMonthButton, monthLabelId, // Enums CalendarMonthEvent, // Functions isSelectedDate, showNextMonth, showPreviousMonth, dayVariant, confirmNavigationDate, // Events onDayClicked, listeners, // Exposed methods focusOnDate, moveToNextDay, moveToPreviousDay, moveToPreviousWeek, moveToNextWeek, moveToFirstDayOfWeek, moveToLastDayOfWeek, moveToPreviousMonth, moveToPreviousYear, moveToNextMonth, moveToNextYear, // Translations t, }; }, }); </script> <style lang="scss" scoped src="./calendar-month.scss" />
1
+ <template> <div v-bento-keyboard-navigation-directive class="b-calendar-month" v-on="listeners"> <div class="b-calendar-month__month-selector"> <!-- go to previous month --> <div class="b-calendar-month__previous"> <bento-button v-if="shouldShowPreviousMonthButton" variant="tertiary" @click="moveToPreviousMonth($event, true)" > <template #iconLeft> <chevron-left-icon :svg-title="t('previousMonth')" /> </template> </bento-button> </div> <!-- month's name --> <bento-typography :id="monthLabelId" el="h2" variant="title" aria-live="polite"> {{ formattedMonthDate }} </bento-typography> <!-- go to next month --> <div class="b-calendar-month__next"> <bento-button v-if="shouldShowNextMonthButton" variant="tertiary" @click="moveToNextMonth($event, true)" > <template #iconLeft> <chevron-right-icon :svg-title="t('nextMonth')" /> </template> </bento-button> </div> </div> <table class="b-calendar-month__month-container" role="grid" :aria-labelledby="monthLabelId"> <thead> <tr class="b-calendar-month__week-header"> <th v-for="(dayOfWeekFormatted, index) in daysOfWeek" :key="index" class="b-calendar-month__day-of-week" scope="col" > <bento-typography class="b-calendar-month__day-of-week-font" el="span" variant="caption"> {{ dayOfWeekFormatted }} </bento-typography> </th> </tr> </thead> <tbody> <tr v-for="(week, index) in weeksOfMonth" :key="index"> <td v-if="index === 0 && week.length < 7" :colspan="7 - week.length"> </td> <calendar-month-day v-for="(day, dayIndex) in week" :ref="addDayRef(day)" :key="`calendar-day-${dayIndex}`" :day="day" :disabled="computedIsDateDisabled(day)" :selected="isSelectedDate(day)" :variant="dayVariant(day)" @click="onDayClicked" @mouseover="$emit('mouseover', day)" @keyup.up.native="$emit('changeDay', day)" @keyup.down.native="$emit('changeDay', day)" @keyup.left.native="$emit('changeDay', day)" @keyup.right.native="$emit('changeDay', day)" /> </tr> <template v-if="weeksOfMonth?.length < 6"> <tr v-for="n in 6 - weeksOfMonth?.length" :key="`calendar month filler row ${n}`"> <td aria-hidden="true" class="b-calendar-month__empty-cell-filler" :colspan="7"></td> </tr> </template> </tbody> </table> </div> </template> <script setup lang="ts"> import { computed, toRef } from 'vue'; import BentoButton from '@/components/button/button.vue'; import { CalendarMonthDay, CalendarMonthDayVariant } from './components/calendar-month-day'; import { BentoTypography } from '@/components/typography'; import { useI18n } from '@/utils/ts/i18n'; import type { CalendarMonthProps } from './calendar-month.types'; import { generateUid } from '@/core/utils/ts'; import { BentoKeyboardNavigationDirective as vBentoKeyboardNavigationDirective } from '@/directives'; import { type CalendarEmit, CalendarFirstDayOfWeek } from '../../calendar.types'; import { useMoveCalendarDate } from './composables/move-calendar-date'; import { setDay } from 'date-fns/setDay'; import { startOfMonth } from 'date-fns/startOfMonth'; import { startOfDay } from 'date-fns/startOfDay'; import { isSameDay } from 'date-fns/isSameDay'; import { isWithinInterval } from 'date-fns/isWithinInterval'; import { getDateTimeFormatter } from '@/utils/ts/format-date'; import ChevronLeftIcon from '@adyen/ui-assets-icons-16/vue/chevron-left'; import ChevronRightIcon from '@adyen/ui-assets-icons-16/vue/chevron-right'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const emit = defineEmits<CalendarEmit>(); const props = withDefaults(defineProps<CalendarMonthProps>(), { endDate: null, firstDayOfWeek: CalendarFirstDayOfWeek.MONDAY, isDateDisabled: undefined, isRange: false, max: null, min: null, position: 'unique', startDate: null, // can be undefined, eslint complains without default }); // Date formatter locale options const { locale, t } = useI18n<{ message: MessageSchema }>({ messages }); const monthLabelId = generateUid('bento-date-picker-month-label'); const computedIsDateDisabled = computed( () => (date: Date) => (props.min && date < props.min) || (props.max && date > props.max) || props.isDateDisabled?.(date) ); const { internalDate, baseDate, // Calendar refs addDayRef, // Day navigation moveToNextDay, moveToPreviousDay, moveToPreviousWeek, moveToNextWeek, moveToFirstDayOfWeek, moveToLastDayOfWeek, moveToPreviousMonth, moveToPreviousYear, moveToNextMonth, moveToNextYear, focusOnDate, } = useMoveCalendarDate( { currentSelectedDate: toRef(props, 'startDate'), month: toRef(props, 'month'), isDateDisabled: computedIsDateDisabled.value, firstDayOfWeek: props.firstDayOfWeek, }, emit ); /** * Default to Monday if the value passed in is not part of the defined Enum */ const fallbackFirstDayOfWeek = computed(() => Object.values(CalendarFirstDayOfWeek).includes(props.firstDayOfWeek) ? props.firstDayOfWeek : CalendarFirstDayOfWeek.MONDAY ); /** * Renders the previous month button if it's Unique or First calendar */ const shouldShowPreviousMonthButton = computed(() => ['first', 'unique'].includes(props.position)); /** * Renders the next month button if it's Unique or Last calendar */ const shouldShowNextMonthButton = computed(() => ['last', 'unique'].includes(props.position)); /** * Formats the month date to the name of the month and the year */ const formattedMonthDate = computed(() => getDateTimeFormatter(locale.value, { month: 'long', year: 'numeric' }).format(baseDate.value) ); /** * Formats all the days of the week to their day name */ const daysOfWeek = computed(() => new Array(7).fill(new Date()).map((date, index) => { const dayOfWeekDate = setDay(date, fallbackFirstDayOfWeek.value + index); return getDateTimeFormatter(locale.value, { weekday: 'short' }).format(dayOfWeekDate); }) ); /** * Calculates all the weeks of the current "props.month" * and adds the respective day number to each column and week */ const weeksOfMonth = computed(() => { const weeks = []; const date = startOfMonth(props.month); let week = weeks[weeks.push([]) - 1]; while (date.getMonth() === props.month.getMonth()) { if (date.getDay() - fallbackFirstDayOfWeek.value === 0 && week.length > 0) { week = weeks[weeks.push([]) - 1]; } week.push(startOfDay(date)); date.setDate(date.getDate() + 1); } return weeks; }); const onDayClicked = (selectedDate: Date) => { emit('input', selectedDate); }; /** * Calculates the variation of the days selected. * For single select it's always SINGLE * For ranges: * - Start & End is the same: SINGLE * - Start: BEGINNING * - End: END * - Within the range: MIDDLE * @param day Current day */ const dayVariant = (day: Date) => { if (!props.isRange || isSameDay(props.startDate, props.endDate)) { return CalendarMonthDayVariant.SINGLE; } if (props.startDate && isSameDay(props.startDate, day)) { return CalendarMonthDayVariant.BEGINNING; } if (props.endDate && isSameDay(props.endDate, day)) { return CalendarMonthDayVariant.END; } if (props.startDate && props.endDate && isWithinInterval(day, { start: props.startDate, end: props.endDate })) { return CalendarMonthDayVariant.MIDDLE; } return undefined; }; /** * Checks every date in the calendar to see if it's selected. * @param day Current day in the calendar */ const isSelectedDate = (day: Date) => { // Single selection if (internalDate.value && isSameDay(internalDate.value, day) && !props.endDate) { return true; } // Range return ( props.startDate && props.endDate && isWithinInterval(day, { start: props.startDate, end: props.endDate }) ); }; const listeners = { 'keydown:arrow-right': moveToNextDay, 'keydown:arrow-left': moveToPreviousDay, 'keydown:arrow-up': moveToPreviousWeek, 'keydown:arrow-down': moveToNextWeek, 'keydown:home': moveToFirstDayOfWeek, 'keydown:end': moveToLastDayOfWeek, 'keydown:page-up': moveToPreviousMonth, 'keydown:shift-page-up': moveToPreviousYear, 'keydown:page-down': moveToNextMonth, 'keydown:shift-page-down': moveToNextYear, }; defineExpose({ focusOnDate, }); </script> <script lang="ts"> /** * CalendarMonth that displays all the days of a given month and allows * to move back and forth the visible month with the Next and Previous buttons * * @example * import { CalendarMonthMonth, CalendarMonthFirstDayOfWeek, CalendarPosition } from '@/internal/calendar'; * * export default { * components: { CalendarMonthMonth }, * template: ` * <calendar * :first-day-of-week="CalendarMonthFirstDayOfWeek.SUNDAY" * :is-date-disabled="(date: Date) => boolean" * :month="new Date()" * position="last" * is-range * :start-date="new Date()" * :end-date="new Date()" * @input="(selectedDate: Date) => void" * @mouseover="(event: Event) => void" * @nextMonth="() => void" * @previousMonth="() => void" * /> * ` * } */ export default { i18n: { messages }, name: 'calendar-month', }; </script> <style lang="scss" scoped src="./calendar-month.scss" />
@@ -1 +1 @@
1
- import type { CalendarGranularityConfig } from '../../../calendar.types'; import type { CalendarMonthFirstDayOfWeek } from '../calendar-month.types'; export interface UseMinMaxDateProps { firstDayOfWeek: CalendarMonthFirstDayOfWeek; min: Date; max: Date; } export type UseGranularMinMaxDate = (props: UseMinMaxDateProps) => { getGranularCalendarLimits: (granularity: CalendarGranularityConfig) => { min: Date; max: Date }; };
1
+ import type { CalendarFirstDayOfWeek, CalendarGranularityConfig } from '../../../calendar.types'; export interface UseMinMaxDateProps { firstDayOfWeek: CalendarFirstDayOfWeek; min: Date; max: Date; } export type UseGranularMinMaxDate = (props: UseMinMaxDateProps) => { getGranularCalendarLimits: (granularity: CalendarGranularityConfig) => { min: Date; max: Date }; };
@@ -1 +1 @@
1
- export enum CalendarYearPosition { FIRST = 'first', LAST = 'last', MIDDLE = 'middle', UNIQUE = 'unique', } /** * Private types */ export interface CalendarYearProps { /** * end of the selected range */ endDate?: Date; /** * Indicate if a date should be disabled or not */ isDateDisabled?: (date: Date) => boolean; /** * Indicates if the calendar is part of a range date picker or a single date picker */ isRange?: boolean; /** * Sets a minimum limit for selectable dates - dates before `min` will not be selectable */ min?: Date; /** * Sets a maximum limit for selectable dates - dates after `max` will not be selectable */ max?: Date; /** * When rendering multiple calendars, this property allows to render the * Next and Previous calendar buttons to the right, left or not at all * depending on the position: Unique (only 1), First, Middle or Last */ position?: `${CalendarYearPosition}`; /** * start of the selected range */ startDate?: Date; /** * Year to be displayed in the calendar */ year: Date; }
1
+ import { type CalendarIsDateDisabled, type CalendarPosition } from '../../calendar.types'; export interface CalendarYearProps { /** * end of the selected range */ endDate?: Date; /** * Indicate if a date should be disabled or not */ isDateDisabled?: CalendarIsDateDisabled; /** * Indicates if the calendar is part of a range date picker or a single date picker */ isRange?: boolean; /** * Sets a minimum limit for selectable dates - dates before `min` will not be selectable */ min?: Date; /** * Sets a maximum limit for selectable dates - dates after `max` will not be selectable */ max?: Date; /** * When rendering multiple calendars, this property allows to render the * Next and Previous calendar buttons to the right, left or not at all * depending on the position: Unique (only 1), First, Middle or Last */ position?: CalendarPosition; /** * start of the selected range */ startDate?: Date; /** * Year to be displayed in the calendar */ year: Date; }
@@ -1 +1 @@
1
- <template> <div v-bento-keyboard-navigation-directive class="b-calendar-year" v-on="listeners"> <div class="b-calendar-year__year-selector"> <!-- go to previous year --> <div class="b-calendar-year__previous"> <bento-button v-if="shouldShowPreviousYearButton" variant="tertiary" @click="moveToPreviousYear($event, true)" > <template #iconLeft> <chevron-left-icon :svg-title="t('previousYear')" /> </template> </bento-button> </div> <!-- year --> <bento-typography :id="yearLabelId" el="h2" variant="title" aria-live="polite"> {{ year.getFullYear() }} </bento-typography> <!-- go to next year --> <div class="b-calendar-year__next"> <bento-button v-if="shouldShowNextYearButton" variant="tertiary" @click="moveToNextYear($event, true)"> <template #iconLeft> <chevron-right-icon :svg-title="t('nextYear')" /> </template> </bento-button> </div> </div> <table class="b-calendar-year__year-container" role="grid" :aria-labelledby="yearLabelId"> <tbody> <tr v-for="(row, index) in monthRows" :key="index"> <calendar-year-month v-for="(month, monthIndex) in row" :ref="addDayRef(month)" :key="`calendar-month-${monthIndex}`" :month="month" :disabled="isDateDisabled(month)" :selected="isSelectedDate(month)" :variant="monthVariant(month)" @click="onMonthClicked" @mouseover="emit('mouseover', month)" /> </tr> </tbody> </table> </div> </template> <script setup lang="ts"> import { computed, toRef } from 'vue'; import { isSameMonth } from 'date-fns/isSameMonth'; import { isWithinInterval } from 'date-fns/isWithinInterval'; import BentoButton from '@/components/button/button.vue'; import { BentoTypography } from '@/components/typography'; import CalendarYearMonth from './components/calendar-year-month/calendar-year-month.vue'; import ChevronLeftIcon from '@adyen/ui-assets-icons-16/vue/chevron-left'; import ChevronRightIcon from '@adyen/ui-assets-icons-16/vue/chevron-right'; import { BentoKeyboardNavigationDirective as vBentoKeyboardNavigationDirective } from '@/directives'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { useMoveCalendarDate } from '@/internal/calendar/components/calendar-month/composables/move-calendar-date'; import { type CalendarYearProps } from './calendar-year.types'; import { CalendarYearMonthVariant } from './components/calendar-year-month/calendar-year-month.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const yearLabelId = generateUid('bento-date-picker-year-label'); const props = withDefaults(defineProps<CalendarYearProps>(), { endDate: null, isDateDisabled: undefined, isRange: false, min: null, max: null, position: 'unique', startDate: null, }); const emit = defineEmits<{ (e: 'input', d: Date); (e: 'mouseover', d: Date); (e: 'nextMonth' | 'previousMonth', a: number); }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const isDateDisabled = (date: Date) => (props.min && date < props.min) || (props.max && date > props.max) || props.isDateDisabled?.(date); /** * Renders the previous year button if it's Unique or First calendar */ const shouldShowPreviousYearButton = computed(() => ['first', 'unique'].includes(props.position)); /** * Renders the next year button if it's Unique or Last calendar */ const shouldShowNextYearButton = computed(() => ['last', 'unique'].includes(props.position)); const { internalDate, // Calendar refs addDayRef, // Day navigation moveToPreviousMonth, moveToPreviousYear, moveToNextMonth, moveToNextYear, moveToFirstMonthOfYear, moveToLastMonthOfYear, focusOnDate, } = useMoveCalendarDate( { currentSelectedDate: toRef(props, 'startDate'), month: toRef(props, 'year'), isDateDisabled, }, emit ); defineExpose({ focusOnDate }); /** * Calculates all the months of the current "props.month" * and adds the respective day number to each column and week */ const monthRows = computed(() => { const rows = []; let count = 0; for (let i = 0; i < 3; i++) { const innerArray = []; for (let j = 0; j < 4; j++) { innerArray.push(new Date(props.year.getFullYear(), count++, 1)); } rows.push(innerArray); } return rows; }); /** * Calculates the variation of the days selected. * For single select it's always SINGLE * For ranges: * - Start & End is the same: SINGLE * - Start: BEGINNING * - End: END * - Within the range: MIDDLE * @param {Date} day Current day */ const monthVariant = (day: Date) => { if (!props.isRange || isSameMonth(props.startDate, props.endDate)) { return CalendarYearMonthVariant.SINGLE; } if (props.startDate && isSameMonth(props.startDate, day)) { return CalendarYearMonthVariant.BEGINNING; } if (props.endDate && isSameMonth(props.endDate, day)) { return CalendarYearMonthVariant.END; } if (props.startDate && props.endDate && isWithinInterval(day, { start: props.startDate, end: props.endDate })) { return CalendarYearMonthVariant.MIDDLE; } return null; }; /** * Checks every date in the calendar to see if it's selected. * @param date Current date in the calendar */ const isSelectedDate = (date: Date) => { // Single selection if (internalDate.value && isSameMonth(internalDate.value, date)) { return true; } // Range return ( props.startDate && props.endDate && isWithinInterval(date, { start: props.startDate, end: props.endDate }) ); }; const onMonthClicked = (selectedDate: Date) => { emit('input', selectedDate); }; const listeners = !props.isRange ? { 'keydown:arrow-right': moveToNextMonth, 'keydown:arrow-left': moveToPreviousMonth, 'keydown:arrow-up': (ev: Event) => moveToPreviousMonth(ev, false, 4), 'keydown:arrow-down': (ev: Event) => moveToNextMonth(ev, false, 4), 'keydown:home': moveToFirstMonthOfYear, 'keydown:end': moveToLastMonthOfYear, 'keydown:page-up': moveToPreviousYear, 'keydown:page-down': moveToNextYear, } : {}; </script> <script lang="ts"> /** * CalendarYear that displays all the months of a given year and allows * to move back and forth the visible year with the Next and Previous buttons * * @example * import { CalendarYear } from '@/internal/calendar'; * * export default { * components: { CalendarYear }, * template: ` * <calendar-year * :is-date-disabled="(date: Date) => boolean" * :year="new Date()" * is-range * :start-date="new Date()" * :end-date="new Date()" * @input="(selectedDate: Date) => void" * @mouseover="(event: Event) => void" * @nextMonth="() => void" * @previousMonth="() => void" * /> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./calendar-year.scss" />
1
+ <template> <div v-bento-keyboard-navigation-directive class="b-calendar-year" v-on="listeners"> <div class="b-calendar-year__year-selector"> <!-- go to previous year --> <div class="b-calendar-year__previous"> <bento-button v-if="shouldShowPreviousYearButton" variant="tertiary" @click="moveToPreviousYear($event, true)" > <template #iconLeft> <chevron-left-icon :svg-title="t('previousYear')" /> </template> </bento-button> </div> <!-- year --> <bento-typography :id="yearLabelId" el="h2" variant="title" aria-live="polite"> {{ year.getFullYear() }} </bento-typography> <!-- go to next year --> <div class="b-calendar-year__next"> <bento-button v-if="shouldShowNextYearButton" variant="tertiary" @click="moveToNextYear($event, true)"> <template #iconLeft> <chevron-right-icon :svg-title="t('nextYear')" /> </template> </bento-button> </div> </div> <table class="b-calendar-year__year-container" role="grid" :aria-labelledby="yearLabelId"> <tbody> <tr v-for="(row, index) in monthRows" :key="index"> <calendar-year-month v-for="(month, monthIndex) in row" :ref="addDayRef(month)" :key="`calendar-month-${monthIndex}`" :month="month" :disabled="isDateDisabled(month)" :selected="isSelectedDate(month)" :variant="monthVariant(month)" @click="onMonthClicked" @mouseover="emit('mouseover', month)" /> </tr> </tbody> </table> </div> </template> <script setup lang="ts"> import { computed, toRef } from 'vue'; import { isSameMonth } from 'date-fns/isSameMonth'; import { isWithinInterval } from 'date-fns/isWithinInterval'; import BentoButton from '@/components/button/button.vue'; import { BentoTypography } from '@/components/typography'; import CalendarYearMonth from './components/calendar-year-month/calendar-year-month.vue'; import ChevronLeftIcon from '@adyen/ui-assets-icons-16/vue/chevron-left'; import ChevronRightIcon from '@adyen/ui-assets-icons-16/vue/chevron-right'; import { BentoKeyboardNavigationDirective as vBentoKeyboardNavigationDirective } from '@/directives'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { useMoveCalendarDate } from '@/internal/calendar/components/calendar-month/composables/move-calendar-date'; import { type CalendarYearProps } from './calendar-year.types'; import { CalendarYearMonthVariant } from './components/calendar-year-month/calendar-year-month.types'; import { type CalendarEmit, CalendarPosition } from '../../calendar.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const yearLabelId = generateUid('bento-date-picker-year-label'); const props = withDefaults(defineProps<CalendarYearProps>(), { endDate: null, isDateDisabled: undefined, isRange: false, min: null, max: null, position: CalendarPosition.UNIQUE, startDate: null, }); const emit = defineEmits<CalendarEmit>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const isDateDisabled = (date: Date) => (props.min && date < props.min) || (props.max && date > props.max) || props.isDateDisabled?.(date); /** * Renders the previous year button if it's Unique or First calendar */ const shouldShowPreviousYearButton = computed(() => (['first', 'unique'] as Array<CalendarPosition>).includes(props.position) ); /** * Renders the next year button if it's Unique or Last calendar */ const shouldShowNextYearButton = computed(() => (['last', 'unique'] as Array<CalendarPosition>).includes(props.position) ); const { internalDate, // Calendar refs addDayRef, // Day navigation moveToPreviousMonth, moveToPreviousYear, moveToNextMonth, moveToNextYear, moveToFirstMonthOfYear, moveToLastMonthOfYear, focusOnDate, } = useMoveCalendarDate( { currentSelectedDate: toRef(props, 'startDate'), month: toRef(props, 'year'), isDateDisabled, }, emit ); defineExpose({ focusOnDate }); /** * Calculates all the months of the current "props.month" * and adds the respective day number to each column and week */ const monthRows = computed(() => { const rows = []; let count = 0; for (let i = 0; i < 3; i++) { const innerArray = []; for (let j = 0; j < 4; j++) { innerArray.push(new Date(props.year.getFullYear(), count++, 1)); } rows.push(innerArray); } return rows; }); /** * Calculates the variation of the days selected. * For single select it's always SINGLE * For ranges: * - Start & End is the same: SINGLE * - Start: BEGINNING * - End: END * - Within the range: MIDDLE * @param {Date} day Current day */ const monthVariant = (day: Date) => { if (!props.isRange || isSameMonth(props.startDate, props.endDate)) { return CalendarYearMonthVariant.SINGLE; } if (props.startDate && isSameMonth(props.startDate, day)) { return CalendarYearMonthVariant.BEGINNING; } if (props.endDate && isSameMonth(props.endDate, day)) { return CalendarYearMonthVariant.END; } if (props.startDate && props.endDate && isWithinInterval(day, { start: props.startDate, end: props.endDate })) { return CalendarYearMonthVariant.MIDDLE; } return null; }; /** * Checks every date in the calendar to see if it's selected. * @param date Current date in the calendar */ const isSelectedDate = (date: Date) => { // Single selection if (internalDate.value && isSameMonth(internalDate.value, date)) { return true; } // Range return ( props.startDate && props.endDate && isWithinInterval(date, { start: props.startDate, end: props.endDate }) ); }; const onMonthClicked = (selectedDate: Date) => { emit('input', selectedDate); }; const listeners = !props.isRange ? { 'keydown:arrow-right': moveToNextMonth, 'keydown:arrow-left': moveToPreviousMonth, 'keydown:arrow-up': (ev: Event) => moveToPreviousMonth(ev, false, 4), 'keydown:arrow-down': (ev: Event) => moveToNextMonth(ev, false, 4), 'keydown:home': moveToFirstMonthOfYear, 'keydown:end': moveToLastMonthOfYear, 'keydown:page-up': moveToPreviousYear, 'keydown:page-down': moveToNextYear, } : {}; </script> <script lang="ts"> /** * CalendarYear that displays all the months of a given year and allows * to move back and forth the visible year with the Next and Previous buttons * * @example * import { CalendarYear } from '@/internal/calendar'; * * export default { * components: { CalendarYear }, * template: ` * <calendar-year * :is-date-disabled="(date: Date) => boolean" * :year="new Date()" * is-range * :start-date="new Date()" * :end-date="new Date()" * @input="(selectedDate: Date) => void" * @mouseover="(event: Event) => void" * @nextMonth="() => void" * @previousMonth="() => void" * /> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./calendar-year.scss" />
@@ -1 +1 @@
1
- <!-- Taken from: https://github.com/Mechazawa/vue2-teleport/blob/master/src/Teleport.vue --> <template> <div v-if="isVue2" :class="classes"> <slot /> </div> <Teleport v-else :to="defaultTo" :where="where" :disabled="disabled" :class="classes"> <slot /> </Teleport> </template> <script setup lang="ts"> import { isVue2 } from 'vue-demi'; import { defineComponent, inject, type PropType } from 'vue'; import { MODAL_DIALOG_ID_INJECTION_KEY } from '@/components/modal/components/base-modal/base-modal.keys'; import { SIDEPANEL_DIALOG_ID_INJECTION_KEY } from '@/components/sidepanel/sidepanel.keys'; import { MODAL_FULLSCREEN_DIALOG_ID_INJECTION_KEY } from '@/components/modal-fullscreen/modal-fullscreen.keys'; import type { BentoTeleportProps } from './teleport.types'; </script> <script lang="ts"> export default defineComponent({ name: 'teleport', props: { to: { type: String as PropType<BentoTeleportProps['to']>, default: '', }, where: { type: String, default: 'after', }, disabled: Boolean as PropType<BentoTeleportProps['disabled']>, }, data: () => ({ childObserver: null, nodes: [], waiting: false, observer: null, parent: null, isVue2, }), computed: { classes() { if (this.disabled) { return ['b-teleport']; } return ['b-teleport', 'b-teleport--hidden']; }, defaultTo() { if (this.to) { return this.to; } const injectionKey = inject<string>(MODAL_DIALOG_ID_INJECTION_KEY, '') || inject<string>(MODAL_FULLSCREEN_DIALOG_ID_INJECTION_KEY, '') || inject<string>(SIDEPANEL_DIALOG_ID_INJECTION_KEY, ''); const teleportElement = injectionKey ? `#${injectionKey}` : 'body'; return teleportElement; }, }, watch: { to: 'maybeMove', where: 'maybeMove', disabled(value) { if (!isVue2) { return; } if (value) { this.disable(); // Ensure all event done. this.$nextTick(() => { this.teardownObserver(); }); } else { this.bootObserver(); this.move(); } }, }, mounted() { // In Vue 3, the built-in <Teleport> handles DOM movement. // Manual DOM manipulation and MutationObservers must be skipped // to avoid conflicting with Vue 3's virtual DOM tracking. if (!isVue2) { return; } // Store a reference to the nodes this.nodes = Array.from(this.$el.childNodes); if (!this.disabled) { this.bootObserver(); } // Move slot content to target this.maybeMove(); }, beforeDestroy() { // In Vue 3, cleanup is not needed as the built-in <Teleport> manages its own lifecycle. if (!isVue2) { return; } // Fix nodes reference this.nodes = this.getComponentChildrenNode(); // Move back this.disable(); // Stop observing this.teardownObserver(); }, methods: { // Using a fragment is faster because it'll trigger only a single reflow // See https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment getFragment(): DocumentFragment { const fragment = document.createDocumentFragment(); this.nodes.forEach(node => fragment.appendChild(node)); return fragment; }, disable() { this.$el.appendChild(this.getFragment()); this.parent = null; }, move() { this.waiting = false; this.parent = document.querySelector(this.defaultTo); if (!this.parent) { this.disable(); this.waiting = true; return; } if (this.where === 'before') { this.parent.prepend(this.getFragment()); } else { this.parent.appendChild(this.getFragment()); } }, maybeMove() { if (!isVue2 || this.disabled) { return; } this.move(); }, onMutations(mutations) { // Makes sure the move operation is only done once let shouldMove = false; for (let i = 0; i < mutations.length; i++) { const mutation = mutations[i]; const filteredAddedNodes = Array.from(mutation.addedNodes).filter( node => !this.nodes.includes(node) ); if (Array.from(mutation.removedNodes).includes(this.parent)) { this.disable(); this.waiting = !this.disabled; } else if (this.waiting && filteredAddedNodes.length > 0) { shouldMove = true; } } if (shouldMove) { this.move(); } }, bootObserver() { if (this.observer) { return; } this.observer = new MutationObserver(mutations => this.onMutations(mutations)); this.observer.observe(document.body, { childList: true, subtree: true, attributes: false, characterData: false, }); if (this.childObserver) { return; } // watch childNodes change this.childObserver = new MutationObserver(mutations => { const childChangeRecord = mutations.find(i => i.target === this.$el); if (childChangeRecord) { // Remove old nodes before update position. this.nodes.forEach(node => node.parentNode && node.parentNode.removeChild(node)); this.nodes = this.getComponentChildrenNode(); this.maybeMove(); } }); this.childObserver.observe(this.$el, { childList: true, subtree: false, attributes: false, characterData: false, }); }, teardownObserver() { if (this.observer) { this.observer.disconnect(); this.observer = null; } if (this.childObserver) { this.childObserver.disconnect(); this.childObserver = null; } }, getComponentChildrenNode() { // Vue3 if (!isVue2) { return []; } // Trick for Vue3 type checking return (this as any).$vnode.componentOptions.children.map(i => i.elm).filter(i => i); }, }, }); </script> <style lang="scss" scoped src="./teleport.scss" />
1
+ <!-- Taken from: https://github.com/Mechazawa/vue2-teleport/blob/master/src/Teleport.vue --> <template> <div v-if="isVue2" :class="classes"> <slot /> </div> <Teleport v-else :to="to || teleportTarget" :disabled="disabled"> <slot /> </Teleport> </template> <script setup lang="ts"> import { isVue2 } from 'vue-demi'; import { computed, defineComponent, inject, type PropType } from 'vue'; import { MODAL_DIALOG_ID_INJECTION_KEY } from '@/components/modal/components/base-modal/base-modal.keys'; import { SIDEPANEL_DIALOG_ID_INJECTION_KEY } from '@/components/sidepanel/sidepanel.keys'; import { MODAL_FULLSCREEN_DIALOG_ID_INJECTION_KEY } from '@/components/modal-fullscreen/modal-fullscreen.keys'; import type { BentoTeleportProps } from './teleport.types'; // inject() MUST be called during component setup (not inside an Options API // computed). In Vue 3.5+ calling inject outside of setup returns undefined, // which caused the "Invalid Teleport target: undefined" warning. const injectedModalKey = inject<string>(MODAL_DIALOG_ID_INJECTION_KEY, ''); const injectedModalFullscreenKey = inject<string>(MODAL_FULLSCREEN_DIALOG_ID_INJECTION_KEY, ''); const injectedSidepanelKey = inject<string>(SIDEPANEL_DIALOG_ID_INJECTION_KEY, ''); // Exposed to the template. Does not consider the `to` prop since that's // handled at the template call-site as `to || teleportTarget`. const teleportTarget = computed(() => { const key = injectedModalKey || injectedModalFullscreenKey || injectedSidepanelKey; return key ? `#${key}` : 'body'; }); </script> <script lang="ts"> export default defineComponent({ name: 'teleport', props: { to: { type: String as PropType<BentoTeleportProps['to']>, default: '', }, where: { type: String, default: 'after', }, disabled: Boolean as PropType<BentoTeleportProps['disabled']>, }, data: () => ({ childObserver: null, nodes: [], waiting: false, observer: null, parent: null, isVue2, }), computed: { classes() { if (this.disabled) { return ['b-teleport']; } return ['b-teleport', 'b-teleport--hidden']; }, // Only used by the Vue 2 polyfill path for internal DOM-move // target lookup (`document.querySelector(this.defaultTo)` in // `move()`). Vue 3 uses `teleportTarget` from <script setup> // directly in the template (`:to="to || teleportTarget"`). // // Vue 2 supports `inject()` during render-time computed // evaluation, so we resolve the modal/sidepanel injection keys // here. On Vue 3 this computed is never read because the // template branches on `isVue2` and the Vue 3 branch uses the // setup-bound `teleportTarget` instead. defaultTo() { if (this.to) { return this.to; } const injectionKey = inject<string>(MODAL_DIALOG_ID_INJECTION_KEY, '') || inject<string>(MODAL_FULLSCREEN_DIALOG_ID_INJECTION_KEY, '') || inject<string>(SIDEPANEL_DIALOG_ID_INJECTION_KEY, ''); return injectionKey ? `#${injectionKey}` : 'body'; }, }, watch: { to: 'maybeMove', where: 'maybeMove', disabled(value) { if (!isVue2) { return; } if (value) { this.disable(); // Ensure all event done. this.$nextTick(() => { this.teardownObserver(); }); } else { this.bootObserver(); this.move(); } }, }, mounted() { // In Vue 3, the built-in <Teleport> handles DOM movement. // Manual DOM manipulation and MutationObservers must be skipped // to avoid conflicting with Vue 3's virtual DOM tracking. if (!isVue2) { return; } // Store a reference to the nodes this.nodes = Array.from(this.$el.childNodes); if (!this.disabled) { this.bootObserver(); } // Move slot content to target this.maybeMove(); }, beforeDestroy() { // In Vue 3, cleanup is not needed as the built-in <Teleport> manages its own lifecycle. if (!isVue2) { return; } // Fix nodes reference this.nodes = this.getComponentChildrenNode(); // Move back this.disable(); // Stop observing this.teardownObserver(); }, methods: { // Using a fragment is faster because it'll trigger only a single reflow // See https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment getFragment(): DocumentFragment { const fragment = document.createDocumentFragment(); this.nodes.forEach(node => fragment.appendChild(node)); return fragment; }, disable() { this.$el.appendChild(this.getFragment()); this.parent = null; }, move() { this.waiting = false; this.parent = document.querySelector(this.defaultTo); if (!this.parent) { this.disable(); this.waiting = true; return; } if (this.where === 'before') { this.parent.prepend(this.getFragment()); } else { this.parent.appendChild(this.getFragment()); } }, maybeMove() { if (!isVue2 || this.disabled) { return; } this.move(); }, onMutations(mutations) { // Makes sure the move operation is only done once let shouldMove = false; for (let i = 0; i < mutations.length; i++) { const mutation = mutations[i]; const filteredAddedNodes = Array.from(mutation.addedNodes).filter( node => !this.nodes.includes(node) ); if (Array.from(mutation.removedNodes).includes(this.parent)) { this.disable(); this.waiting = !this.disabled; } else if (this.waiting && filteredAddedNodes.length > 0) { shouldMove = true; } } if (shouldMove) { this.move(); } }, bootObserver() { if (this.observer) { return; } this.observer = new MutationObserver(mutations => this.onMutations(mutations)); this.observer.observe(document.body, { childList: true, subtree: true, attributes: false, characterData: false, }); if (this.childObserver) { return; } // watch childNodes change this.childObserver = new MutationObserver(mutations => { const childChangeRecord = mutations.find(i => i.target === this.$el); if (childChangeRecord) { // Remove old nodes before update position. this.nodes.forEach(node => node.parentNode && node.parentNode.removeChild(node)); this.nodes = this.getComponentChildrenNode(); this.maybeMove(); } }); this.childObserver.observe(this.$el, { childList: true, subtree: false, attributes: false, characterData: false, }); }, teardownObserver() { if (this.observer) { this.observer.disconnect(); this.observer = null; } if (this.childObserver) { this.childObserver.disconnect(); this.childObserver = null; } }, getComponentChildrenNode() { // Vue3 if (!isVue2) { return []; } // Trick for Vue3 type checking return (this as any).$vnode.componentOptions.children.map(i => i.elm).filter(i => i); }, }, }); </script> <style lang="scss" scoped src="./teleport.scss" />
@@ -1 +1 @@
1
- <template> <li class="b-navigation-menu-group"> <button :id="buttonId" type="button" class="b-navigation-menu-group__header" :class="{ ...headerClasses, 'b-navigation-menu-group__header--no-icon': !hasSlot('icon'), }" :aria-expanded="`${isOpen && !isSemiCollapsed}`" :aria-controls="contentId" :aria-description="headerAriaDescription" @click="toggleGroup" > <div v-if="hasSlot('icon')" class="b-navigation-menu-group__icon"> <slot name="icon" /> </div> <bento-typography ref="labelRef" v-bento-tooltip-directive="isTruncated ? label : undefined" el="span" variant="body" stronger class="b-navigation-menu-group__label" > {{ label }} </bento-typography> <div class="b-navigation-menu-group__icon b-navigation-menu-group__icon--chevron"> <chevron-up-icon v-if="isOpen && !isSemiCollapsed" aria-hidden="true" /> <chevron-down-icon v-else aria-hidden="true" /> </div> </button> <Transition name="b-navigation-menu-group__animation--content"> <div v-show="isOpen" :id="contentId" class="b-navigation-menu-group__content-wrapper" role="region" :aria-labelledby="buttonId" :style="contentMaxHeight" > <ul ref="contentDiv" class="b-navigation-menu-group__content"> <slot /> </ul> </div> </Transition> </li> </template> <script setup lang="ts"> import { computed, inject, onMounted, onUnmounted, provide, ref, useSlots, watch } from 'vue'; import { useResizeObserver } from '@vueuse/core'; import { BentoTypography } from '@/components/typography'; import { observeSizeOfElement } from '@/utils/ts/resize'; import { useExpandableContentHeight } from '@/composables/use-expandable-content-height/use-expandable-content-height'; import { useHasSlot } from '@/composables'; import { BentoTooltipDirective as vBentoTooltipDirective } from '@/directives/tooltip'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import ChevronUpIcon from '@adyen/ui-assets-icons-16/vue/chevron-up'; import type { BentoNavigationMenuGroupProps, BentoNavigationMenuGroupState, BentoNavigationMenuItemRegistration, } from './navigation-menu-group.types'; import { NAVIGATION_MENU_STATE_INJECTION_KEY } from '../../navigation-menu.keys'; import { NAVIGATION_MENU_GROUP_STATE_INJECTION_KEY } from './navigation-menu-group.keys'; import { type BentoNavigationMenuState } from '../../navigation-menu.types'; import messages from '../../messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoNavigationMenuGroupProps>(), { isExpanded: false, }); const slots = useSlots(); const hasSlot = useHasSlot(slots); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const headerClasses = computed(() => ({ 'b-navigation-menu-group__header--expanded': isOpen.value && !isSemiCollapsed.value, })); const headerAriaDescription = computed(() => (isSemiCollapsed.value ? t('showingCurrentPage') : undefined)); const labelRef = ref(null); const isTruncated = ref(false); useResizeObserver(labelRef, () => { const el = labelRef.value?.$el ?? labelRef.value; if (el) { isTruncated.value = el.scrollWidth > el.clientWidth; } }); const groupId = generateUid('navigation-menu-group'); const buttonId = generateUid('navigation-menu-group-button'); const contentId = generateUid('navigation-menu-group-content'); const navigationMenuState = inject<BentoNavigationMenuState | null>(NAVIGATION_MENU_STATE_INJECTION_KEY, null); const nestedItems = ref<Array<BentoNavigationMenuItemRegistration>>([]); const registerItem = (itemId: string, isSelected: () => boolean) => { nestedItems.value.push({ id: itemId, isSelected }); }; const unregisterItem = (itemId: string) => { nestedItems.value = nestedItems.value.filter(item => item.id !== itemId); }; const isSemiCollapsed = ref(false); provide<BentoNavigationMenuGroupState>(NAVIGATION_MENU_GROUP_STATE_INJECTION_KEY, { registerItem, unregisterItem, isSemiCollapsed, }); const hasActiveItem = computed(() => nestedItems.value.some(item => item.isSelected())); const emit = defineEmits<{ /** * Emits update event when group is toggled */ (e: 'update:is-expanded', value: boolean): void; }>(); const isOpen = ref(props.isExpanded); const contentDiv = ref<HTMLUListElement>(null); const { updateContentHeight, contentHeight } = useExpandableContentHeight(contentDiv, isOpen); const contentMaxHeight = computed(() => (contentHeight.value ? { 'max-height': contentHeight.value } : {})); const toggleGroup = () => { if (isOpen.value && !isSemiCollapsed.value) { if (hasActiveItem.value) { isSemiCollapsed.value = true; } else { isOpen.value = false; } } else if (isSemiCollapsed.value) { isSemiCollapsed.value = false; } else { isOpen.value = true; } emit('update:is-expanded', isOpen.value && !isSemiCollapsed.value); }; const setOpen = (open: boolean) => { if (open) { isOpen.value = true; isSemiCollapsed.value = false; } else if (hasActiveItem.value) { isSemiCollapsed.value = true; } else { isSemiCollapsed.value = false; isOpen.value = false; } emit('update:is-expanded', open); }; watch( () => props.isExpanded, value => { if (!isSemiCollapsed.value) { isOpen.value = value; } } ); watch( () => contentDiv.value, () => { let observer: ResizeObserver | null = null; if (!observer && contentDiv.value) { observer = observeSizeOfElement(contentDiv.value, () => { if (contentDiv.value) { updateContentHeight(); } }); } } ); watch( isOpen, () => { updateContentHeight(); }, { immediate: true } ); onMounted(() => { if (hasActiveItem.value && !isOpen.value) { isOpen.value = true; emit('update:is-expanded', true); } if (navigationMenuState) { navigationMenuState.registerGroup(groupId, setOpen, () => isOpen.value && !isSemiCollapsed.value); } }); onUnmounted(() => navigationMenuState?.unregisterGroup(groupId)); </script> <script lang="ts"> /** * Navigation menu group component for creating collapsible sections. * * @usage * import { BentoNavigationMenuGroup, BentoNavigationMenuItem } from '@adyen/bento-vue2'; * * export default { * components: { BentoNavigationMenuGroup, BentoNavigationMenuItem }, * template: ` * <bento-navigation-menu-group label="Section" :is-expanded="true"> * <template #icon><folder-icon /></template> * <bento-navigation-menu-item nested value="item-1" label="Item 1" /> * <bento-navigation-menu-item nested value="item-2" label="Item 2" /> * </bento-navigation-menu-group> * ` * } */ export default { name: 'bento-navigation-menu-group', i18n: { messages }, }; </script> <style lang="scss" scoped src="./navigation-menu-group.scss" />
1
+ <template> <li class="b-navigation-menu-group"> <button :id="buttonId" type="button" class="b-navigation-menu-group__header" :class="{ ...headerClasses, 'b-navigation-menu-group__header--no-icon': !hasSlot('icon'), }" :aria-expanded="`${isOpen && !isSemiCollapsed}`" :aria-controls="contentId" :aria-description="headerAriaDescription" @click="toggleGroup" > <div v-if="hasSlot('icon')" class="b-navigation-menu-group__icon"> <slot name="icon" /> </div> <bento-typography ref="labelRef" v-bento-tooltip-directive="isTruncated ? label : undefined" el="span" variant="body" stronger class="b-navigation-menu-group__label" > {{ label }} </bento-typography> <div class="b-navigation-menu-group__icon b-navigation-menu-group__icon--chevron"> <chevron-up-icon v-if="isOpen && !isSemiCollapsed" aria-hidden="true" /> <chevron-down-icon v-else aria-hidden="true" /> </div> </button> <Transition name="b-navigation-menu-group__animation--content"> <div v-show="isOpen" :id="contentId" class="b-navigation-menu-group__content-wrapper" role="region" :aria-labelledby="buttonId" :style="contentMaxHeight" > <ul ref="contentDiv" class="b-navigation-menu-group__content"> <slot /> </ul> </div> </Transition> </li> </template> <script setup lang="ts"> import { computed, inject, onMounted, onUnmounted, provide, ref, useSlots, watch } from 'vue'; import { useResizeObserver } from '@vueuse/core'; import { BentoTypography } from '@/components/typography'; import { useExpandableContentHeight } from '@/composables/use-expandable-content-height/use-expandable-content-height'; import { useHasSlot } from '@/composables'; import { BentoTooltipDirective as vBentoTooltipDirective } from '@/directives/tooltip'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import ChevronUpIcon from '@adyen/ui-assets-icons-16/vue/chevron-up'; import type { BentoNavigationMenuGroupProps, BentoNavigationMenuGroupState, BentoNavigationMenuItemRegistration, } from './navigation-menu-group.types'; import { NAVIGATION_MENU_STATE_INJECTION_KEY } from '../../navigation-menu.keys'; import { NAVIGATION_MENU_GROUP_STATE_INJECTION_KEY } from './navigation-menu-group.keys'; import { type BentoNavigationMenuState } from '../../navigation-menu.types'; import messages from '../../messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoNavigationMenuGroupProps>(), { isExpanded: false, }); const slots = useSlots(); const hasSlot = useHasSlot(slots); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const headerClasses = computed(() => ({ 'b-navigation-menu-group__header--expanded': isOpen.value && !isSemiCollapsed.value, })); const headerAriaDescription = computed(() => (isSemiCollapsed.value ? t('showingCurrentPage') : undefined)); const labelRef = ref(null); const isTruncated = ref(false); useResizeObserver(labelRef, () => { const el = labelRef.value?.$el ?? labelRef.value; if (el) { isTruncated.value = el.scrollWidth > el.clientWidth; } }); const groupId = generateUid('navigation-menu-group'); const buttonId = generateUid('navigation-menu-group-button'); const contentId = generateUid('navigation-menu-group-content'); const navigationMenuState = inject<BentoNavigationMenuState | null>(NAVIGATION_MENU_STATE_INJECTION_KEY, null); const nestedItems = ref<Array<BentoNavigationMenuItemRegistration>>([]); const registerItem = (itemId: string, isSelected: () => boolean) => { nestedItems.value.push({ id: itemId, isSelected }); }; const unregisterItem = (itemId: string) => { nestedItems.value = nestedItems.value.filter(item => item.id !== itemId); }; const isSemiCollapsed = ref(false); provide<BentoNavigationMenuGroupState>(NAVIGATION_MENU_GROUP_STATE_INJECTION_KEY, { registerItem, unregisterItem, isSemiCollapsed, }); const hasActiveItem = computed(() => nestedItems.value.some(item => item.isSelected())); const emit = defineEmits<{ /** * Emits update event when group is toggled */ (e: 'update:is-expanded', value: boolean): void; }>(); const isOpen = ref(props.isExpanded); const contentDiv = ref<HTMLUListElement>(null); const { contentHeight } = useExpandableContentHeight(contentDiv, isOpen); const contentMaxHeight = computed(() => (contentHeight.value ? { 'max-height': contentHeight.value } : {})); const toggleGroup = () => { if (isOpen.value && !isSemiCollapsed.value) { if (hasActiveItem.value) { isSemiCollapsed.value = true; } else { isOpen.value = false; } } else if (isSemiCollapsed.value) { isSemiCollapsed.value = false; } else { isOpen.value = true; } emit('update:is-expanded', isOpen.value && !isSemiCollapsed.value); }; const setOpen = (open: boolean) => { if (open) { isOpen.value = true; isSemiCollapsed.value = false; } else if (hasActiveItem.value) { isSemiCollapsed.value = true; } else { isSemiCollapsed.value = false; isOpen.value = false; } emit('update:is-expanded', open); }; watch( () => props.isExpanded, value => { if (!isSemiCollapsed.value) { isOpen.value = value; } } ); onMounted(() => { if (hasActiveItem.value && !isOpen.value) { isOpen.value = true; emit('update:is-expanded', true); } if (navigationMenuState) { navigationMenuState.registerGroup(groupId, setOpen, () => isOpen.value && !isSemiCollapsed.value); } }); onUnmounted(() => navigationMenuState?.unregisterGroup(groupId)); </script> <script lang="ts"> /** * Navigation menu group component for creating collapsible sections. * * @usage * import { BentoNavigationMenuGroup, BentoNavigationMenuItem } from '@adyen/bento-vue2'; * * export default { * components: { BentoNavigationMenuGroup, BentoNavigationMenuItem }, * template: ` * <bento-navigation-menu-group label="Section" :is-expanded="true"> * <template #icon><folder-icon /></template> * <bento-navigation-menu-item nested value="item-1" label="Item 1" /> * <bento-navigation-menu-item nested value="item-2" label="Item 2" /> * </bento-navigation-menu-group> * ` * } */ export default { name: 'bento-navigation-menu-group', i18n: { messages }, }; </script> <style lang="scss" scoped src="./navigation-menu-group.scss" />
@@ -1 +1 @@
1
- <template> <section class="b-secondary-nav-category" :aria-label="label"> <bento-typography el="h1" class="b-secondary-nav-category__label" variant="caption" stronger>{{ label }}</bento-typography> <ul class="b-secondary-nav__list"> <li v-for="item in items" :key="item.value"> <secondary-nav-item :label="item.label" :value="item.value" :items="item.items" /> </li> </ul> </section> </template> <script lang="ts" setup> import { BentoTypography } from '@/components/typography'; import { SecondaryNavItem } from '../secondary-nav-item'; import { type BentoSecondaryNavItemsItem } from '../../secondary-nav.types'; defineProps<{ /** * The display name of the category */ label: string; /** * A list of {@see BentoSecondaryNavItemsItem} that belongs to this category */ items: Array<BentoSecondaryNavItemsItem>; }>(); </script> <script lang="ts"> /** * This component shouldn't be used directly. It is internally used by {@see BentoSecondaryNav} */ export default {}; </script> <style lang="scss" scoped src="./secondary-nav-category.scss" />
1
+ <template> <section class="b-secondary-nav-category" :aria-label="label"> <bento-typography el="h1" class="b-secondary-nav-category__label" variant="caption" stronger>{{ label }}</bento-typography> <ul class="b-secondary-nav__list"> <li v-for="item in items" :key="item.value"> <secondary-nav-item :label="item.label" :value="item.value" :items="item.items" :error-counter="item.errorCounter" /> </li> </ul> </section> </template> <script lang="ts" setup> import { BentoTypography } from '@/components/typography'; import { SecondaryNavItem } from '../secondary-nav-item'; import { type BentoSecondaryNavItemsItem } from '../../secondary-nav.types'; defineProps<{ /** * The display name of the category */ label: string; /** * A list of {@see BentoSecondaryNavItemsItem} that belongs to this category */ items: Array<BentoSecondaryNavItemsItem>; }>(); </script> <script lang="ts"> /** * This component shouldn't be used directly. It is internally used by {@see BentoSecondaryNav} */ export default {}; </script> <style lang="scss" scoped src="./secondary-nav-category.scss" />
@@ -1 +1 @@
1
- <template> <div class="b-secondary-nav-item"> <div class="b-secondary-nav-item__wrapper"> <button class="b-secondary-nav-item__option" :class="conditionalClasses" :aria-current="isItemSelected ? 'page' : undefined" :aria-expanded="expandleItemIsToggleOnly ? `${isOpen}` : undefined" :aria-controls="expandleItemIsToggleOnly ? subItemsId : undefined" @click="expandleItemIsToggleOnly ? onToggleClick() : onClick($event)" > <bento-typography class="b-secondary-nav-item__title" variant="body" stronger> {{ label }} </bento-typography> </button> <bento-button v-if="hasSubItems" :aria-hidden="expandleItemIsToggleOnly" :tabindex="expandleItemIsToggleOnly ? -1 : undefined" class="b-secondary-nav-item__toggle" variant="tertiary" :aria-label="isOpen ? `Collapse ${label}` : `Expand ${label}`" :aria-expanded="!expandleItemIsToggleOnly ? `${isOpen}` : undefined" :aria-controls="!expandleItemIsToggleOnly ? subItemsId : undefined" @click.stop="onToggleClick" > <template #iconLeft> <chevron-up-icon v-if="isOpen" class="b-secondary-nav-item__icon" aria-hidden="true" /> <chevron-down-icon v-else class="b-secondary-nav-item__icon" aria-hidden="true" /> </template> </bento-button> </div> <Transition :name="CONTENT_TRANSITION_NAME"> <div v-if="hasSubItems && isOpen" :id="subItemsId" :style="subItemsMaxHeight"> <ul ref="subItemsRef" class="b-secondary-nav-item__sub-items"> <li v-for="item in items" :key="item.value"> <secondary-nav-item :label="item.label" :value="item.value" :items="item.items" is-sub-item /> </li> </ul> </div> </Transition> </div> </template> <script lang="ts" setup> import { SECONDARY_NAV_STATE } from '../../secondary-nav.keys'; import { BentoButton } from '@/components/button'; import { BentoTypography } from '@/components/typography'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import ChevronUpIcon from '@adyen/ui-assets-icons-16/vue/chevron-up'; import { computed, inject, ref, watch } from 'vue'; import { generateUid } from '@/core/utils/ts'; import { type BentoSecondaryNavItemsItem } from '../../secondary-nav.types'; import { observeSizeOfElement } from '@/utils/ts/resize'; import { useExpandableContentHeight } from '@/composables/use-expandable-content-height/use-expandable-content-height'; const { activeItemValue, updateActiveItemValue, allowExpandableItemNavigation } = inject(SECONDARY_NAV_STATE); const CONTENT_TRANSITION_NAME = 'b-secondary-nav-item__animation--content'; const props = defineProps<{ /** * The display name of the navigation item */ label: string; /** * The value of this item. */ value: string; /** * Nested sub-items for this item. */ items?: Array<BentoSecondaryNavItemsItem>; /** * Defines if the item is a subitem */ isSubItem?: boolean; }>(); const isOpen = ref(false); const subItemsId = generateUid('secondary-nav-sub-items'); const subItemsRef = ref<HTMLUListElement>(null); const hasSubItems = computed(() => !!props.items?.length); const isItemSelected = computed(() => activeItemValue.value === props.value); const expandleItemIsToggleOnly = computed(() => hasSubItems.value && !allowExpandableItemNavigation.value); const { updateContentHeight, contentHeight } = useExpandableContentHeight(subItemsRef, isOpen); const subItemsMaxHeight = computed(() => (contentHeight.value ? { 'max-height': contentHeight.value } : {})); const conditionalClasses = computed(() => ({ 'b-secondary-nav-item__option--active': isItemSelected.value, 'b-secondary-nav-item__option--expandable': hasSubItems.value, 'b-secondary-nav-item__option--sub-item': props.isSubItem, })); const emit = defineEmits<{ /** * Event emitted when this element is clicked */ (e: 'click', clickEvent: MouseEvent): void; }>(); const onClick = (e: MouseEvent) => { emit('click', e); updateActiveItemValue(props.value); }; const onToggleClick = () => { isOpen.value = !isOpen.value; }; const hasActiveDescendant = (items: Array<BentoSecondaryNavItemsItem>, value: string): boolean => { return items.some( item => item.value === value || (item.items ? hasActiveDescendant(item.items, value) : false) ); }; watch( () => subItemsRef.value, () => { let observer: ResizeObserver | null = null; if (!observer && subItemsRef.value) { observer = observeSizeOfElement(subItemsRef.value, () => { if (subItemsRef.value) { updateContentHeight(); } }); } } ); watch( isOpen, () => { updateContentHeight(); }, { immediate: true } ); watch( activeItemValue, value => { if (hasSubItems.value && props.items && hasActiveDescendant(props.items, value)) { isOpen.value = true; } }, { immediate: true } ); </script> <script lang="ts"> /** * This component shouldn't be used directly. It is internally used by {@see BentoSecondaryNav} */ export default {}; </script> <style lang="scss" scoped src="./secondary-nav-item.scss" />
1
+ <template> <div class="b-secondary-nav-item"> <div class="b-secondary-nav-item__wrapper"> <button class="b-secondary-nav-item__option" :class="conditionalClasses" :aria-current="isItemSelected ? 'page' : undefined" :aria-expanded="expandleItemIsToggleOnly ? `${isOpen}` : undefined" :aria-controls="expandleItemIsToggleOnly ? subItemsId : undefined" @click="expandleItemIsToggleOnly ? onToggleClick() : onClick($event)" > <bento-typography class="b-secondary-nav-item__title" variant="body" stronger> {{ label }} </bento-typography> <bento-tag v-if="errorCounter" class="b-secondary-nav-item__error-counter" variant="red"> <template #icon> <cross-circle-fill-icon aria-hidden="true" /> </template> <span>{{ errorCounter }}</span> <span class="b-secondary-nav-item__visually-hidden">{{ tc('errorCount', errorCounter) }}</span> </bento-tag> </button> <bento-button v-if="hasSubItems" :aria-hidden="expandleItemIsToggleOnly" :tabindex="expandleItemIsToggleOnly ? -1 : undefined" class="b-secondary-nav-item__toggle" variant="tertiary" :aria-label="isOpen ? `Collapse ${label}` : `Expand ${label}`" :aria-expanded="!expandleItemIsToggleOnly ? `${isOpen}` : undefined" :aria-controls="!expandleItemIsToggleOnly ? subItemsId : undefined" @click.stop="onToggleClick" > <template #iconLeft> <chevron-up-icon v-if="isOpen" class="b-secondary-nav-item__icon" aria-hidden="true" /> <chevron-down-icon v-else class="b-secondary-nav-item__icon" aria-hidden="true" /> </template> </bento-button> </div> <Transition :name="CONTENT_TRANSITION_NAME"> <div v-if="hasSubItems && isOpen" :id="subItemsId" :style="subItemsMaxHeight"> <ul ref="subItemsRef" class="b-secondary-nav-item__sub-items"> <li v-for="item in items" :key="item.value"> <secondary-nav-item :label="item.label" :value="item.value" :items="item.items" :error-counter="item.errorCounter" is-sub-item /> </li> </ul> </div> </Transition> </div> </template> <script lang="ts" setup> import { SECONDARY_NAV_STATE } from '../../secondary-nav.keys'; import { BentoButton } from '@/components/button'; import { BentoTag } from '@/components/tag'; import { BentoTypography } from '@/components/typography'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import ChevronUpIcon from '@adyen/ui-assets-icons-16/vue/chevron-up'; import CrossCircleFillIcon from '@adyen/ui-assets-icons-16/vue/cross-circle-fill'; import { computed, inject, ref, watch } from 'vue'; import { useI18n } from '@/utils/ts/i18n'; import { generateUid } from '@/core/utils/ts'; import { type BentoSecondaryNavItemsItem } from '../../secondary-nav.types'; import { useExpandableContentHeight } from '@/composables/use-expandable-content-height/use-expandable-content-height'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { tc } = useI18n<{ message: MessageSchema }>({ messages }); const { activeItemValue, updateActiveItemValue, allowExpandableItemNavigation } = inject(SECONDARY_NAV_STATE); const CONTENT_TRANSITION_NAME = 'b-secondary-nav-item__animation--content'; const props = defineProps<{ /** * The display name of the navigation item */ label: string; /** * The value of this item. */ value: string; /** * Nested sub-items for this item. */ items?: Array<BentoSecondaryNavItemsItem>; /** * Defines if the item is a subitem */ isSubItem?: boolean; /** * Defines the number of errors pertaining to this item */ errorCounter?: number; }>(); const isOpen = ref(false); const subItemsId = generateUid('secondary-nav-sub-items'); const subItemsRef = ref<HTMLUListElement>(null); const hasSubItems = computed(() => !!props.items?.length); const isItemSelected = computed(() => activeItemValue.value === props.value); const expandleItemIsToggleOnly = computed(() => hasSubItems.value && !allowExpandableItemNavigation.value); const { contentHeight } = useExpandableContentHeight(subItemsRef, isOpen); const subItemsMaxHeight = computed(() => (contentHeight.value ? { 'max-height': contentHeight.value } : {})); const conditionalClasses = computed(() => ({ 'b-secondary-nav-item__option--active': isItemSelected.value, 'b-secondary-nav-item__option--expandable': hasSubItems.value, 'b-secondary-nav-item__option--sub-item': props.isSubItem, })); const emit = defineEmits<{ /** * Event emitted when this element is clicked */ (e: 'click', clickEvent: MouseEvent): void; }>(); const onClick = (e: MouseEvent) => { emit('click', e); updateActiveItemValue(props.value); }; const onToggleClick = () => { isOpen.value = !isOpen.value; }; const hasActiveDescendant = (items: Array<BentoSecondaryNavItemsItem>, value: string): boolean => { return items.some( item => item.value === value || (item.items ? hasActiveDescendant(item.items, value) : false) ); }; const hasDescendantErrors = (items: Array<BentoSecondaryNavItemsItem>): boolean => items.some(item => !!item.errorCounter || (item.items ? hasDescendantErrors(item.items) : false)); watch( [activeItemValue, () => props.items], ([activeValue]) => { if (hasSubItems.value && props.items) { if (hasActiveDescendant(props.items, activeValue) || hasDescendantErrors(props.items)) { isOpen.value = true; } } }, { immediate: true, deep: true } ); </script> <script lang="ts"> /** * This component shouldn't be used directly. It is internally used by {@see BentoSecondaryNav} */ export default { i18n: { messages } }; </script> <style lang="scss" scoped src="./secondary-nav-item.scss" />
@@ -1 +1 @@
1
- import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoSecondaryNav from './secondary-nav.vue'; import WithSubItemsCodeSample from './__tests__/secondary-nav-with-sub-items.vue?raw'; import type { Meta, StoryObj } from '@storybook/vue'; import { type BentoSecondaryNavItems } from '@/components/secondary-nav/secondary-nav.types'; const meta: Meta = { title: 'Secondary Nav', component: BentoSecondaryNav, }; export default meta; type Story = StoryObj<typeof BentoSecondaryNav>; const DEFAULT_ITEMS: BentoSecondaryNavItems = [ { label: 'Option 1', value: 'option_1', }, { label: 'Option 2', value: 'option_2', }, { label: 'Option 3', value: 'option_3', }, ]; const ITEMS_WITH_CATEGORIES: BentoSecondaryNavItems = [ { label: 'Category 1', items: [ { label: 'Option 1', value: 'option_1', }, { label: 'Option 2', value: 'option_2', }, ], }, { label: 'Category 2', items: [ { label: 'Option 3', value: 'option_3', }, { label: 'Option 4', value: 'option_4', }, { label: 'Option 5', value: 'option_5', }, ], }, ]; const ITEMS_WITH_SUB_ITEMS: BentoSecondaryNavItems = ITEMS_WITH_CATEGORIES.map(category => ({ ...category, items: category.items.map(item => { if (item.value === 'option_2') { return { ...item, items: [ { label: 'Option 2.1', value: 'option_2_1', }, { label: 'Option 2.2', value: 'option_2_2', }, ], }; } if (item.value === 'option_4') { return { ...item, items: [ { label: 'Option 4.1', value: 'option_4_1', }, { label: 'Option 4.2', value: 'option_4_2', }, ], }; } return item; }), })); const DEFAULT_STORY_CODE_SAMPLE = ` <template> <bento-secondary-nav :items="items" :value="activeItem" @change="onChange" /> <template> <script lang="ts" setup> const items = [ { label: 'Option 1', value: 'option_1', }, { label: 'Option 2', value: 'option_2', }, { label: 'Option 3', value: 'option_3', }, ]; const activeItem = ref(items[0].value); const onChange = (newActiveItem: string) => { activeItem.value = newActiveItem; }; </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoSecondaryNav, }, props: Object.keys(argTypes), template: ` <bento-secondary-nav v-bind="args" @change="clickedAction" /> `, setup(props) { const clickedAction = value => action('change')(value); return { // Values args: isVue2 ? props : _args, // Events clickedAction, }; }, }), args: { items: DEFAULT_ITEMS, value: 'option_1', }, parameters: storybookDocsParameter(DEFAULT_STORY_CODE_SAMPLE), }; const WITH_CATEGORIES_STORY_CODE_SAMPLE = ` <template> <bento-secondary-nav :items="items" :value="activeItem" @change="onChange" /> <template> <script lang="ts" setup> const items = [ { label: 'Category 1', items: [ { label: 'Option 1', value: 'option_1', }, { label: 'Option 2', value: 'option_2', }, ], }, { label: 'Category 2', items: [ { label: 'Option 3', value: 'option_3', }, { label: 'Option 4', value: 'option_4', }, { label: 'Option 5', value: 'option_5', }, ], }, ]; const activeItem = ref(items[0].value); const onChange = (newActiveItem: string) => { activeItem.value = newActiveItem; }; </script> `; export const WithCategories: Story = { render: (_args, { argTypes }) => ({ components: { BentoSecondaryNav, }, props: Object.keys(argTypes), template: ` <bento-secondary-nav v-bind="args" @change="clickedAction" /> `, setup(props) { const clickedAction = value => action('change')(value); return { // Values args: isVue2 ? props : _args, // Events clickedAction, }; }, }), args: { items: ITEMS_WITH_CATEGORIES, }, parameters: storybookDocsParameter(WITH_CATEGORIES_STORY_CODE_SAMPLE), }; export const WithSubItems: Story = { render: (_args, { argTypes }) => ({ components: { BentoSecondaryNav, }, props: Object.keys(argTypes), template: ` <bento-secondary-nav v-bind="args" @change="clickedAction" /> `, setup(props) { const clickedAction = value => action('change')(value); return { args: isVue2 ? props : _args, clickedAction, }; }, }), args: { value: 'option_2_1', items: ITEMS_WITH_SUB_ITEMS, }, parameters: storybookDocsParameter(WithSubItemsCodeSample), };
1
+ import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoSecondaryNav from './secondary-nav.vue'; import DefaultCodeSample from './__tests__/secondary-nav-default-example.vue?raw'; import WithCategoriesCodeSample from './__tests__/secondary-nav-with-categories-example.vue?raw'; import WithSubItemsCodeSample from './__tests__/secondary-nav-with-sub-items-example.vue?raw'; import type { Meta, StoryObj } from '@storybook/vue'; import { type BentoSecondaryNavItems } from '@/components/secondary-nav/secondary-nav.types'; const meta: Meta = { title: 'Secondary Nav', component: BentoSecondaryNav, }; export default meta; type Story = StoryObj<typeof BentoSecondaryNav>; const DEFAULT_ITEMS: BentoSecondaryNavItems = [ { label: 'Option 1', value: 'option_1', }, { label: 'Option 2', value: 'option_2', errorCounter: 3, }, { label: 'Option 3', value: 'option_3', }, ]; const ITEMS_WITH_CATEGORIES: BentoSecondaryNavItems = [ { label: 'Category 1', items: [ { label: 'Option 1', value: 'option_1', errorCounter: 1, }, { label: 'Option 2', value: 'option_2', }, ], }, { label: 'Category 2', items: [ { label: 'Option 3', value: 'option_3', }, { label: 'Option 4', value: 'option_4', }, { label: 'Option 5', value: 'option_5', }, ], }, ]; const ITEMS_WITH_SUB_ITEMS: BentoSecondaryNavItems = ITEMS_WITH_CATEGORIES.map(category => ({ ...category, items: category.items.map(item => { if (item.value === 'option_2') { return { ...item, items: [ { label: 'Option 2.1', value: 'option_2_1', }, { label: 'Option 2.2', value: 'option_2_2', }, ], }; } if (item.value === 'option_4') { return { ...item, items: [ { label: 'Option 4.1', value: 'option_4_1', errorCounter: 2, }, { label: 'Option 4.2', value: 'option_4_2', }, ], }; } return item; }), })); export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoSecondaryNav, }, props: Object.keys(argTypes), template: ` <bento-secondary-nav v-bind="args" @change="clickedAction" /> `, setup(props) { const clickedAction = value => action('change')(value); return { // Values args: isVue2 ? props : _args, // Events clickedAction, }; }, }), args: { items: DEFAULT_ITEMS, value: 'option_1', }, parameters: storybookDocsParameter(DefaultCodeSample), }; export const WithCategories: Story = { render: (_args, { argTypes }) => ({ components: { BentoSecondaryNav, }, props: Object.keys(argTypes), template: ` <bento-secondary-nav v-bind="args" @change="clickedAction" /> `, setup(props) { const clickedAction = value => action('change')(value); return { // Values args: isVue2 ? props : _args, // Events clickedAction, }; }, }), args: { items: ITEMS_WITH_CATEGORIES, }, parameters: storybookDocsParameter(WithCategoriesCodeSample), }; export const WithSubItems: Story = { render: (_args, { argTypes }) => ({ components: { BentoSecondaryNav, }, props: Object.keys(argTypes), template: ` <bento-secondary-nav v-bind="args" @change="clickedAction" /> `, setup(props) { const clickedAction = value => action('change')(value); return { args: isVue2 ? props : _args, clickedAction, }; }, }), args: { value: 'option_2_1', items: ITEMS_WITH_SUB_ITEMS, }, parameters: storybookDocsParameter(WithSubItemsCodeSample), };
@@ -1 +1 @@
1
- import type { Ref } from 'vue'; export interface BentoSecondaryNavState { activeItemValue: Ref<string>; updateActiveItemValue: (id: string) => void; allowExpandableItemNavigation: Ref<boolean>; } export interface BentoSecondaryNavItemsItem { label: string; value: string; items?: Array<BentoSecondaryNavItemsItem>; } export interface BentoSecondaryNavItemsCategory { label: string; items: Array<BentoSecondaryNavItemsItem>; } export type BentoSecondaryNavItems = Array<BentoSecondaryNavItemsItem | BentoSecondaryNavItemsCategory>;
1
+ import type { Ref } from 'vue'; export interface BentoSecondaryNavState { activeItemValue: Ref<string>; updateActiveItemValue: (id: string) => void; allowExpandableItemNavigation: Ref<boolean>; } export interface BentoSecondaryNavItemsItem { label: string; value: string; items?: Array<BentoSecondaryNavItemsItem>; errorCounter?: number; } export interface BentoSecondaryNavItemsCategory { label: string; items: Array<BentoSecondaryNavItemsItem>; } export type BentoSecondaryNavItems = Array<BentoSecondaryNavItemsItem | BentoSecondaryNavItemsCategory>;
@@ -1 +1 @@
1
- <template> <nav class="b-secondary-nav"> <bento-dropdown v-if="isMobile" :aria-label="t('mobileDropdownAriaLabel')" :items="mobileDropdownItems" :static-categories="hasCategories" :model-value="activeItemValue" :is-option-disabled="isMobileOptionDisabled" @update:model-value="updateActiveItemValue" > <template #display-value="{ label }"> <bento-typography el="span"> {{ label }} </bento-typography> </template> <template #default="{ label, data }"> <bento-typography el="span" :class="{ 'b-secondary-nav__nested-option': !!data?.level }"> {{ label }} </bento-typography> </template> </bento-dropdown> <ul v-else class="b-secondary-nav__list"> <li v-for="item in items" :key="item.label"> <secondary-nav-category v-if="'items' in item && !('value' in item)" :label="item.label" :items="item.items" /> <secondary-nav-item v-else :label="item.label" :value="item.value" :items="item.items" /> </li> </ul> </nav> </template> <script setup lang="ts"> import { computed, provide, readonly, ref, toRef, watch } from 'vue'; import { SecondaryNavItem } from './components/secondary-nav-item'; import { SecondaryNavCategory } from './components/secondary-nav-category'; import { SECONDARY_NAV_STATE } from './secondary-nav.keys'; import { type BentoSecondaryNavItems, type BentoSecondaryNavItemsCategory, type BentoSecondaryNavItemsItem, type BentoSecondaryNavState, } from './secondary-nav.types'; import { BentoDropdown } from '@/components/dropdown'; import { BentoTypography } from '@/components/typography'; import { useBreakpoints } from '@vueuse/core'; import { useI18n } from '@/utils/ts/i18n'; import { type BentoListboxOptionItem, type BentoListboxOptions } from '@/types/listbox'; import { BMediaQuerySMax } from '@adyen/bento-design-tokens/dist/js/bento/es6'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const breakpoints = useBreakpoints({ s: BMediaQuerySMax, }); const isMobile = breakpoints.smallerOrEqual('s'); const props = defineProps<{ /** * An array containing all the items on the nav. The array can have either {@see BentoSecondaryNavItemsCategory }, * which is used to group different items or {@see BentoSecondaryNavItemsItem } if you don't need to group them. */ items: BentoSecondaryNavItems; /** * The value of the selected item. * If nothing is passed, it defaults to the first item value in items. */ value?: string; /** * When true, items that contain subitems are also clickable and can be selected. * When false, those items only act as expand/collapse toggles. */ allowExpandableItemNavigation?: boolean; }>(); const emit = defineEmits<{ /** * Emmited when the current selected item changes */ (e: 'change', category: string): void; }>(); const isCategory = (item: BentoSecondaryNavItems[number]): item is BentoSecondaryNavItemsCategory => 'items' in item && !('value' in item); const mapNestedItems = (items: Array<BentoSecondaryNavItemsItem>, level = 0): BentoListboxOptions => items.flatMap((item): BentoListboxOptions => { const currentItem: BentoListboxOptionItem = { label: item.label, value: item.value, data: { level, hasSubItems: !!item.items?.length }, }; if (item.items?.length) { return [currentItem, ...mapNestedItems(item.items, level + 1)]; } return [currentItem]; }); const isMobileOptionDisabled = (option: BentoListboxOptionItem): boolean => !props.allowExpandableItemNavigation && (option.data as { hasSubItems?: boolean })?.hasSubItems; const hasCategories = computed(() => props.items.some(item => isCategory(item))); const mobileDropdownItems = computed<BentoListboxOptions>(() => { return props.items.flatMap(item => { if (isCategory(item)) { return [ { label: item.label, value: `category-${item.label}`, items: mapNestedItems(item.items), } satisfies BentoListboxOptionItem, ]; } if (item.items?.length) { return mapNestedItems([item]); } return [ { label: item.label, value: item.value, data: { level: 0 }, } satisfies BentoListboxOptionItem, ]; }); }); const flattenNestedItems = (items: Array<BentoSecondaryNavItemsItem>): Array<BentoSecondaryNavItemsItem> => items.flatMap(item => [item, ...(item.items?.length ? flattenNestedItems(item.items) : [])]); const findFirstSelectableNestedItemValue = (items: Array<BentoSecondaryNavItemsItem>): string => flattenNestedItems(items).find(item => props.allowExpandableItemNavigation || !item.items?.length)?.value ?? ''; const activeItemValueFallback = computed(() => { if (!props.items.length) { return ''; } return findFirstSelectableNestedItemValue( props.items.flatMap(item => isCategory(item) ? flattenNestedItems(item.items) : flattenNestedItems([item]) ) ); }); const activeItemValue = ref(props.value ?? activeItemValueFallback.value); const updateActiveItemValue = (value: string) => { activeItemValue.value = value; emit('change', value); }; // The component's state is inject on `secondary-nav-item` to determine the // current active item and to change its value const state: BentoSecondaryNavState = { activeItemValue: readonly(activeItemValue), updateActiveItemValue, allowExpandableItemNavigation: toRef(props, 'allowExpandableItemNavigation'), }; watch( () => props.value, value => { if (value !== undefined) { activeItemValue.value = value; } } ); provide(SECONDARY_NAV_STATE, state); </script> <script lang="ts"> /** * Secondary Nav offers users a structured way to access information. * It also provides a clear hierarchy for users to understand and navigate through various information. * * @example * import { BentoSecondaryNav } from '@adyen/bento-vue2'; * * export default { * components: { BentoSecondaryNav }, * template: ` * <bento-secondary-nav :value="value" :items="items" /> * `, * setup() { * const value = ref(2) // Active item value * const items: BentoSecondaryNavItemInfo[] = [ * { label: 'Option 1', value: 1 }, * { label: 'Option 2', value: 2 }, * ]; * return { items, value }; * } * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./secondary-nav.scss" />
1
+ <template> <nav class="b-secondary-nav"> <bento-dropdown v-if="isMobile" :aria-label="t('mobileDropdownAriaLabel')" :items="mobileDropdownItems" :static-categories="hasCategories" :model-value="activeItemValue" :is-option-disabled="isMobileOptionDisabled" @update:model-value="updateActiveItemValue" > <template #display-value="{ label }"> <bento-typography el="span"> {{ label }} </bento-typography> </template> <template #default="{ label, data }"> <bento-typography el="div" :class="{ 'b-secondary-nav__option': true, 'b-secondary-nav__nested-option': !!data?.level }" > {{ label }} <bento-tag v-if="data?.errorCounter" class="b-secondary-nav__error-counter" variant="red"> <template #icon> <cross-circle-fill-icon aria-hidden="true" /> </template> <span>{{ data.errorCounter }}</span> <span class="b-secondary-nav__visually-hidden">{{ tc('errorCount', data.errorCounter) }}</span> </bento-tag> </bento-typography> </template> </bento-dropdown> <ul v-else class="b-secondary-nav__list"> <li v-for="item in items" :key="item.label"> <secondary-nav-category v-if="'items' in item && !('value' in item)" :label="item.label" :items="item.items" /> <secondary-nav-item v-else :label="item.label" :value="item.value" :items="item.items" :error-counter="item.errorCounter" /> </li> </ul> </nav> </template> <script setup lang="ts"> import { computed, provide, readonly, ref, toRef, watch } from 'vue'; import { SecondaryNavItem } from './components/secondary-nav-item'; import { SecondaryNavCategory } from './components/secondary-nav-category'; import { SECONDARY_NAV_STATE } from './secondary-nav.keys'; import { type BentoSecondaryNavItems, type BentoSecondaryNavItemsCategory, type BentoSecondaryNavItemsItem, type BentoSecondaryNavState, } from './secondary-nav.types'; import { BentoDropdown } from '@/components/dropdown'; import { BentoTypography } from '@/components/typography'; import { BentoTag } from '@/components/tag'; import CrossCircleFillIcon from '@adyen/ui-assets-icons-16/vue/cross-circle-fill'; import { useBreakpoints } from '@vueuse/core'; import { useI18n } from '@/utils/ts/i18n'; import { type BentoListboxOptionItem, type BentoListboxOptions } from '@/types/listbox'; import { BMediaQuerySMax } from '@adyen/bento-design-tokens/dist/js/bento/es6'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t, tc } = useI18n<{ message: MessageSchema }>({ messages }); const breakpoints = useBreakpoints({ s: BMediaQuerySMax, }); const isMobile = breakpoints.smallerOrEqual('s'); const props = defineProps<{ /** * An array containing all the items on the nav. The array can have either {@see BentoSecondaryNavItemsCategory }, * which is used to group different items or {@see BentoSecondaryNavItemsItem } if you don't need to group them. */ items: BentoSecondaryNavItems; /** * The value of the selected item. * If nothing is passed, it defaults to the first item value in items. */ value?: string; /** * When true, items that contain subitems are also clickable and can be selected. * When false, those items only act as expand/collapse toggles. */ allowExpandableItemNavigation?: boolean; }>(); const emit = defineEmits<{ /** * Emmited when the current selected item changes */ (e: 'change', category: string): void; }>(); const isCategory = (item: BentoSecondaryNavItems[number]): item is BentoSecondaryNavItemsCategory => 'items' in item && !('value' in item); const mapNestedItems = (items: Array<BentoSecondaryNavItemsItem>, level = 0): BentoListboxOptions => items.flatMap((item): BentoListboxOptions => { const currentItem: BentoListboxOptionItem = { label: item.label, value: item.value, data: { level, hasSubItems: !!item.items?.length, errorCounter: item.errorCounter }, }; if (item.items?.length) { return [currentItem, ...mapNestedItems(item.items, level + 1)]; } return [currentItem]; }); const isMobileOptionDisabled = (option: BentoListboxOptionItem): boolean => !props.allowExpandableItemNavigation && (option.data as { hasSubItems?: boolean })?.hasSubItems; const hasCategories = computed(() => props.items.some(item => isCategory(item))); const mobileDropdownItems = computed<BentoListboxOptions>(() => { return props.items.flatMap(item => { if (isCategory(item)) { return [ { label: item.label, value: `category-${item.label}`, items: mapNestedItems(item.items), } satisfies BentoListboxOptionItem, ]; } if (item.items?.length) { return mapNestedItems([item]); } return [ { label: item.label, value: item.value, data: { level: 0, errorCounter: item.errorCounter }, } satisfies BentoListboxOptionItem, ]; }); }); const flattenNestedItems = (items: Array<BentoSecondaryNavItemsItem>): Array<BentoSecondaryNavItemsItem> => items.flatMap(item => [item, ...(item.items?.length ? flattenNestedItems(item.items) : [])]); const findFirstSelectableNestedItemValue = (items: Array<BentoSecondaryNavItemsItem>): string => flattenNestedItems(items).find(item => props.allowExpandableItemNavigation || !item.items?.length)?.value ?? ''; const activeItemValueFallback = computed(() => { if (!props.items.length) { return ''; } return findFirstSelectableNestedItemValue( props.items.flatMap(item => isCategory(item) ? flattenNestedItems(item.items) : flattenNestedItems([item]) ) ); }); const activeItemValue = ref(props.value ?? activeItemValueFallback.value); const updateActiveItemValue = (value: string) => { activeItemValue.value = value; emit('change', value); }; // The component's state is inject on `secondary-nav-item` to determine the // current active item and to change its value const state: BentoSecondaryNavState = { activeItemValue: readonly(activeItemValue), updateActiveItemValue, allowExpandableItemNavigation: toRef(props, 'allowExpandableItemNavigation'), }; watch( () => props.value, value => { if (value !== undefined) { activeItemValue.value = value; } } ); provide(SECONDARY_NAV_STATE, state); </script> <script lang="ts"> /** * Secondary Nav offers users a structured way to access information. * It also provides a clear hierarchy for users to understand and navigate through various information. * * @example * import { BentoSecondaryNav } from '@adyen/bento-vue2'; * * export default { * components: { BentoSecondaryNav }, * template: ` * <bento-secondary-nav :value="value" :items="items" /> * `, * setup() { * const value = ref(2) // Active item value * const items: BentoSecondaryNavItemInfo[] = [ * { label: 'Option 1', value: 1 }, * { label: 'Option 2', value: 2 }, * ]; * return { items, value }; * } * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./secondary-nav.scss" />
@@ -1 +1 @@
1
- import { type Ref } from 'vue'; import { type MaybeRef } from '@/types/maybe-ref'; /** * Public */ export interface BentoTabProps { /** * Displays a counter next to the tab title */ counter?: number; /** * Determines whether the tab is disabled */ disabled?: boolean; /** * Prevents a tab from being displayed. * Use `hidden` instead of `v-show`. */ hidden?: boolean; /** * Title of the tab */ title: string; /** * Displays the tab with a notification indicator */ withNotificationIndicator?: boolean; /** * Callback function to safely capture the underlying DOM element of the tab. * Useful for anchoring tooltips, popovers, or tutorials. * * @example * // 1. Define your ref and setter in setup() * const tabNode = ref<Element | null>(null); * const setTabNode = (el: Element | null) => { tabNode.value = el; }; * * // 2. Pass the setter to the tab * <bento-tab :set-element-ref="setTabNode"> */ setElementRef?: (el: Element | null) => void; } /** * Private */ export interface BentoTab { counter: Ref<number>; disabled: Ref<boolean>; hidden: Ref<boolean>; id: string; title: MaybeRef<string>; withNotificationIndicator: Ref<boolean>; setElementRef: BentoTabProps['setElementRef']; } export type BentoTabProvidedTitle = (value: string) => void;
1
+ import { type Ref } from 'vue'; import { type MaybeRef } from '@/types/maybe-ref'; /** * Public */ export interface BentoTabProps { /** * Displays a counter next to the tab title */ counter?: number; /** * Determines whether the tab is disabled */ disabled?: boolean; /** * Displays a counter with an error icon next to the tab title */ errorCounter?: number; /** * Prevents a tab from being displayed. * Use `hidden` instead of `v-show`. */ hidden?: boolean; /** * Title of the tab */ title: string; /** * Displays the tab with a notification indicator */ withNotificationIndicator?: boolean; /** * Callback function to safely capture the underlying DOM element of the tab. * Useful for anchoring tooltips, popovers, or tutorials. * * @example * // 1. Define your ref and setter in setup() * const tabNode = ref<Element | null>(null); * const setTabNode = (el: Element | null) => { tabNode.value = el; }; * * // 2. Pass the setter to the tab * <bento-tab :set-element-ref="setTabNode"> */ setElementRef?: (el: Element | null) => void; } /** * Private */ export interface BentoTab { counter: Ref<number>; disabled: Ref<boolean>; errorCounter: Ref<number>; hidden: Ref<boolean>; id: string; title: MaybeRef<string>; withNotificationIndicator: Ref<boolean>; setElementRef: BentoTabProps['setElementRef']; } export type BentoTabProvidedTitle = (value: string) => void;
@@ -1 +1 @@
1
- <template> <div v-show="activeTabId === id" :id="`${id}-tabpanel`" class="b-tab" role="tabpanel" :aria-labelledby="id"> <template v-if="activeTabId === id"> <slot /> </template> </div> </template> <script setup lang="ts"> import { computed, inject, onUnmounted, provide, ref, toRefs } from 'vue'; import { EXTERNAL_TITLE_REGISTER_ENTRY_INJECTION_KEY, TABS_ACTIVE_TAB_ID_KEY, TABS_REGISTER_TAB_INJECTION_KEY, } from '../tabs.keys'; import { type BentoTabsRegisterTab } from '../tabs.types'; import { type BentoTabProps, type BentoTabProvidedTitle } from './tab.types'; const props = withDefaults(defineProps<BentoTabProps>(), { counter: null, disabled: false, hidden: false, withNotificationIndicator: false, setElementRef: undefined, }); // External title injection const { counter, disabled, hidden, withNotificationIndicator } = toRefs(props); const componentTitle = ref<BentoTabProps['title']>(); const setTitle: BentoTabProvidedTitle = (value: string) => { componentTitle.value = value; }; provide(EXTERNAL_TITLE_REGISTER_ENTRY_INJECTION_KEY, setTitle); const computedTitle = computed<BentoTabProps['title']>(() => componentTitle.value ?? props.title); const registerTab: BentoTabsRegisterTab = inject(TABS_REGISTER_TAB_INJECTION_KEY); const activeTabId = inject(TABS_ACTIVE_TAB_ID_KEY); const { id, unregisterTab } = registerTab({ counter, disabled, hidden, title: computedTitle, withNotificationIndicator, setElementRef: props.setElementRef, }); onUnmounted(() => unregisterTab()); </script> <script lang="ts"> /** * Tab content which attaches a tab to the `bento-tabs` component * and renders is content when it becomes active. * * @example * import { BentoTab, BentoTabs } from '@adyen/bento-vue2'; * * <template> * <bento-tabs> * <bento-tab * :counter="1" * :disabled="false" * :hidden="false" * title="Tab's title" * with-notification-indicator * > * Tab content * </bento-tab> * </bento-tabs> * </template> */ export default { name: 'bento-tab', }; </script> <style lang="scss" scoped src="./tab.scss" />
1
+ <template> <div v-show="activeTabId === id" :id="`${id}-tabpanel`" class="b-tab" role="tabpanel" :aria-labelledby="id"> <template v-if="activeTabId === id"> <slot /> </template> </div> </template> <script setup lang="ts"> import { computed, inject, onUnmounted, provide, ref, toRefs } from 'vue'; import { EXTERNAL_TITLE_REGISTER_ENTRY_INJECTION_KEY, TABS_ACTIVE_TAB_ID_KEY, TABS_REGISTER_TAB_INJECTION_KEY, } from '../tabs.keys'; import { type BentoTabsRegisterTab } from '../tabs.types'; import { type BentoTabProps, type BentoTabProvidedTitle } from './tab.types'; const props = withDefaults(defineProps<BentoTabProps>(), { counter: null, disabled: false, errorCounter: null, hidden: false, withNotificationIndicator: false, setElementRef: undefined, }); // External title injection const { counter, disabled, errorCounter, hidden, withNotificationIndicator } = toRefs(props); const componentTitle = ref<BentoTabProps['title']>(); const setTitle: BentoTabProvidedTitle = (value: string) => { componentTitle.value = value; }; provide(EXTERNAL_TITLE_REGISTER_ENTRY_INJECTION_KEY, setTitle); const computedTitle = computed<BentoTabProps['title']>(() => componentTitle.value ?? props.title); const registerTab: BentoTabsRegisterTab = inject(TABS_REGISTER_TAB_INJECTION_KEY); const activeTabId = inject(TABS_ACTIVE_TAB_ID_KEY); const { id, unregisterTab } = registerTab({ counter, disabled, errorCounter, hidden, title: computedTitle, withNotificationIndicator, setElementRef: props.setElementRef, }); onUnmounted(() => unregisterTab()); </script> <script lang="ts"> /** * Tab content which attaches a tab to the `bento-tabs` component * and renders is content when it becomes active. * * @example * import { BentoTab, BentoTabs } from '@adyen/bento-vue2'; * * <template> * <bento-tabs> * <bento-tab * :counter="1" * :disabled="false" * :hidden="false" * title="Tab's title" * with-notification-indicator * > * Tab content * </bento-tab> * </bento-tabs> * </template> */ export default { name: 'bento-tab', }; </script> <style lang="scss" scoped src="./tab.scss" />
@@ -1 +1 @@
1
- import { isVue2 } from 'vue-demi'; import BentoTabs from './tabs.vue'; import BentoTab from './components/tab.vue'; import type { Meta, StoryObj } from '@storybook/vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; const meta: Meta = { title: 'Tabs', component: BentoTabs, argTypes: { // Slots default: { name: 'default', type: { name: 'string', required: false }, description: 'Default slot content', control: 'text', table: { type: { summary: 'VNode[]' }, }, }, }, }; export default meta; type Story = StoryObj<typeof BentoTabs>; const defaultCode = ` <template> <bento-tabs :activeTabIndex="activeTabIndex" @update:active-tab-index="onTabSelected"> <bento-tab title="Maria Ahlefeldt"> (16 January 1755 - 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. </bento-tab> <bento-tab title="Carl Andersen"> (29 April 1847 - 7 May 1909) was a Danish flutist, conductor and composer born in Copenhagen, son of the flutist Christian Joachim Andersen. </bento-tab> <bento-tab title="Karl Truffel"> (1 March 1981 - 7 November 1992) was a Danish dog and commander born in Berlin, son of the machinist Tom Jahoor Truffel. </bento-tab> <bento-tab title="Maria Ahlefeldt" :disabled="true"> (16 January 1755 - 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. </bento-tab> </bento-tabs> </template> <script setup lang="ts"> import { BentoTab, BentoTabs } from '@adyen/bento-vue2'; import { ref } from 'vue' const activeTabIndex = ref(1) const onTabSelected = (newActiveTabIndex: number) => { activeTabIndex.value = newActiveTabIndex; } </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoTabs, BentoTab }, props: Object.keys(argTypes), template: ` <bento-tabs v-bind="args"> <bento-tab title="Maria Ahlefeldt"> (16 January 1755 - 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. </bento-tab> <bento-tab title="Carl Andersen"> (29 April 1847 - 7 May 1909) was a Danish flutist, conductor and composer born in Copenhagen, son of the flutist Christian Joachim Andersen. </bento-tab> <bento-tab title="Karl Truffel"> (1 March 1981 - 7 November 1992) was a Danish dog and commander born in Berlin, son of the machinist Tom Jahoor Truffel. </bento-tab> <bento-tab title="Maria Ahlefeldt" :disabled="true"> (16 January 1755 - 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. </bento-tab> </bento-tabs> `, setup(props) { return { args: isVue2 ? props : _args, }; }, }), parameters: storybookDocsParameter(defaultCode), }; export const Sticky: Story = { render: (_args, { argTypes }) => ({ components: { BentoTabs, BentoTab }, props: Object.keys(argTypes), template: ` <bento-tabs v-bind="args"> <bento-tab title="Maria Ahlefeldt"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas gravida dictum tellus vel semper. Maecenas id purus id massa sollicitudin lobortis sed at massa. Ut venenatis laoreet purus, ac sodales arcu facilisis vitae. Curabitur interdum quis nunc ac faucibus. Sed id dictum nibh, in dignissim augue. Mauris id convallis orci, ac ultrices nibh. Cras sollicitudin vitae tortor id aliquet. Sed vel interdum lectus. Sed nec augue non odio aliquet aliquam quis ut urna. Donec at tincidunt dolor, quis eleifend tellus. Nam ornare cursus ipsum, eu euismod dui hendrerit sed. Donec pulvinar ante a mattis gravida. Cras posuere magna nec lectus varius tincidunt. Suspendisse potenti. In congue, nibh vitae dapibus mollis, nibh erat dapibus magna, vel pretium risus nibh vitae nunc. Nunc ultricies iaculis erat, id accumsan tortor. Proin nec lacus quis arcu tempor cursus vitae et augue. Donec eget egestas est. Praesent pharetra aliquam justo id cursus. Suspendisse ut viverra dolor. Aliquam at rutrum enim, eget volutpat mi. Nunc ut pulvinar turpis. Aliquam porttitor mollis lacus et aliquam. Etiam ullamcorper ut nunc ac ultricies. Vestibulum congue ultrices dictum. Etiam volutpat, felis at fringilla luctus, turpis arcu varius dui, eget eleifend mi mauris nec nisl. Ut et elementum eros, a dapibus leo. Sed molestie lectus in dui consequat vehicula. Phasellus sodales odio et vestibulum consectetur. Maecenas sodales malesuada felis eget semper. Morbi et vestibulum arcu. Fusce ex massa, elementum at massa non, ultricies vestibulum elit. Pellentesque nec felis id quam vulputate dapibus id id dolor. Aenean facilisis nibh sed enim lacinia sodales. Cras viverra ac ipsum vitae commodo. Nulla quis congue ipsum. Nunc ultricies tristique fermentum. Fusce nec maximus nisl, id facilisis mauris. Duis nec maximus tellus. Fusce quis mauris posuere ex mollis faucibus viverra at leo. Donec ac neque fermentum, fringilla magna vel, scelerisque nisi. Nulla id vehicula lectus, ac gravida justo. Sed in lorem odio. Vivamus lacinia ipsum id nisi tincidunt mollis. Sed quis dui ut dolor tempus mattis id eu erat. Sed tempus arcu lectus, sit amet placerat dolor consequat sed. Maecenas fermentum, ligula eu luctus imperdiet, dolor nisi condimentum dolor, nec molestie justo dolor eu dolor. Nullam nisl dui, gravida et massa vel, malesuada faucibus diam. Aenean non convallis nulla, sit amet egestas eros. Ut efficitur tellus vel placerat euismod. In luctus scelerisque elit rutrum ornare. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Fusce iaculis scelerisque tincidunt. Ut vel consequat eros. Vivamus sodales dictum quam quis mattis. Curabitur rutrum bibendum dictum. Suspendisse eros nisi, maximus sed lorem eu, elementum dictum nibh. Donec pellentesque lorem non egestas aliquet. Suspendisse sit amet sagittis ipsum, sit amet venenatis nulla. Sed finibus lacus eu cursus vestibulum. Quisque laoreet tellus nunc, sit amet viverra leo consequat vel. Nunc feugiat porttitor diam, id gravida arcu pretium mollis. Duis mollis odio quam, vel rhoncus metus rhoncus non. Proin ac rutrum nisl. Pellentesque nec porta turpis, accumsan faucibus nisi. Nam at dui sed dui vestibulum porttitor. Nam sed tristique nisi, vitae placerat urna. Aliquam sit amet pretium dui, ac cursus dolor. Nullam vitae auctor neque, quis ornare massa. Duis hendrerit dolor sed ligula interdum pellentesque. Nullam enim turpis, venenatis quis diam vel, mattis porttitor metus. Vivamus vel porttitor nisi, eu commodo augue. Vestibulum libero erat, fringilla ac fermentum in, malesuada nec velit. Morbi quis venenatis dolor. Quisque a congue metus, in pellentesque nibh. Phasellus sodales felis id tempus dictum. Nam molestie nulla eu iaculis porttitor. Etiam in urna mi. Aenean sit amet egestas arcu, a pretium sem. Vestibulum dictum odio vel tempor porta. Mauris laoreet tristique feugiat. In dui est, sollicitudin sed aliquet non, posuere at augue. Praesent auctor vel tellus mollis imperdiet. Phasellus et tincidunt lacus, ut volutpat velit. Quisque vitae tortor accumsan ante semper tincidunt ac vel lorem. Duis in elit vel arcu malesuada semper ac ac mi. Duis massa sapien, lacinia ac sapien ut, volutpat sollicitudin eros. Maecenas cursus massa eu felis dignissim, gravida tincidunt lectus tristique. Maecenas et vehicula leo, consectetur vehicula orci. Sed viverra dictum lacus, ac tristique nisi laoreet vitae. Cras porta suscipit tempus. Praesent lacus massa, volutpat et convallis ac, feugiat sit amet purus. Sed semper faucibus dui, vitae ultricies urna scelerisque at. Curabitur tortor felis, porttitor non augue quis, cursus sollicitudin augue. Fusce sed viverra lorem. Integer sit amet ullamcorper odio. Vivamus et metus ac sem convallis accumsan ut vitae purus. Praesent ligula arcu, varius id vehicula non, sagittis at risus. Duis porta pellentesque commodo. Nullam fermentum nisi neque, in scelerisque eros rutrum ut. Duis id orci non justo sodales dictum et eget ex. Curabitur quis ante at lectus semper eleifend in id purus. Aenean sed suscipit nulla. Mauris ullamcorper sapien velit, sed volutpat ante luctus quis. Cras lobortis orci nec quam finibus dapibus. Aenean nec purus et nisl tristique commodo ac sed odio. Integer placerat ac mauris a elementum. Etiam vestibulum tellus diam, non eleifend justo ultrices a. Duis et elementum tellus. Pellentesque felis orci, rhoncus sed diam eu, finibus faucibus diam. Donec a magna odio. Vestibulum iaculis interdum arcu, ut vulputate nisl dictum sed. Praesent quis eros et nulla condimentum placerat. Nam vel auctor erat. Donec lobortis dapibus erat sit amet luctus. Sed vitae feugiat nulla, in suscipit massa. Nunc ut tellus consequat, pellentesque nibh id, finibus risus. Etiam vulputate a nisi vitae fermentum. Duis tempor, mauris scelerisque commodo molestie, risus massa gravida nisi, et iaculis libero risus non velit. Vestibulum in felis sed justo porttitor bibendum nec id ligula. Maecenas eros sapien, dapibus non scelerisque et, tristique et mi. Maecenas id felis odio. Donec sed lacus gravida justo egestas varius in id dolor. Nunc eu luctus urna, vitae consectetur lorem. Nunc et diam consequat, rhoncus odio non, bibendum ipsum. Aenean et sagittis elit. Aliquam posuere sit amet nisi vitae efficitur. Nulla id blandit dui, at iaculis tellus. In varius sem ac nulla volutpat egestas. Donec condimentum feugiat mauris, in tempor nibh cursus ac. Etiam interdum imperdiet vehicula. Aliquam fringilla, risus eu porttitor accumsan, mi ante consequat tellus, sed vehicula mi nulla quis nisl. Nunc sollicitudin turpis commodo urna egestas, eget pharetra ligula consequat. Donec in odio a arcu tempus tempus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Vestibulum iaculis turpis quis euismod pellentesque. Curabitur mattis ex et libero auctor, vel placerat est mollis. Sed nec felis eu sapien faucibus fermentum. Morbi ut risus a lectus fringilla commodo nec in massa. Sed ac suscipit magna. Nunc vel augue mollis, fermentum risus eu, varius dolor. Quisque molestie fringilla ligula. Integer sed posuere magna. Mauris porta ornare interdum. Vivamus ullamcorper maximus lorem, eu consectetur est finibus a. Vivamus sagittis egestas eros nec porta. Sed congue lacus nisl, nec euismod ex volutpat ut. Aliquam erat volutpat. Nam posuere nisl in scelerisque venenatis. Curabitur sed mauris lorem. Mauris ornare est in velit dapibus, nec aliquet dui rhoncus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc euismod orci a odio iaculis, venenatis congue lorem vestibulum. Aenean sollicitudin egestas porta. Nulla ut sem eu nulla vehicula fringilla. Morbi eu sodales sem. Integer tristique tincidunt est vestibulum vehicula. Donec eget nulla congue, posuere lacus at, ornare sapien. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean interdum volutpat lobortis. Sed nulla augue, suscipit quis laoreet sit amet, vestibulum ac ex. Phasellus in volutpat diam. Phasellus tristique ultrices purus, a aliquam erat. Proin diam quam, luctus non bibendum non, vulputate eget nulla. Morbi ornare mauris eget nulla imperdiet, eu pulvinar est blandit. Mauris ut tempor arcu. Cras facilisis dictum lacus, et faucibus sem consectetur et. Cras arcu ligula, posuere ac tempor eu, ornare a felis. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean posuere congue elementum. Sed id ligula a ante tristique aliquet. Quisque nec mollis felis. Pellentesque varius eu massa venenatis dapibus. Praesent lacinia lorem nec facilisis luctus. Donec malesuada pretium dui, vitae luctus metus. Morbi ut aliquam ligula. Sed gravida urna in augue accumsan facilisis. Maecenas ultricies augue eu fermentum vestibulum. Vivamus at orci a libero sagittis pretium. Suspendisse quis est justo. Duis rutrum varius est eget lacinia. Duis augue ante, finibus quis auctor sit amet, ultrices id odio. Duis ultricies laoreet fringilla. Nulla vitae placerat lectus. Aenean nisl dui, pellentesque et bibendum quis, fringilla vitae tortor. Aenean cursus velit sed ex facilisis eleifend et eu ante. Maecenas leo est, auctor id tempus aliquet, pharetra eget leo. Nullam fermentum metus mi, ac varius velit lobortis non. Nam ultricies, orci ut lacinia elementum, justo elit efficitur orci, nec ornare augue justo vel odio. Quisque efficitur mauris ac sagittis varius. Fusce id nunc feugiat, efficitur libero eget, imperdiet mi. Nullam at sollicitudin augue, vitae bibendum turpis. Quisque vitae semper nisl. Aenean justo nulla, tincidunt at est vitae, consectetur porta turpis. Ut accumsan dolor mi, feugiat accumsan orci aliquet at. Pellentesque velit tortor, gravida quis magna quis, eleifend posuere est. Aliquam libero quam, porta non tincidunt eu, consequat eu eros. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Mauris justo lectus, maximus in faucibus vitae, vestibulum ut tortor. Nulla facilisi. Nunc et lectus tellus. Aliquam pretium fringilla mauris, eu congue dui ultricies a. Suspendisse potenti. Maecenas dapibus auctor nisl a tincidunt. Vivamus elementum ex elit, vel molestie sem tristique nec. Etiam tristique volutpat risus, at sodales ex cursus sit amet. Donec aliquam, turpis sit amet vestibulum vulputate, mi leo mollis lorem, eleifend venenatis neque sapien ac erat. Morbi ullamcorper, mi et placerat dictum, tellus libero bibendum sem, id auctor justo lectus sed leo. Phasellus lacinia scelerisque tristique. Curabitur ut congue elit, vitae ullamcorper ligula. Cras purus lacus, dignissim eget justo ac, commodo convallis arcu. In ac pellentesque metus. Proin lobortis diam ac pellentesque sodales. In porta dolor id augue ultricies laoreet. Phasellus aliquam ultricies lorem, a mattis quam maximus vel. Vestibulum eu maximus nunc, eu pellentesque lorem. Vivamus sagittis arcu odio. Praesent nec malesuada sem. Morbi lobortis ullamcorper mollis. Morbi porttitor lectus sed erat luctus cursus. Donec a lacus a ligula vehicula iaculis. Vestibulum sagittis quam eu bibendum dapibus. Donec eleifend mollis suscipit. Morbi vitae magna libero. Nunc imperdiet ligula augue, ullamcorper molestie lacus mollis eget. Vestibulum vel tellus tortor. Suspendisse tincidunt in nisl vitae volutpat. Sed pharetra consequat dui sit amet venenatis. Suspendisse lobortis quis lectus ac maximus. Phasellus feugiat quam accumsan ipsum gravida faucibus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vivamus pellentesque diam non urna tristique, vel elementum mi aliquam. Aliquam turpis urna, fringilla a lorem sit amet, vestibulum laoreet mauris. Etiam ut justo finibus, tempor turpis eu, aliquet nisi. Aliquam eget lorem quam. Ut venenatis eros velit, eget lacinia augue aliquet at. Sed massa urna, imperdiet ut augue at, varius hendrerit arcu. Curabitur quis imperdiet diam. Aliquam porta eu metus et posuere. Suspendisse finibus lacus ex, quis aliquam urna rhoncus vitae. Morbi vel massa a sem vehicula elementum. Etiam orci diam, euismod quis aliquam eget, pulvinar et magna. Suspendisse volutpat leo risus, sed pretium tellus pharetra non. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Phasellus sed nunc vel enim vulputate convallis. Maecenas sit amet pretium mauris, in suscipit ligula. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras ultrices facilisis nunc vel suscipit. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Curabitur nunc justo, placerat tristique leo eu, facilisis tempor sapien. Nulla tempor metus ligula, sed consectetur eros hendrerit eget. Maecenas vel ex eget mi malesuada ultricies ut eget lorem. Morbi rutrum et nunc non volutpat. Sed id tellus est. Vivamus ac leo vulputate nulla pellentesque aliquam ut vel sem. Quisque sed semper enim. Quisque mattis ultrices tortor, at dapibus lectus facilisis non. Ut leo massa, semper a facilisis at, ultrices id risus. Duis id leo bibendum lacus lobortis laoreet. In hac habitasse platea dictumst. Suspendisse tempus diam ac tempus tempor. Suspendisse tincidunt quis tellus quis bibendum. Vivamus eu urna quis ex mollis mattis auctor nec sapien. Aliquam a libero egestas, accumsan ligula at, porttitor enim. Etiam finibus felis non nisl euismod, eu aliquam metus maximus. Nulla a tortor eu magna ultricies tempus sed vel risus. Nam id lacus quis massa auctor mattis. Vestibulum nec tempus enim, ut rhoncus enim. Vestibulum semper imperdiet lacus sit amet aliquet. Cras eros justo, vestibulum in mattis a, efficitur ac diam. Sed a malesuada leo. Quisque porta ex iaculis, pulvinar velit et, egestas nibh. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer et euismod sem. Aliquam ligula tortor, fermentum eu erat eu, pellentesque pharetra nibh. Fusce rutrum scelerisque purus, id auctor lorem vestibulum sed. Fusce id convallis sapien. Mauris feugiat eleifend orci. Donec nunc lorem, malesuada sit amet odio a, facilisis cursus odio. Maecenas eu faucibus lorem. Duis ut diam eu diam volutpat mollis. Aenean id dui nec ipsum porttitor scelerisque a quis purus. In scelerisque arcu fringilla, suscipit metus vitae, gravida lacus. Suspendisse non nibh consectetur, elementum nisi faucibus, tristique diam. Nunc auctor ipsum ac neque suscipit, nec dignissim est viverra. Cras luctus tempus suscipit. Maecenas ullamcorper diam ac quam fermentum venenatis. </bento-tab> <bento-tab title="Carl Andersen"> (29 April 1847 - 7 May 1909) was a Danish flutist, conductor and composer born in Copenhagen, son of the flutist Christian Joachim Andersen. </bento-tab> <bento-tab title="Karl Truffel"> (1 March 1981 - 7 November 1992) was a Danish dog and commander born in Berlin, son of the machinist Tom Jahoor Truffel. </bento-tab> <bento-tab title="Maria Ahlefeldt" :disabled="true"> (16 January 1755 - 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. </bento-tab> </bento-tabs> `, setup(props) { return { args: isVue2 ? props : _args, }; }, }), parameters: storybookDocsParameter(defaultCode), args: { sticky: true, }, }; const withNotificationIndicatorCode = ` <template> <bento-tabs :activeTabIndex="activeTabIndex" @update:active-tab-index="onTabSelected"> <bento-tab title="Maria Ahlefeldt"> (16 January 1755 - 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. </bento-tab> <bento-tab title="Carl Andersen" :with-notification-indicator="true"> (29 April 1847 - 7 May 1909) was a Danish flutist, conductor and composer born in Copenhagen, son of the flutist Christian Joachim Andersen. </bento-tab> <bento-tab title="Karl Truffel"> (1 March 1981 - 7 November 1992) was a Danish dog and commander born in Berlin, son of the machinist Tom Jahoor Truffel. </bento-tab> <bento-tab title="Maria Ahlefeldt" :disabled="true"> (16 January 1755 - 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. </bento-tab> </bento-tabs> </template> <script setup lang="ts"> import { BentoTab, BentoTabs } from '@adyen/bento-vue2'; import { ref } from 'vue' const activeTabIndex = ref(1) const onTabSelected = (newActiveTabIndex: number) => { activeTabIndex.value = newActiveTabIndex; } </script> `; export const WithNotificationIndicator: Story = { render: (_args, { argTypes }) => ({ components: { BentoTabs, BentoTab }, props: Object.keys(argTypes), template: ` <bento-tabs v-bind="args"> <bento-tab title="Maria Ahlefeldt"> (16 January 1755 - 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. </bento-tab> <bento-tab title="Carl Andersen" :with-notification-indicator="true"> (29 April 1847 - 7 May 1909) was a Danish flutist, conductor and composer born in Copenhagen, son of the flutist Christian Joachim Andersen. </bento-tab> <bento-tab title="Karl Truffel"> (1 March 1981 - 7 November 1992) was a Danish dog and commander born in Berlin, son of the machinist Tom Jahoor Truffel. </bento-tab> <bento-tab title="Maria Ahlefeldt" :disabled="true"> (16 January 1755 - 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. </bento-tab> </bento-tabs> `, setup(props) { return { args: isVue2 ? props : _args, }; }, }), parameters: storybookDocsParameter(withNotificationIndicatorCode), }; const withCountersCode = ` <template> <bento-tabs :activeTabIndex="activeTabIndex" @update:active-tab-index="onTabSelected"> <bento-tab title="Maria Ahlefeldt"> (16 January 1755 - 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. </bento-tab> <bento-tab title="Carl Andersen" :counter="10"> (29 April 1847 - 7 May 1909) was a Danish flutist, conductor and composer born in Copenhagen, son of the flutist Christian Joachim Andersen. </bento-tab> <bento-tab title="Karl Truffel" :counter="7" with-notification-indicator> (1 March 1981 - 7 November 1992) was a Danish dog and commander born in Berlin, son of the machinist Tom Jahoor Truffel. </bento-tab> <bento-tab title="Maria Ahlefeldt" :disabled="true" :counter="2"> (16 January 1755 - 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. </bento-tab> </bento-tabs> </template> <script setup lang="ts"> import { BentoTab, BentoTabs } from '@adyen/bento-vue2'; import { ref } from 'vue' const activeTabIndex = ref(1) const onTabSelected = (newActiveTabIndex: number) => { activeTabIndex.value = newActiveTabIndex; } </script> `; export const WithCounters: Story = { render: (_args, { argTypes }) => ({ components: { BentoTabs, BentoTab }, props: Object.keys(argTypes), template: ` <bento-tabs v-bind="args"> <bento-tab title="Maria Ahlefeldt"> (16 January 1755 - 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. </bento-tab> <bento-tab title="Carl Andersen" :counter="10"> (29 April 1847 - 7 May 1909) was a Danish flutist, conductor and composer born in Copenhagen, son of the flutist Christian Joachim Andersen. </bento-tab> <bento-tab title="Karl Truffel" :counter="7" with-notification-indicator> (1 March 1981 - 7 November 1992) was a Danish dog and commander born in Berlin, son of the machinist Tom Jahoor Truffel. </bento-tab> <bento-tab title="Maria Ahlefeldt" :disabled="true" :counter="2"> (16 January 1755 - 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. </bento-tab> </bento-tabs> `, setup(props) { return { args: isVue2 ? props : _args, }; }, }), parameters: storybookDocsParameter(withCountersCode), };
1
+ import { isVue2 } from 'vue-demi'; import BentoTabs from './tabs.vue'; import BentoTab from './components/tab.vue'; import type { Meta, StoryObj } from '@storybook/vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoTabsEverythingBagelExample from './__tests__/bento-tabs-everything-bagel-example.vue?raw'; const meta: Meta = { title: 'Tabs', component: BentoTabs, argTypes: { // Slots default: { name: 'default', type: { name: 'string', required: false }, description: 'Default slot content', control: 'text', table: { type: { summary: 'VNode[]' }, }, }, }, }; export default meta; type Story = StoryObj<typeof BentoTabs>; const defaultCode = ` <template> <bento-tabs :activeTabIndex="activeTabIndex" @update:active-tab-index="onTabSelected"> <bento-tab title="Maria Ahlefeldt"> (16 January 1755 - 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. </bento-tab> <bento-tab title="Carl Andersen"> (29 April 1847 - 7 May 1909) was a Danish flutist, conductor and composer born in Copenhagen, son of the flutist Christian Joachim Andersen. </bento-tab> <bento-tab title="Karl Truffel"> (1 March 1981 - 7 November 1992) was a Danish dog and commander born in Berlin, son of the machinist Tom Jahoor Truffel. </bento-tab> <bento-tab title="Maria Ahlefeldt" :disabled="true"> (16 January 1755 - 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. </bento-tab> </bento-tabs> </template> <script setup lang="ts"> import { BentoTab, BentoTabs } from '@adyen/bento-vue2'; import { ref } from 'vue' const activeTabIndex = ref(1) const onTabSelected = (newActiveTabIndex: number) => { activeTabIndex.value = newActiveTabIndex; } </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoTabs, BentoTab }, props: Object.keys(argTypes), template: ` <bento-tabs v-bind="args"> <bento-tab title="Maria Ahlefeldt"> (16 January 1755 - 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. </bento-tab> <bento-tab title="Carl Andersen"> (29 April 1847 - 7 May 1909) was a Danish flutist, conductor and composer born in Copenhagen, son of the flutist Christian Joachim Andersen. </bento-tab> <bento-tab title="Karl Truffel"> (1 March 1981 - 7 November 1992) was a Danish dog and commander born in Berlin, son of the machinist Tom Jahoor Truffel. </bento-tab> <bento-tab title="Maria Ahlefeldt" :disabled="true"> (16 January 1755 - 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. </bento-tab> </bento-tabs> `, setup(props) { return { args: isVue2 ? props : _args, }; }, }), parameters: storybookDocsParameter(defaultCode), }; export const Sticky: Story = { render: (_args, { argTypes }) => ({ components: { BentoTabs, BentoTab }, props: Object.keys(argTypes), template: ` <bento-tabs v-bind="args"> <bento-tab title="Maria Ahlefeldt"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas gravida dictum tellus vel semper. Maecenas id purus id massa sollicitudin lobortis sed at massa. Ut venenatis laoreet purus, ac sodales arcu facilisis vitae. Curabitur interdum quis nunc ac faucibus. Sed id dictum nibh, in dignissim augue. Mauris id convallis orci, ac ultrices nibh. Cras sollicitudin vitae tortor id aliquet. Sed vel interdum lectus. Sed nec augue non odio aliquet aliquam quis ut urna. Donec at tincidunt dolor, quis eleifend tellus. Nam ornare cursus ipsum, eu euismod dui hendrerit sed. Donec pulvinar ante a mattis gravida. Cras posuere magna nec lectus varius tincidunt. Suspendisse potenti. In congue, nibh vitae dapibus mollis, nibh erat dapibus magna, vel pretium risus nibh vitae nunc. Nunc ultricies iaculis erat, id accumsan tortor. Proin nec lacus quis arcu tempor cursus vitae et augue. Donec eget egestas est. Praesent pharetra aliquam justo id cursus. Suspendisse ut viverra dolor. Aliquam at rutrum enim, eget volutpat mi. Nunc ut pulvinar turpis. Aliquam porttitor mollis lacus et aliquam. Etiam ullamcorper ut nunc ac ultricies. Vestibulum congue ultrices dictum. Etiam volutpat, felis at fringilla luctus, turpis arcu varius dui, eget eleifend mi mauris nec nisl. Ut et elementum eros, a dapibus leo. Sed molestie lectus in dui consequat vehicula. Phasellus sodales odio et vestibulum consectetur. Maecenas sodales malesuada felis eget semper. Morbi et vestibulum arcu. Fusce ex massa, elementum at massa non, ultricies vestibulum elit. Pellentesque nec felis id quam vulputate dapibus id id dolor. Aenean facilisis nibh sed enim lacinia sodales. Cras viverra ac ipsum vitae commodo. Nulla quis congue ipsum. Nunc ultricies tristique fermentum. Fusce nec maximus nisl, id facilisis mauris. Duis nec maximus tellus. Fusce quis mauris posuere ex mollis faucibus viverra at leo. Donec ac neque fermentum, fringilla magna vel, scelerisque nisi. Nulla id vehicula lectus, ac gravida justo. Sed in lorem odio. Vivamus lacinia ipsum id nisi tincidunt mollis. Sed quis dui ut dolor tempus mattis id eu erat. Sed tempus arcu lectus, sit amet placerat dolor consequat sed. Maecenas fermentum, ligula eu luctus imperdiet, dolor nisi condimentum dolor, nec molestie justo dolor eu dolor. Nullam nisl dui, gravida et massa vel, malesuada faucibus diam. Aenean non convallis nulla, sit amet egestas eros. Ut efficitur tellus vel placerat euismod. In luctus scelerisque elit rutrum ornare. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Fusce iaculis scelerisque tincidunt. Ut vel consequat eros. Vivamus sodales dictum quam quis mattis. Curabitur rutrum bibendum dictum. Suspendisse eros nisi, maximus sed lorem eu, elementum dictum nibh. Donec pellentesque lorem non egestas aliquet. Suspendisse sit amet sagittis ipsum, sit amet venenatis nulla. Sed finibus lacus eu cursus vestibulum. Quisque laoreet tellus nunc, sit amet viverra leo consequat vel. Nunc feugiat porttitor diam, id gravida arcu pretium mollis. Duis mollis odio quam, vel rhoncus metus rhoncus non. Proin ac rutrum nisl. Pellentesque nec porta turpis, accumsan faucibus nisi. Nam at dui sed dui vestibulum porttitor. Nam sed tristique nisi, vitae placerat urna. Aliquam sit amet pretium dui, ac cursus dolor. Nullam vitae auctor neque, quis ornare massa. Duis hendrerit dolor sed ligula interdum pellentesque. Nullam enim turpis, venenatis quis diam vel, mattis porttitor metus. Vivamus vel porttitor nisi, eu commodo augue. Vestibulum libero erat, fringilla ac fermentum in, malesuada nec velit. Morbi quis venenatis dolor. Quisque a congue metus, in pellentesque nibh. Phasellus sodales felis id tempus dictum. Nam molestie nulla eu iaculis porttitor. Etiam in urna mi. Aenean sit amet egestas arcu, a pretium sem. Vestibulum dictum odio vel tempor porta. Mauris laoreet tristique feugiat. In dui est, sollicitudin sed aliquet non, posuere at augue. Praesent auctor vel tellus mollis imperdiet. Phasellus et tincidunt lacus, ut volutpat velit. Quisque vitae tortor accumsan ante semper tincidunt ac vel lorem. Duis in elit vel arcu malesuada semper ac ac mi. Duis massa sapien, lacinia ac sapien ut, volutpat sollicitudin eros. Maecenas cursus massa eu felis dignissim, gravida tincidunt lectus tristique. Maecenas et vehicula leo, consectetur vehicula orci. Sed viverra dictum lacus, ac tristique nisi laoreet vitae. Cras porta suscipit tempus. Praesent lacus massa, volutpat et convallis ac, feugiat sit amet purus. Sed semper faucibus dui, vitae ultricies urna scelerisque at. Curabitur tortor felis, porttitor non augue quis, cursus sollicitudin augue. Fusce sed viverra lorem. Integer sit amet ullamcorper odio. Vivamus et metus ac sem convallis accumsan ut vitae purus. Praesent ligula arcu, varius id vehicula non, sagittis at risus. Duis porta pellentesque commodo. Nullam fermentum nisi neque, in scelerisque eros rutrum ut. Duis id orci non justo sodales dictum et eget ex. Curabitur quis ante at lectus semper eleifend in id purus. Aenean sed suscipit nulla. Mauris ullamcorper sapien velit, sed volutpat ante luctus quis. Cras lobortis orci nec quam finibus dapibus. Aenean nec purus et nisl tristique commodo ac sed odio. Integer placerat ac mauris a elementum. Etiam vestibulum tellus diam, non eleifend justo ultrices a. Duis et elementum tellus. Pellentesque felis orci, rhoncus sed diam eu, finibus faucibus diam. Donec a magna odio. Vestibulum iaculis interdum arcu, ut vulputate nisl dictum sed. Praesent quis eros et nulla condimentum placerat. Nam vel auctor erat. Donec lobortis dapibus erat sit amet luctus. Sed vitae feugiat nulla, in suscipit massa. Nunc ut tellus consequat, pellentesque nibh id, finibus risus. Etiam vulputate a nisi vitae fermentum. Duis tempor, mauris scelerisque commodo molestie, risus massa gravida nisi, et iaculis libero risus non velit. Vestibulum in felis sed justo porttitor bibendum nec id ligula. Maecenas eros sapien, dapibus non scelerisque et, tristique et mi. Maecenas id felis odio. Donec sed lacus gravida justo egestas varius in id dolor. Nunc eu luctus urna, vitae consectetur lorem. Nunc et diam consequat, rhoncus odio non, bibendum ipsum. Aenean et sagittis elit. Aliquam posuere sit amet nisi vitae efficitur. Nulla id blandit dui, at iaculis tellus. In varius sem ac nulla volutpat egestas. Donec condimentum feugiat mauris, in tempor nibh cursus ac. Etiam interdum imperdiet vehicula. Aliquam fringilla, risus eu porttitor accumsan, mi ante consequat tellus, sed vehicula mi nulla quis nisl. Nunc sollicitudin turpis commodo urna egestas, eget pharetra ligula consequat. Donec in odio a arcu tempus tempus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Vestibulum iaculis turpis quis euismod pellentesque. Curabitur mattis ex et libero auctor, vel placerat est mollis. Sed nec felis eu sapien faucibus fermentum. Morbi ut risus a lectus fringilla commodo nec in massa. Sed ac suscipit magna. Nunc vel augue mollis, fermentum risus eu, varius dolor. Quisque molestie fringilla ligula. Integer sed posuere magna. Mauris porta ornare interdum. Vivamus ullamcorper maximus lorem, eu consectetur est finibus a. Vivamus sagittis egestas eros nec porta. Sed congue lacus nisl, nec euismod ex volutpat ut. Aliquam erat volutpat. Nam posuere nisl in scelerisque venenatis. Curabitur sed mauris lorem. Mauris ornare est in velit dapibus, nec aliquet dui rhoncus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc euismod orci a odio iaculis, venenatis congue lorem vestibulum. Aenean sollicitudin egestas porta. Nulla ut sem eu nulla vehicula fringilla. Morbi eu sodales sem. Integer tristique tincidunt est vestibulum vehicula. Donec eget nulla congue, posuere lacus at, ornare sapien. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean interdum volutpat lobortis. Sed nulla augue, suscipit quis laoreet sit amet, vestibulum ac ex. Phasellus in volutpat diam. Phasellus tristique ultrices purus, a aliquam erat. Proin diam quam, luctus non bibendum non, vulputate eget nulla. Morbi ornare mauris eget nulla imperdiet, eu pulvinar est blandit. Mauris ut tempor arcu. Cras facilisis dictum lacus, et faucibus sem consectetur et. Cras arcu ligula, posuere ac tempor eu, ornare a felis. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean posuere congue elementum. Sed id ligula a ante tristique aliquet. Quisque nec mollis felis. Pellentesque varius eu massa venenatis dapibus. Praesent lacinia lorem nec facilisis luctus. Donec malesuada pretium dui, vitae luctus metus. Morbi ut aliquam ligula. Sed gravida urna in augue accumsan facilisis. Maecenas ultricies augue eu fermentum vestibulum. Vivamus at orci a libero sagittis pretium. Suspendisse quis est justo. Duis rutrum varius est eget lacinia. Duis augue ante, finibus quis auctor sit amet, ultrices id odio. Duis ultricies laoreet fringilla. Nulla vitae placerat lectus. Aenean nisl dui, pellentesque et bibendum quis, fringilla vitae tortor. Aenean cursus velit sed ex facilisis eleifend et eu ante. Maecenas leo est, auctor id tempus aliquet, pharetra eget leo. Nullam fermentum metus mi, ac varius velit lobortis non. Nam ultricies, orci ut lacinia elementum, justo elit efficitur orci, nec ornare augue justo vel odio. Quisque efficitur mauris ac sagittis varius. Fusce id nunc feugiat, efficitur libero eget, imperdiet mi. Nullam at sollicitudin augue, vitae bibendum turpis. Quisque vitae semper nisl. Aenean justo nulla, tincidunt at est vitae, consectetur porta turpis. Ut accumsan dolor mi, feugiat accumsan orci aliquet at. Pellentesque velit tortor, gravida quis magna quis, eleifend posuere est. Aliquam libero quam, porta non tincidunt eu, consequat eu eros. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Mauris justo lectus, maximus in faucibus vitae, vestibulum ut tortor. Nulla facilisi. Nunc et lectus tellus. Aliquam pretium fringilla mauris, eu congue dui ultricies a. Suspendisse potenti. Maecenas dapibus auctor nisl a tincidunt. Vivamus elementum ex elit, vel molestie sem tristique nec. Etiam tristique volutpat risus, at sodales ex cursus sit amet. Donec aliquam, turpis sit amet vestibulum vulputate, mi leo mollis lorem, eleifend venenatis neque sapien ac erat. Morbi ullamcorper, mi et placerat dictum, tellus libero bibendum sem, id auctor justo lectus sed leo. Phasellus lacinia scelerisque tristique. Curabitur ut congue elit, vitae ullamcorper ligula. Cras purus lacus, dignissim eget justo ac, commodo convallis arcu. In ac pellentesque metus. Proin lobortis diam ac pellentesque sodales. In porta dolor id augue ultricies laoreet. Phasellus aliquam ultricies lorem, a mattis quam maximus vel. Vestibulum eu maximus nunc, eu pellentesque lorem. Vivamus sagittis arcu odio. Praesent nec malesuada sem. Morbi lobortis ullamcorper mollis. Morbi porttitor lectus sed erat luctus cursus. Donec a lacus a ligula vehicula iaculis. Vestibulum sagittis quam eu bibendum dapibus. Donec eleifend mollis suscipit. Morbi vitae magna libero. Nunc imperdiet ligula augue, ullamcorper molestie lacus mollis eget. Vestibulum vel tellus tortor. Suspendisse tincidunt in nisl vitae volutpat. Sed pharetra consequat dui sit amet venenatis. Suspendisse lobortis quis lectus ac maximus. Phasellus feugiat quam accumsan ipsum gravida faucibus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vivamus pellentesque diam non urna tristique, vel elementum mi aliquam. Aliquam turpis urna, fringilla a lorem sit amet, vestibulum laoreet mauris. Etiam ut justo finibus, tempor turpis eu, aliquet nisi. Aliquam eget lorem quam. Ut venenatis eros velit, eget lacinia augue aliquet at. Sed massa urna, imperdiet ut augue at, varius hendrerit arcu. Curabitur quis imperdiet diam. Aliquam porta eu metus et posuere. Suspendisse finibus lacus ex, quis aliquam urna rhoncus vitae. Morbi vel massa a sem vehicula elementum. Etiam orci diam, euismod quis aliquam eget, pulvinar et magna. Suspendisse volutpat leo risus, sed pretium tellus pharetra non. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Phasellus sed nunc vel enim vulputate convallis. Maecenas sit amet pretium mauris, in suscipit ligula. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras ultrices facilisis nunc vel suscipit. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Curabitur nunc justo, placerat tristique leo eu, facilisis tempor sapien. Nulla tempor metus ligula, sed consectetur eros hendrerit eget. Maecenas vel ex eget mi malesuada ultricies ut eget lorem. Morbi rutrum et nunc non volutpat. Sed id tellus est. Vivamus ac leo vulputate nulla pellentesque aliquam ut vel sem. Quisque sed semper enim. Quisque mattis ultrices tortor, at dapibus lectus facilisis non. Ut leo massa, semper a facilisis at, ultrices id risus. Duis id leo bibendum lacus lobortis laoreet. In hac habitasse platea dictumst. Suspendisse tempus diam ac tempus tempor. Suspendisse tincidunt quis tellus quis bibendum. Vivamus eu urna quis ex mollis mattis auctor nec sapien. Aliquam a libero egestas, accumsan ligula at, porttitor enim. Etiam finibus felis non nisl euismod, eu aliquam metus maximus. Nulla a tortor eu magna ultricies tempus sed vel risus. Nam id lacus quis massa auctor mattis. Vestibulum nec tempus enim, ut rhoncus enim. Vestibulum semper imperdiet lacus sit amet aliquet. Cras eros justo, vestibulum in mattis a, efficitur ac diam. Sed a malesuada leo. Quisque porta ex iaculis, pulvinar velit et, egestas nibh. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer et euismod sem. Aliquam ligula tortor, fermentum eu erat eu, pellentesque pharetra nibh. Fusce rutrum scelerisque purus, id auctor lorem vestibulum sed. Fusce id convallis sapien. Mauris feugiat eleifend orci. Donec nunc lorem, malesuada sit amet odio a, facilisis cursus odio. Maecenas eu faucibus lorem. Duis ut diam eu diam volutpat mollis. Aenean id dui nec ipsum porttitor scelerisque a quis purus. In scelerisque arcu fringilla, suscipit metus vitae, gravida lacus. Suspendisse non nibh consectetur, elementum nisi faucibus, tristique diam. Nunc auctor ipsum ac neque suscipit, nec dignissim est viverra. Cras luctus tempus suscipit. Maecenas ullamcorper diam ac quam fermentum venenatis. </bento-tab> <bento-tab title="Carl Andersen"> (29 April 1847 - 7 May 1909) was a Danish flutist, conductor and composer born in Copenhagen, son of the flutist Christian Joachim Andersen. </bento-tab> <bento-tab title="Karl Truffel"> (1 March 1981 - 7 November 1992) was a Danish dog and commander born in Berlin, son of the machinist Tom Jahoor Truffel. </bento-tab> <bento-tab title="Maria Ahlefeldt" :disabled="true"> (16 January 1755 - 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. </bento-tab> </bento-tabs> `, setup(props) { return { args: isVue2 ? props : _args, }; }, }), parameters: storybookDocsParameter(defaultCode), args: { sticky: true, }, }; export const EverythingBagel: Story = { render: (_args, { argTypes }) => ({ components: { BentoTabs, BentoTab }, props: Object.keys(argTypes), template: ` <bento-tabs v-bind="args"> <bento-tab title="Maria Ahlefeldt"> (16 January 1755 - 20 December 1810) was a Danish, (originally German), composer. She is known as the first female composer in Denmark. </bento-tab> <bento-tab title="Carl Andersen" :counter="10" :error-counter="1" with-notification-indicator> (29 April 1847 - 7 May 1909) was a Danish flutist, conductor and composer born in Copenhagen, son of the flutist Christian Joachim Andersen. </bento-tab> <bento-tab title="Niels Gade" :counter="7"> (22 February 1817 - 21 December 1890) was a Danish composer, conductor and violinist. He was the leading Danish Romantic composer of his day. </bento-tab> <bento-tab title="Rued Langgaard" :error-counter="3"> (28 July 1893 - 10 July 1952) was a Danish composer and organist. His works were largely neglected during his lifetime but have since been widely performed. </bento-tab> <bento-tab title="Ludvig Holstein" :disabled="true" :counter="2"> (14 December 1864 - 5 July 1943) was a Danish lyrical poet known for his nature poetry and neo-romantic style. </bento-tab> <bento-tab title="Inger Christensen" with-notification-indicator> (16 January 1935 - 2 January 2009) was a Danish poet and novelist, widely regarded as one of the most important Scandinavian writers of the 20th century. </bento-tab> </bento-tabs> `, setup(props) { return { args: isVue2 ? props : _args, }; }, }), parameters: storybookDocsParameter(BentoTabsEverythingBagelExample), };
@@ -1 +1 @@
1
- import type { Ref } from 'vue'; /** * Private */ export interface BentoStickyTabsOptions { top?: Ref<number>; } export type BentoTabsRegisterTab = (props: { counter: Ref<number>; disabled: Ref<boolean>; hidden: Ref<boolean>; title: Ref<string>; withNotificationIndicator: Ref<boolean>; setElementRef: (el: Element | null) => void; }) => { id: string; unregisterTab: () => void }; export enum BentoTabsEvent { ACTIVE_TAB_INDEX = 'update:active-tab-index', }
1
+ import type { Ref } from 'vue'; /** * Private */ export interface BentoStickyTabsOptions { top?: Ref<number>; } export type BentoTabsRegisterTab = (props: { counter: Ref<number>; disabled: Ref<boolean>; errorCounter: Ref<number>; hidden: Ref<boolean>; title: Ref<string>; withNotificationIndicator: Ref<boolean>; setElementRef: (el: Element | null) => void; }) => { id: string; unregisterTab: () => void }; export enum BentoTabsEvent { ACTIVE_TAB_INDEX = 'update:active-tab-index', }