@adyen/bento-mcp 0.5.0 → 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 (52) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +41 -9
  3. package/dist/assets/components/accordion/components/accordion-item.vue +1 -1
  4. package/dist/assets/components/card/card.vue +1 -1
  5. package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.vue +1 -1
  6. package/dist/assets/components/draggable/draggable.docs.mdx +76 -135
  7. package/dist/assets/components/draggable/draggable.stories.ts +1 -1
  8. package/dist/assets/components/draggable/draggable.types.ts +1 -1
  9. package/dist/assets/components/draggable/draggable.vue +1 -1
  10. package/dist/assets/components/drawer/drawer.docs.mdx +68 -0
  11. package/dist/assets/components/drawer/drawer.stories.ts +1 -0
  12. package/dist/assets/components/drawer/drawer.types.ts +1 -0
  13. package/dist/assets/components/drawer/drawer.vue +1 -0
  14. package/dist/assets/components/filter-bar/components/base-filter/base-filter.vue +1 -1
  15. package/dist/assets/components/input-field/input-field.types.ts +1 -1
  16. package/dist/assets/components/input-field/input-field.vue +1 -1
  17. package/dist/assets/components/internal/calendar/calendar.types.ts +1 -1
  18. package/dist/assets/components/internal/calendar/calendar.vue +1 -1
  19. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.stories.ts +1 -1
  20. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.types.ts +1 -1
  21. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.vue +1 -1
  22. package/dist/assets/components/internal/calendar/components/calendar-month/composables/granular-min-max-date.types.ts +1 -1
  23. package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.types.ts +1 -1
  24. package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.vue +1 -1
  25. package/dist/assets/components/internal/teleport/teleport.vue +1 -1
  26. package/dist/assets/components/modal/modal.vue +1 -1
  27. package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.stories.ts +1 -0
  28. package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.types.ts +1 -0
  29. package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.vue +1 -0
  30. package/dist/assets/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.stories.ts +1 -0
  31. package/dist/assets/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.types.ts +1 -0
  32. package/dist/assets/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.vue +1 -0
  33. package/dist/assets/components/navigation-menu/navigation-menu.docs.mdx +321 -0
  34. package/dist/assets/components/navigation-menu/navigation-menu.stories.ts +1 -0
  35. package/dist/assets/components/navigation-menu/navigation-menu.types.ts +1 -0
  36. package/dist/assets/components/navigation-menu/navigation-menu.vue +1 -0
  37. package/dist/assets/components/secondary-nav/components/secondary-nav-category/secondary-nav-category.vue +1 -1
  38. package/dist/assets/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue +1 -1
  39. package/dist/assets/components/secondary-nav/secondary-nav.stories.ts +1 -1
  40. package/dist/assets/components/secondary-nav/secondary-nav.types.ts +1 -1
  41. package/dist/assets/components/secondary-nav/secondary-nav.vue +1 -1
  42. package/dist/assets/components/tabs/components/tab.types.ts +1 -1
  43. package/dist/assets/components/tabs/components/tab.vue +1 -1
  44. package/dist/assets/components/tabs/tabs.stories.ts +1 -1
  45. package/dist/assets/components/tabs/tabs.types.ts +1 -1
  46. package/dist/assets/components/tabs/tabs.vue +1 -1
  47. package/dist/assets/components.json +5 -0
  48. package/dist/assets/index.ts +1 -1
  49. package/dist/assets/usage.json +13 -8
  50. package/dist/assets/variables.css +3 -0
  51. package/dist/main.js +1 -1
  52. package/package.json +1 -1
@@ -1 +1 @@
1
- import type { Meta, StoryObj } from '@storybook/vue'; import { type BentoDraggableItems, type BentoDraggableOptions, type BentoDraggableProps, type BentoDraggableSortableEvent, } from './draggable.types'; import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import { ref } from 'vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { BentoDraggableHandle } from './components/draggable-handle'; import { BentoTypography } from '../typography'; import { BentoButton } from '@/components/button'; import BinIcon from '@adyen/ui-assets-icons-16/vue/bin'; import BentoDraggable from './draggable.vue'; import BentoDraggableDefaultExample from './__tests__/bento-draggable-default-example.vue?raw'; import BentoDraggableNestedExample from './__tests__/bento-draggable-nested-example.vue?raw'; import BentoDraggableGridExample from './__tests__/bento-draggable-grid-example.vue?raw'; import './draggable.stories.scss?module'; const meta: Meta = { title: 'Draggable', component: BentoDraggable, }; export default meta; type Story = StoryObj<typeof BentoDraggable>; const ITEMS: BentoDraggableItems = [ { id: '1', label: 'Item 1' }, { id: '2', label: 'Item 2' }, { id: '3', label: 'Item 3' }, { id: '4', label: 'Item 4' }, { id: '5', label: 'Item 5' }, { id: '6', label: 'Item 6' }, ]; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDraggable, BentoDraggableHandle, BentoTypography }, props: Object.keys(argTypes), template: ` <bento-draggable v-bind="args" ref="draggableElementRef" :items="itemsList" class="b-draggable-default__container" :options="options" > <template #default="{item, index}"> <div class="b-draggable-default__item" :key="item.id"> <div :style="{ display: 'flex', alignItems: 'center'}"> <bento-draggable-handle :label="item.label" condensed :disabled="item.label === 'Item 2'" :disabled-menus="{ up: index === 0, top: index === 0, down: index === itemsList.length - 1, bottom: index === itemsList.length - 1, }" variant="up-and-down" @move-up="() => moveUp(index)" @move-down="() => moveDown(index)" @move-top="() => moveTop(index)" @move-bottom="() => moveBottom(index)" /> <bento-typography>{{ item.label }}</bento-typography> </div> </div> </template> </bento-draggable> `, setup(props) { const args = isVue2 ? props : _args; const itemsList = ref(args.items); const draggableElementRef = ref<typeof BentoDraggable>(null); const shiftList = (indexOrigin: number, indexTarget: number) => { if (indexOrigin === -1 || indexTarget === -1) { return Array.from(itemsList.value); } const newList = Array.from(itemsList.value as BentoDraggableItems); const [removed] = newList.splice(indexOrigin, 1); newList.splice(indexTarget, 0, removed); const itemLabel = removed.label; draggableElementRef.value.announce( `You've moved ${itemLabel} from position ${indexOrigin + 1} to position ${indexTarget + 1} of ${itemsList.value.length}.` ); return newList; }; const moveUp = (index: number) => { itemsList.value = shiftList(index, index - 1); action('move-up')(itemsList.value); }; const moveDown = (index: number) => { itemsList.value = shiftList(index, index + 1); action('move-down')(itemsList.value); }; const moveTop = (index: number) => { itemsList.value = shiftList(index, 0); action('move-top')(itemsList.value); }; const moveBottom = (index: number) => { itemsList.value = shiftList(index, itemsList.value.length - 1); action('move-bottom')(itemsList.value); }; const onSort = (event: BentoDraggableSortableEvent) => { itemsList.value = shiftList(event.oldIndex, event.newIndex); action('sort')(event, itemsList.value); }; const options: BentoDraggableOptions = { onAdd: action('add'), onChange: () => action('change'), onChoose: action('choose'), onClone: action('clone'), onEnd: action('end'), onFilter: action('filter'), onMove: action('move'), onRemove: action('remove'), onStart: action('start'), onSort, onUnchoose: action('unchoose'), onUpdate: action('update'), }; return { // Values args: isVue2 ? props : _args, itemsList, draggableElementRef, // Events options, moveUp, moveDown, moveTop, moveBottom, }; }, }), args: { items: ITEMS, options: { group: 'nested', animation: 150, multiDrag: true, } as BentoDraggableProps, }, parameters: storybookDocsParameter(BentoDraggableDefaultExample), }; const NESTED_ITEMS: BentoDraggableItems = [ { id: '1.1', label: 'Item 1.1', children: [ { id: '2.1', label: 'Item 2.1' }, { id: '2.2', label: 'Item 2.2', children: [ { id: '3.1', label: 'Item 3.1' }, { id: '3.2', label: 'Item 3.2' }, ], }, { id: '2.3', label: 'Item 2.3' }, ], }, { id: '1.2', label: 'Item 1.2' }, { id: '1.3', label: 'Item 1.3' }, ]; export const Nested: Story = { render: (_args, { argTypes }) => ({ components: { BentoDraggable, BentoDraggableHandle, BentoTypography }, props: Object.keys(argTypes), template: ` <bento-draggable v-bind="args" class="b-draggable-default__container" v-on="listeners" > <template #default="{item, index}"> <div class="b-draggable-default__item" :key="item.id"> <div :style="{ display: 'flex', alignItems: 'center'}"> <bento-draggable-handle :label="item.label" condensed variant="up-and-down" /> <bento-typography>{{ item.label }}</bento-typography> </div> <!-- Children --> <template v-if="item.children"> <bento-draggable class="b-draggable-default__container" :options="args.options" :items="item.children" v-on="listeners" > <template v-slot:default="{item: childItem, index: childIndex}"> <div class="b-draggable-default__item" :key="childItem.id"> <div :style="{ display: 'flex', alignItems: 'center'}"> <bento-draggable-handle :label="childItem.label" condensed variant="up-and-down" /> <bento-typography>{{ childItem.label }}</bento-typography> </div> </div> </template> </bento-draggable> </template> </div> </template> </bento-draggable> `, setup(props) { const clickedAction = (eventName: string) => (event: BentoDraggableSortableEvent) => action(eventName)(event); const listeners = { add: clickedAction('add'), change: clickedAction('change'), choose: clickedAction('choose'), clone: clickedAction('clone'), end: clickedAction('end'), filter: clickedAction('filter'), move: clickedAction('move'), remove: clickedAction('remove'), start: clickedAction('start'), sort: clickedAction('sort'), unchoose: clickedAction('unchoose'), update: clickedAction('update'), }; return { // Values args: isVue2 ? props : _args, // Events listeners, }; }, }), args: { items: NESTED_ITEMS, options: { group: 'nested', animation: 150, }, } as BentoDraggableProps, parameters: storybookDocsParameter(BentoDraggableNestedExample), }; export const Grid: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoDraggable, BentoDraggableHandle, BentoTypography, BinIcon }, props: Object.keys(argTypes), template: ` <bento-draggable v-bind="args" :style="{ padding: 'var(--b-spacer-050)', width: '670px', display: 'flex', flexDirection: 'row', flexWrap: 'wrap', gap: 'var(--b-spacer-050)', }" v-on="listeners" > <template #default="{item, index}"> <div :key="item.id" :style="{ width: '160px' }"> <div :style="{ display: 'flex', background: 'var(--b-color-background-tertiary)', borderTopLeftRadius: 'var(--b-border-radius-m)', borderTopRightRadius: 'var(--b-border-radius-m)', justifyContent: 'space-between', padding: 'var(--b-spacer-010) var(--b-spacer-020)', }" > <bento-draggable-handle :label="item.label" :disabled-menus="{ up: index <= 3, top: index <= 3, down: index > 3, bottom: index > 3, left: index === 0 || index === 4, right: index === 3 || index === 5, }" condensed variant="all-directions" /> <bento-button condensed variant="secondary"> <template #iconLeft> <bin-icon svg-title="remove" /> </template> </bento-button> </div> <div :style="{ backgroundColor: 'var(--b-color-background-secondary)', display: 'flex', flexDirection: 'column', borderBottomLeftRadius: 'var(--b-border-radius-m)', borderBottomRightRadius: 'var(--b-border-radius-m)', gap: 'var(--b-spacer-060)', padding: 'var(--b-spacer-040)', }" > <bento-typography>Card {{ index + 1 }}</bento-typography> </div> </div> </template> </bento-draggable> `, setup(props) { const clickedAction = (eventName: string) => (event: BentoDraggableSortableEvent) => action(eventName)(event); const listeners = { add: clickedAction('add'), change: clickedAction('change'), choose: clickedAction('choose'), clone: clickedAction('clone'), end: clickedAction('end'), filter: clickedAction('filter'), move: clickedAction('move'), remove: clickedAction('remove'), start: clickedAction('start'), sort: clickedAction('sort'), unchoose: clickedAction('unchoose'), update: clickedAction('update'), }; return { // Values args: isVue2 ? props : _args, // Events listeners, }; }, }), args: { items: ITEMS, options: { group: 'nested', animation: 150, } as BentoDraggableProps, }, parameters: storybookDocsParameter(BentoDraggableGridExample), };
1
+ import type { Meta, StoryObj } from '@storybook/vue'; import { type BentoDraggableItems, type BentoDraggableOptions, type BentoDraggableProps } from './draggable.types'; import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import { ref } from 'vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { BentoDraggableHandle } from './components/draggable-handle'; import { BentoTypography } from '../typography'; import { BentoButton } from '@/components/button'; import BinIcon from '@adyen/ui-assets-icons-16/vue/bin'; import BentoDraggable from './draggable.vue'; import BentoDraggableDefaultExample from './__tests__/bento-draggable-default-example.vue?raw'; import BentoDraggableNestedExample from './__tests__/bento-draggable-nested-example.vue?raw'; import BentoDraggableGridExample from './__tests__/bento-draggable-grid-example.vue?raw'; import BentoDraggableBoardExample from './__tests__/bento-draggable-board-example.vue?raw'; import './draggable.stories.scss?module'; const meta: Meta = { title: 'Draggable', component: BentoDraggable, }; export default meta; type Story = StoryObj<typeof BentoDraggable>; const ITEMS: BentoDraggableItems = [ { id: '1', label: 'Item 1' }, { id: '2', label: 'Item 2' }, { id: '3', label: 'Item 3' }, { id: '4', label: 'Item 4' }, { id: '5', label: 'Item 5' }, { id: '6', label: 'Item 6' }, ]; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDraggable, BentoDraggableHandle, BentoTypography }, props: Object.keys(argTypes), template: ` <bento-draggable v-bind="args" :items="itemsList" class="b-draggable-default__container" @update:items="onUpdateItems" > <template #default="{item, moveUp, moveDown, moveTop, moveBottom, disabledMenus}"> <div class="b-draggable-default__item" :key="item.id"> <div :style="{ display: 'flex', alignItems: 'center'}"> <bento-draggable-handle :label="item.label" condensed :disabled="item.label === 'Item 2'" :disabled-menus="disabledMenus" variant="up-and-down" @move-up="moveUp" @move-down="moveDown" @move-top="moveTop" @move-bottom="moveBottom" /> <bento-typography>{{ item.label }}</bento-typography> </div> </div> </template> </bento-draggable> `, setup(props) { const args = isVue2 ? props : _args; const itemsList = ref(args.items); const onUpdateItems = (items: BentoDraggableItems) => { itemsList.value = [...items]; action('update:items')(items); }; return { args: isVue2 ? props : _args, itemsList, onUpdateItems, }; }, }), args: { items: ITEMS, options: { group: 'default', }, }, parameters: storybookDocsParameter(BentoDraggableDefaultExample), }; const NESTED_ITEMS: BentoDraggableItems = [ { id: '1.1', label: 'Item 1.1', children: [ { id: '2.1', label: 'Item 2.1' }, { id: '2.2', label: 'Item 2.2', children: [ { id: '3.1', label: 'Item 3.1' }, { id: '3.2', label: 'Item 3.2' }, ], }, { id: '2.3', label: 'Item 2.3' }, ], }, { id: '1.2', label: 'Item 1.2' }, { id: '1.3', label: 'Item 1.3' }, ]; export const Nested: Story = { render: (_args, { argTypes }) => ({ components: { BentoDraggable, BentoDraggableHandle, BentoTypography }, props: Object.keys(argTypes), template: ` <bento-draggable v-bind="args" :items="itemsList" class="b-draggable-nested__container" @update:items="itemsList = $event" > <template #default="{item, moveUp, moveDown, moveTop, moveBottom, disabledMenus}"> <div class="b-draggable-nested__item" :key="item.id"> <div :style="{ display: 'flex', alignItems: 'center'}"> <bento-draggable-handle :label="item.label" condensed variant="up-and-down" :disabled-menus="disabledMenus" @move-up="moveUp" @move-down="moveDown" @move-top="moveTop" @move-bottom="moveBottom" /> <bento-typography>{{ item.label }}</bento-typography> </div> <!-- Children --> <template v-if="item.children"> <bento-draggable class="b-draggable-nested__container" :options="args.options" :items="item.children" @update:items="item.children = $event" > <template v-slot:default="{item: childItem, moveUp: moveUpChild, moveDown: moveDownChild, moveTop: moveTopChild, moveBottom: moveBottomChild, disabledMenus: disabledMenusChild}"> <div class="b-draggable-nested__item" :key="childItem.id"> <div :style="{ display: 'flex', alignItems: 'center'}"> <bento-draggable-handle :label="childItem.label" condensed variant="up-and-down" :disabled-menus="disabledMenusChild" @move-up="moveUpChild" @move-down="moveDownChild" @move-top="moveTopChild" @move-bottom="moveBottomChild" /> <bento-typography>{{ childItem.label }}</bento-typography> </div> </div> </template> </bento-draggable> </template> </div> </template> </bento-draggable> `, setup(props) { const args = isVue2 ? props : _args; const itemsList = ref(args.items); return { args, itemsList, }; }, }), args: { items: NESTED_ITEMS, options: {}, } as BentoDraggableProps, parameters: storybookDocsParameter(BentoDraggableNestedExample), }; export const Grid: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoDraggable, BentoDraggableHandle, BentoTypography, BinIcon }, props: Object.keys(argTypes), template: ` <bento-draggable ref="draggableRef" v-bind="args" :items="itemsList" @update:items="itemsList = $event" :style="{ padding: 'var(--b-spacer-050)', width: '670px', display: 'flex', flexDirection: 'row', flexWrap: 'wrap', gap: 'var(--b-spacer-050)', }" > <template #default="{item, index, moveTop, moveBottom, moveLeft, moveRight, disabledMenus}"> <div :key="item.id" :style="{ width: '160px' }"> <div :style="{ display: 'flex', background: 'var(--b-color-background-tertiary)', borderTopLeftRadius: 'var(--b-border-radius-m)', borderTopRightRadius: 'var(--b-border-radius-m)', justifyContent: 'space-between', padding: 'var(--b-spacer-010) var(--b-spacer-020)', }" > <bento-draggable-handle :label="item.label" :disabled-menus="{ ...disabledMenus, up: index < COLUMNS, down: index + COLUMNS >= itemsList.length, left: index % COLUMNS === 0, right: index % COLUMNS === COLUMNS - 1 || index === itemsList.length - 1, }" condensed variant="all-directions" @move-up="gridMoveUp(index)" @move-down="gridMoveDown(index)" @move-top="moveTop" @move-bottom="moveBottom" @move-left="moveLeft" @move-right="moveRight" /> <bento-button condensed variant="secondary"> <template #iconLeft> <bin-icon svg-title="remove" /> </template> </bento-button> </div> <div :style="{ backgroundColor: 'var(--b-color-background-secondary)', display: 'flex', flexDirection: 'column', borderBottomLeftRadius: 'var(--b-border-radius-m)', borderBottomRightRadius: 'var(--b-border-radius-m)', gap: 'var(--b-spacer-060)', padding: 'var(--b-spacer-040)', }" > <bento-typography>Card {{ item.id }}</bento-typography> </div> </div> </template> </bento-draggable> `, setup(props) { const args = isVue2 ? props : _args; const itemsList = ref(args.items); const draggableRef = ref(null); const COLUMNS = 4; const gridMoveUp = (index: number) => { draggableRef.value?.moveItem(index, index - COLUMNS); }; const gridMoveDown = (index: number) => { draggableRef.value?.moveItem(index, index + COLUMNS); }; return { args, itemsList, draggableRef, COLUMNS, gridMoveUp, gridMoveDown, }; }, }), args: { items: ITEMS, options: {}, }, parameters: storybookDocsParameter(BentoDraggableGridExample), }; export const Board: Story = { render: (_args, { argTypes }) => ({ components: { BentoDraggable, BentoDraggableHandle, BentoTypography }, props: Object.keys(argTypes), template: ` <div :style="{ display: 'flex', gap: 'var(--b-spacer-100)', alignItems: 'flex-start', padding: 'var(--b-spacer-080)', backgroundColor: 'var(--b-color-background-tertiary)', minHeight: '400px' }"> <!-- TODO Column --> <div :style="{ flex: 1, background: 'var(--b-color-background-secondary)', padding: 'var(--b-spacer-040)', borderRadius: 'var(--b-border-radius-m)', boxShadow: 'var(--b-shadow-small)' }"> <bento-typography variant="title" :style="{ marginBottom: 'var(--b-spacer-040)', padding: 'var(--b-spacer-020) var(--b-spacer-040)', display: 'block' }"> TODO </bento-typography> <bento-draggable ref="todoDraggableRef" :items="todoItems" :options="toDoOptions" @update:items="todoItems = $event" class="b-draggable-board__container" style="min-height: 50px;" > <template #default="{ item, index, moveUp, moveDown, moveTop, moveBottom, disabledMenus }"> <div class="b-draggable-board__item" :key="item.id"> <div :style="{ display: 'flex', alignItems: 'center' }"> <bento-draggable-handle :label="item.label" condensed variant="all-directions" :disabled-menus="{ ...disabledMenus, right: false, left: true, }" @move-up="moveUp" @move-down="moveDown" @move-top="moveTop" @move-bottom="moveBottom" @move-right="moveToDone(index)" /> <bento-typography>{{ item.label }}</bento-typography> </div> </div> </template> </bento-draggable> </div> <!-- DONE Column --> <div :style="{ flex: 1, background: 'var(--b-color-background-secondary)', padding: 'var(--b-spacer-040)', borderRadius: 'var(--b-border-radius-m)', boxShadow: 'var(--b-shadow-small)' }"> <bento-typography variant="title" :style="{ marginBottom: 'var(--b-spacer-040)', padding: 'var(--b-spacer-020) var(--b-spacer-040)', display: 'block' }"> DONE </bento-typography> <bento-draggable ref="doneDraggableRef" :items="doneItems" :options="doneOptions" @update:items="doneItems = $event" class="b-draggable-board__container" style="min-height: 50px;" > <template #default="{ item, index, moveUp, moveDown, moveTop, moveBottom, moveLeft, disabledMenus }"> <div class="b-draggable-board__item" :key="item.id"> <div :style="{ display: 'flex', alignItems: 'center' }"> <bento-draggable-handle :label="item.label" condensed variant="all-directions" :disabled-menus="{ ...disabledMenus, left: false, }" @move-up="moveUp" @move-down="moveDown" @move-top="moveTop" @move-bottom="moveBottom" @move-left="moveToTodo(index)" /> <bento-typography>{{ item.label }}</bento-typography> </div> </div> </template> </bento-draggable> </div> </div> `, setup() { const todoItems = ref<BentoDraggableItems>([ { id: '1', label: 'Improve documentation' }, { id: '2', label: 'Fix navigation bug' }, { id: '3', label: 'Add unit tests' }, ]); const doneItems = ref<BentoDraggableItems>([{ id: '4', label: 'Initial release' }]); const todoDraggableRef = ref<InstanceType<typeof BentoDraggable> | null>(null); const doneDraggableRef = ref<InstanceType<typeof BentoDraggable> | null>(null); // Since we're moving items between completely separate refs, we track the item being dragged // manually to facilitate transfers between the todoItems and doneItems lists. let draggedItem: BentoDraggableItems[number] | null = null; const moveToDone = (index: number) => { const [item] = todoItems.value.splice(index, 1); doneItems.value.push(item); doneDraggableRef.value?.announce( `${item.label} moved to Done, position ${doneItems.value.length} of ${doneItems.value.length}` ); action('move-to-done')(item, { todo: todoItems.value, done: doneItems.value }); }; const moveToTodo = (index: number) => { const [item] = doneItems.value.splice(index, 1); todoItems.value.push(item); todoDraggableRef.value?.announce( `${item.label} moved to Todo, position ${todoItems.value.length} of ${todoItems.value.length}` ); action('move-to-todo')(item, { todo: todoItems.value, done: doneItems.value }); }; const toDoOptions: BentoDraggableOptions = { group: 'board', animation: 150, onStart: event => { draggedItem = todoItems.value[event.oldIndex!]; }, onAdd: event => { todoItems.value.splice(event.newIndex!, 0, draggedItem!); todoDraggableRef.value?.announce( `${draggedItem!.label} added to Todo, position ${event.newIndex! + 1} of ${todoItems.value.length}` ); }, onRemove: event => { todoItems.value.splice(event.oldIndex!, 1); }, onSort: event => { const { oldIndex, newIndex, from, to } = event; if (from === to && oldIndex !== undefined && newIndex !== undefined) { const [item] = todoItems.value.splice(oldIndex, 1); todoItems.value.splice(newIndex, 0, item); todoDraggableRef.value?.announce( `${item.label} moved to position ${newIndex + 1} of ${todoItems.value.length} in Todo` ); } }, }; const doneOptions: BentoDraggableOptions = { group: 'board', animation: 150, onStart: event => { draggedItem = doneItems.value[event.oldIndex!]; }, onAdd: event => { doneItems.value.splice(event.newIndex!, 0, draggedItem!); doneDraggableRef.value?.announce( `${draggedItem!.label} added to Done, position ${event.newIndex! + 1} of ${doneItems.value.length}` ); }, onRemove: event => { doneItems.value.splice(event.oldIndex!, 1); }, onSort: event => { const { oldIndex, newIndex, from, to } = event; if (from === to && oldIndex !== undefined && newIndex !== undefined) { const [item] = doneItems.value.splice(oldIndex, 1); doneItems.value.splice(newIndex, 0, item); doneDraggableRef.value?.announce( `${item.label} moved to position ${newIndex + 1} of ${doneItems.value.length} in Done` ); action('sort-done')(event, doneItems.value); } }, }; return { todoItems, doneItems, toDoOptions, doneOptions, moveToDone, moveToTodo, todoDraggableRef, doneDraggableRef, }; }, }), parameters: storybookDocsParameter(BentoDraggableBoardExample), };
@@ -1 +1 @@
1
- import { type SortableOptions } from 'sortablejs'; import { type MoveEvent, type SortableEvent } from 'sortablejs'; /** * Event instances generated by each of the events * emitted by the draggable component. */ export type BentoDraggableSortableEvent = SortableEvent; /** * Event emitted by the draggable move event */ export type BentoDraggableMoveEvent = MoveEvent; /** * Options to be passed to SortableJS. * @see {@link https://github.com/SortableJS/Sortable#readme | SortableJS documentation} for all the details of the options available */ export type BentoDraggableOptions = Omit<SortableOptions, 'handle' | 'filter'>; /** * Events to hook on to the draggable component through the options */ export type BentoDraggableEvents = Pick< BentoDraggableOptions, | 'onUnchoose' | 'onChoose' | 'onStart' | 'onEnd' | 'onAdd' | 'onUpdate' | 'onSort' | 'onRemove' | 'onFilter' | 'onMove' | 'onClone' | 'onChange' >; /** * Item data of the items to be rendered as draggable elements */ export interface BentoDraggableItem { label: string; [x: string]: any; children?: Array<BentoDraggableItem>; } /** * List of items to be rendered as draggable elements */ export type BentoDraggableItems = Array<BentoDraggableItem>; export interface BentoDraggableProps { /** * Element that will contain the draggable elements. * @default 'div' */ el?: keyof HTMLElementTagNameMap; /** * List of items to be rendered as draggable elements * @default [] */ items?: Array<BentoDraggableItem>; /** * Options to be passed to SortableJS. * @see {@link https://github.com/SortableJS/Sortable#readme | SortableJS documentation} for all the details of the options available * @default {} */ options?: BentoDraggableOptions; }
1
+ import { type SortableOptions } from 'sortablejs'; import { type MoveEvent, type SortableEvent } from 'sortablejs'; /** * Event instances generated by each of the events * emitted by the draggable component. */ export type BentoDraggableSortableEvent = SortableEvent; /** * Event emitted by the draggable move event */ export type BentoDraggableMoveEvent = MoveEvent; /** * Options to be passed to SortableJS. * @see {@link https://github.com/SortableJS/Sortable#readme | SortableJS documentation} for all the details of the options available */ export type BentoDraggableOptions = Omit<SortableOptions, 'handle' | 'filter'>; /** * Events to hook on to the draggable component through the options */ export type BentoDraggableEvents = Pick< BentoDraggableOptions, | 'onUnchoose' | 'onChoose' | 'onStart' | 'onEnd' | 'onAdd' | 'onUpdate' | 'onSort' | 'onRemove' | 'onFilter' | 'onMove' | 'onClone' | 'onChange' >; /** * Item data of the items to be rendered as draggable elements */ export interface BentoDraggableItem { label: string; [x: string]: any; children?: Array<BentoDraggableItem>; } /** * List of items to be rendered as draggable elements */ export type BentoDraggableItems = Array<BentoDraggableItem>; /** * Events emitted by the draggable component */ export interface BentoDraggableEmits { /** * Emitted when the items list is updated */ (e: 'update:items', items: BentoDraggableItems): void; } export interface BentoDraggableProps { /** * Custom announcement message override. When set, this value is used instead of the * default reorder announcement. Set to `'disabled'` to suppress announcements entirely. */ announcement?: string | 'disabled'; /** * When enabled, the component will not automatically reorder items on sort. * Use this when providing a custom `onSort` callback via options. * @default false */ disableAutoSort?: boolean; /** * Element that will contain the draggable elements. * @default 'div' */ el?: keyof HTMLElementTagNameMap; /** * List of items to be rendered as draggable elements * @default [] */ items?: Array<BentoDraggableItem>; /** * Options to be passed to SortableJS. * @see {@link https://github.com/SortableJS/Sortable#readme | SortableJS documentation} for all the details of the options available * @default {} */ options?: BentoDraggableOptions; }
@@ -1 +1 @@
1
- <template> <component :is="el" ref="containerRef"> <slot v-for="(item, index) of items" :item="item" :index="index" name="default"></slot> </component> </template> <script setup lang="ts"> import { computed, onUnmounted, ref } from 'vue'; import type { BentoDraggableOptions, BentoDraggableProps, BentoDraggableSortableEvent } from './draggable.types'; import { useBentoDraggable } from './composables/use-bento-draggable/use-bento-draggable'; import { announce, cleanup } from './components/live-region/live-region'; const props = withDefaults(defineProps<BentoDraggableProps>(), { el: 'div', items: () => [], options: () => ({}), }); /** * Reference to the container of the draggable elements. */ const containerRef = ref<HTMLElement>(null); /** * Indicates if there is an element being dragged inside the container. */ const isDragging = ref<boolean>(false); /** * Computed options to be passed to SortableJS */ const computedOptions = computed<BentoDraggableOptions>(() => { if (!containerRef.value) { return {}; } return { ...props.options, onStart: (event: BentoDraggableSortableEvent) => { isDragging.value = true; if (props.options?.onStart) { props.options?.onStart(event); } }, onEnd: (event: BentoDraggableSortableEvent) => { // This is a hack to move the event to the end of the event queue. // cf this issue: https://github.com/SortableJS/Sortable/issues/1184 setTimeout(() => { isDragging.value = false; if (props.options?.onEnd) { props.options?.onEnd(event); } }); }, }; }); /** * Generate the draggable instance with SortableJS */ const { draggableElement } = useBentoDraggable(containerRef, computedOptions); onUnmounted(() => { // Remove the a11y live-region cleanup(); }); defineExpose({ /** * A ref to the container DOM element. */ containerRef, /** * The underlying SortableJS instance, which provides methods like `toArray()`, `sort()`, and `destroy()`. */ draggableElement, /** * A boolean ref that is `true` when an item is being dragged. */ isDragging, /** * Used to announce reordering or movement actions to screen reader users. */ announce, }); </script> <script lang="ts"> /** * Drag and drop container that transforms a list of items into draggable elements. * Th library is based on SortableJS. * @see {@link https://github.com/SortableJS/Sortable#readme | SortableJS documentation} * * @example * import { BentoDraggable, BentoDraggableHandle, type BentoDraggableEvents } from '@adyen/bento-vue2'; * * export default { * components: { BentoDraggable, BentoDraggableHandle }, * template: ` * <bento-draggable * :items="[ * { id: '1', label: 'Item 1' }, * { id: '2', label: 'Item 2' } * ]" * condensed * :options="{ * group: 'nested', * animation: 150, * ...listeners * }" * > * <template #default="{ item, index }"> * <div :key="item.id"> * <bento-draggable-handle :label="item.label" /> * </div> * </template> * </bento-draggable> * `, * setup () { * const listeners: BentoDraggableEvents = { * onAdd: () => {}, * onChange: () => {}, * onChoose: () => {}, * onClone: () => {}, * onEnd: () => {}, * onFilter: () => {}, * onMove: () => {}, * onRemove: () => {}, * onStart: () => {}, * onSort: () => {}, * onUnchoose: () => {}, * onUpdate: () => {}, * }; * return { * listeners, * } * } * } */ export default { name: 'bento-draggable', }; </script>
1
+ <template> <component :is="el" ref="containerRef"> <slot v-for="(item, index) of items" :item="item" :index="index" :move-up="() => moveItemUp(index)" :move-right="() => moveItemRight(index)" :move-down="() => moveItemDown(index)" :move-left="() => moveItemLeft(index)" :move-top="() => moveItemTop(index)" :move-bottom="() => moveItemBottom(index)" :disabled-menus="disabledMenuItems(index)" name="default" ></slot> <div class="b-draggable__announcement" role="status" aria-live="polite"> {{ announcementMessage }} </div> </component> </template> <script setup lang="ts"> import { computed, nextTick, ref, watch } from 'vue'; import type { BentoDraggableEmits, BentoDraggableItems, BentoDraggableOptions, BentoDraggableProps, BentoDraggableSortableEvent, } from './draggable.types'; import { useBentoDraggable } from './composables/use-bento-draggable/use-bento-draggable'; import { useDraggableAnnouncement } from './composables/use-draggable-announcement'; import { useTrackKeyboardUser } from './composables/use-track-keyboard-user'; import { useI18n } from '@/utils/ts/i18n'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoDraggableProps>(), { el: 'div', items: () => [], options: () => ({}), announcement: '', disableAutoSort: false, }); const emit = defineEmits<BentoDraggableEmits>(); /** * Reference to the container of the draggable elements. */ const containerRef = ref<HTMLElement | null>(null); /** * Indicates if there is an element being dragged inside the container. */ const isDragging = ref<boolean>(false); /** * Internal copy of items used to track position changes for focus restoration. */ const internalItems = ref<BentoDraggableItems>([]); /** * Tracks the item being moved via keyboard so focus can be restored to the correct element. */ const lastMovedItem = ref<BentoDraggableItems[number] | null>(null); const computedOptions = computed<BentoDraggableOptions>(() => { if (!containerRef.value) { return {}; } return { ...props.options, onStart: (event: BentoDraggableSortableEvent) => { isDragging.value = true; if (props.options?.onStart) { props.options?.onStart(event); } }, onEnd: (event: BentoDraggableSortableEvent) => { // This is a hack to move the event to the end of the event queue. // cf this issue: https://github.com/SortableJS/Sortable/issues/1184 setTimeout(() => { isDragging.value = false; if (props.options?.onEnd) { props.options?.onEnd(event); } }); }, onSort: (event: BentoDraggableSortableEvent) => { // Always execute the user's custom onSort callback if provided if (props.options?.onSort) { props.options.onSort(event); return; } // Automatically sync the v-model unless the user explicitly disabled it if (!props.disableAutoSort) { const { oldIndex, newIndex, from, to } = event; if ( from === to && from === containerRef.value && oldIndex !== undefined && newIndex !== undefined ) { moveItem(oldIndex, newIndex); } } }, }; }); /** * Handle reordering announcements for screen readers. */ const { announcementMessage, announce } = useDraggableAnnouncement(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const { draggableElement } = useBentoDraggable(containerRef, computedOptions); // When moving items between sibling `bento-draggable`, the item may be removed, causing the DOM to be destroyed and temporarily lose focus // This is an alternative to query element `:focus-within` as the `:focus-within` will be gone until the other `bento-draggable` adds it into the list const { isKeyboardUser } = useTrackKeyboardUser(); /** * Focuses the draggable handle button at the given index. * Waits for the next tick to ensure the DOM has been updated before focusing. * * @param index - The index of the item whose handle should receive focus. */ const focus = async (index: number) => { await nextTick(); if (index !== -1 && containerRef.value) { const itemElement = containerRef.value.children[index] as HTMLElement; const button = itemElement?.querySelector<HTMLButtonElement>('.b-draggable-handle button'); await nextTick(); button?.focus(); } }; /** * Moves an item from one position to another within the list, * emits the updated list, and announces the change for screen readers. * * @param indexOrigin - The current index of the item to move. * @param indexTarget - The desired index to move the item to. */ const moveItem = async (indexOrigin: number, indexTarget: number) => { if ( indexOrigin < 0 || indexTarget < 0 || indexOrigin >= props.items.length || indexTarget >= props.items.length || indexOrigin === indexTarget ) { return; } const newList = Array.from(props.items as BentoDraggableItems); const [removed] = newList.splice(indexOrigin, 1); newList.splice(indexTarget, 0, removed); lastMovedItem.value = removed; // Occasionally, there may be race conditions where the index is out-of-date due to the fast drag-and-drop or // other async operations like onEnd > setTimeout. // Filtering out null/undefined values ensures the list remains valid. emit('update:items', newList.filter(Boolean)); const itemLabel = removed.label; const message = props?.announcement || t('youHaveMovedFromPositionToPosition', { itemLabel, indexOrigin: indexOrigin + 1, indexTarget: indexTarget + 1, total: props.items.length, }); if (props.announcement !== 'disabled') { announce(message); } }; const moveItemUp = (index: number) => moveItem(index, index - 1); const moveItemDown = (index: number) => moveItem(index, index + 1); const moveItemLeft = (index: number) => moveItem(index, index - 1); const moveItemRight = (index: number) => moveItem(index, index + 1); const moveItemTop = (index: number) => moveItem(index, 0); const moveItemBottom = (index: number) => moveItem(index, props.items.length - 1); /** * Returns a pre-calculated disabled menus object for the current item. */ const disabledMenuItems = (index: number) => { return { up: index === 0, top: index === 0, down: index === props.items.length - 1, bottom: index === props.items.length - 1, left: true, right: true, }; }; /** * Watch for items changes and restore focus automatically */ watch( () => props.items, async newItems => { if (!newItems) { return; } if (isKeyboardUser.value && newItems.length > 0 && internalItems.value.length > 0) { if (lastMovedItem.value) { // Within-list reorder: focus the moved item at its new position const movedItemIndex = newItems.indexOf(lastMovedItem.value); if (movedItemIndex !== -1) { await focus(movedItemIndex); } lastMovedItem.value = null; } else if (newItems.length > internalItems.value.length) { // Cross-list addition: focus the newly added item const newItemIndex = newItems.findIndex(item => !internalItems.value.includes(item)); if (newItemIndex !== -1) { await focus(newItemIndex); } } } internalItems.value = [...newItems]; }, { deep: true, immediate: true } ); defineExpose({ /** * A ref to the container DOM element. */ containerRef, /** * The underlying SortableJS instance, which provides methods like `toArray()`, `sort()`, and `destroy()`. */ draggableElement, /** * A boolean ref that is `true` when an item is being dragged. */ isDragging, /** * Announces a message to screen reader users via the live region. */ announce, /** * Moves an item from one index to another and emits the updated list. */ moveItem, /** * Programmatically focuses the draggable handle button at the given index. */ focus, }); </script> <script lang="ts"> /** * Drag and drop container that transforms a list of items into draggable elements. * The library is based on SortableJS. * @see {@link https://github.com/SortableJS/Sortable#readme | SortableJS documentation} * * @example * import { BentoDraggable, BentoDraggableHandle, type BentoDraggableEvents } from '@adyen/bento-vue2'; * * export default { * components: { BentoDraggable, BentoDraggableHandle }, * template: ` * <bento-draggable * v-model="items" * condensed * :options="{ * group: 'nested', * animation: 150 * }" * > * <template #default="{ item, index, moveUp, moveDown }"> * <div :key="item.id"> * <bento-draggable-handle * :label="item.label" * @move-up="moveUp" * @move-down="moveDown" * /> * </div> * </template> * </bento-draggable> * `, * setup () { * const items = ref([ * { id: '1', label: 'Item 1' }, * { id: '2', label: 'Item 2' } * ]); * return { * items, * } * } * } */ export default { name: 'bento-draggable', model: { prop: 'items', event: 'update:items' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./draggable.scss" />
@@ -0,0 +1,68 @@
1
+ import { Canvas, Meta, Story } from '@storybook/blocks';
2
+ import * as DrawerStories from './drawer.stories';
3
+
4
+ <Meta of={DrawerStories} />
5
+
6
+ # Drawer
7
+
8
+ The Drawer component provides a responsive drawer-based navigation system for your application. The drawer can be
9
+ toggled open/closed and automatically adapts to different screen sizes with overlay and full-width modes on mobile
10
+ devices.
11
+
12
+ <Canvas of={DrawerStories.Default} />
13
+
14
+ ## Use Cases
15
+
16
+ - Use for primary application navigation with a collapsible side drawer
17
+ - Use when you need a responsive navigation that works across desktop, tablet, and mobile
18
+ - Use when you want to maximize content space by hiding navigation when not needed
19
+
20
+ ### Do not use
21
+
22
+ - Don't use for simple top navigation bars
23
+ - Don't use for secondary or contextual navigation
24
+
25
+ ## Best practices
26
+
27
+ - Always provide a unique `drawerId` for accessibility. This should be the same as the `aria-controls` attribute on the
28
+ toggle button.
29
+ - Provide a descriptive `drawerLabel` for screen readers
30
+ - Use a controlled component pattern - manage `isDrawerOpen` state in the parent
31
+ - Include a toggle button with proper ARIA attributes (`aria-controls`, `aria-expanded`)
32
+ - Keep navigation menu items organized and scannable
33
+ - Consider using sections with headers to group related navigation items
34
+
35
+ ## Properties
36
+
37
+ | Property | Type | Default | Description |
38
+ | -------------- | --------- | ------- | -------------------------------------------------------------------------------------- |
39
+ | `drawerId` | `string` | — | **Required.** Unique identifier for the drawer, used for ARIA attributes. |
40
+ | `isDrawerOpen` | `boolean` | `false` | Controls whether the drawer is visible. |
41
+ | `drawerLabel` | `string` | — | Accessible label for the drawer. Falls back to localized "Navigation" if not provided. |
42
+
43
+ ## Slots
44
+
45
+ - `menu`: Content for the drawer (menu items, links, sections)
46
+ - `page`: Main page content that appears alongside or behind the drawer
47
+
48
+ ## Responsive Behavior
49
+
50
+ The Drawer component automatically adapts to different screen sizes:
51
+
52
+ - **Desktop (> 1024px)**: Drawer appears side-by-side with page content
53
+ - **Tablet (768px - 1024px)**: Drawer overlays the page content when open
54
+ - **Mobile (< 768px)**: Drawer takes full width and overlays the page content when open
55
+
56
+ ## Accessibility
57
+
58
+ The Drawer component is built with accessibility in mind:
59
+
60
+ - Uses semantic `<nav>` element with proper ARIA labeling
61
+ - Supports `aria-label` via the `drawerLabel` prop
62
+ - Toggle button should use `aria-controls` pointing to `drawerId`
63
+ - Toggle button should use `aria-expanded` to indicate drawer state
64
+ - Drawer uses `v-show` to maintain DOM presence for screen readers
65
+
66
+ ## Resources
67
+
68
+ - [Figma link](https://www.figma.com/design/uLabwF3243jdMDsNSP7U9I/Bento---Components)
@@ -0,0 +1 @@
1
+ import { isVue2 } from 'vue-demi'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoDrawer from './drawer.vue'; import { BentoButton } from '@/components/button'; import { BentoTypography } from '@/components/typography'; import { ref, watch } from 'vue'; import type { Meta, StoryObj } from '@storybook/vue'; import DrawerDefaultExample from './__tests__/drawer-default-example.vue?raw'; const meta: Meta = { title: 'Drawer', component: BentoDrawer, parameters: { layout: 'fullscreen', }, }; export default meta; type Story = StoryObj<typeof BentoDrawer>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDrawer, BentoButton, BentoTypography, }, props: Object.keys(argTypes), template: ` <div :style="{ display: 'flex', flexDirection: 'column', height: '100vh' }"> <div :style="{ borderBottom: '1px solid var(--b-color-separator-primary)', padding: 'var(--b-spacer-060)' }"> <bento-button variant="secondary" condensed :aria-controls="args.drawerId" aria-label="toggle drawer" :aria-expanded="String(isOpen)" @click="isOpen = !isOpen" > Toggle </bento-button> </div> <bento-drawer :drawer-id="args.drawerId" :drawer-label="args.drawerLabel" :is-drawer-open="isOpen" :style="{ flexGrow: '1', minHeight: '0' }" > <template #drawer> <div :style="{ padding: 'var(--b-spacer-080)' }"> <bento-typography variant="body">Drawer content</bento-typography> </div> </template> <template #page> <div :style="{ padding: 'var(--b-spacer-080)' }"> <bento-typography variant="body">Page content</bento-typography> </div> </template> </bento-drawer> </div> `, setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(args.isDrawerOpen); watch( () => args.isDrawerOpen, newValue => { isOpen.value = newValue; } ); return { args, isOpen, }; }, }), args: { drawerId: 'main-navigation', drawerLabel: 'Main navigation', isDrawerOpen: true, }, parameters: storybookDocsParameter(DrawerDefaultExample), };
@@ -0,0 +1 @@
1
+ export interface BentoDrawerProps { /** * Whether the drawer is open or not. * Can be synced with .sync modifier in Vue 2. */ isDrawerOpen?: boolean; /** * Unique identifier for the drawer component. * Used for accessibility and DOM element identification. */ drawerId: string; /** * Accessible label for the drawer. * Provides context for screen readers when the drawer is opened. */ drawerLabel?: string; }
@@ -0,0 +1 @@
1
+ <template> <div class="b-drawer"> <Transition name="b-drawer__animation"> <aside v-show="isDrawerOpen" :id="props.drawerId" class="b-drawer__drawer" :class="conditionalDrawerClass" :aria-label="computedDrawerLabel" > <div class="b-drawer__drawer-content"> <slot name="drawer" /> </div> </aside> </Transition> <div class="b-drawer__page"> <div class="b-drawer__page-content"> <slot name="page" /> </div> </div> </div> </template> <script setup lang="ts"> import { computed } from 'vue'; import messages from './messages.json'; import { type BentoDrawerProps } from './drawer.types'; import { BMediaQueryMMax, BMediaQuerySMax } from '@adyen/bento-design-tokens/dist/js/bento/es6'; import { useBreakpoints } from '@vueuse/core'; import { useI18n } from '@adyen/bento-vue-i18n-demi'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoDrawerProps>(), { isDrawerOpen: false, drawerLabel: '', }); const breakpoints = useBreakpoints({ s: BMediaQuerySMax, m: BMediaQueryMMax, }); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const isSmallDevice = breakpoints.smallerOrEqual('m'); const isMobileDevice = breakpoints.smallerOrEqual('s'); const computedDrawerLabel = computed(() => { return props.drawerLabel || t('menu'); }); const conditionalDrawerClass = computed(() => ({ 'b-drawer__drawer--overlay-content': isSmallDevice.value, 'b-drawer__drawer--full-width': isMobileDevice.value, })); </script> <script lang="ts"> /** * The `bento-drawer` component provides a responsive drawer-based navigation system. * * @usage * <bento-drawer * drawer-id="main-drawer" * drawer-label="Main drawer" * :is-drawer-open="true" * > * <template #menu>Drawer menu items</template> * <template #page>Main page content</template> * </bento-drawer> */ export default { name: 'bento-drawer', i18n: { messages }, }; </script> <style lang="scss" scoped src="./drawer.scss" />
@@ -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 BentoDropdownProps } from '../dropdown/dropdown.types'; import { type FormField } from '../../types/form-field'; /** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoInputFieldVariant { DEFAULT = 'default', PAYMENT_METHOD = 'payment-method', STATIC_VALUE = 'static-value', DROPDOWN = 'dropdown', } /** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoInputFieldElementPosition { START = 'start', END = 'end', } export enum BentoInputFieldStateClass { CONDENSED = 'condensed', DISABLED = 'disabled', READONLY = 'readonly', ERROR = 'error', } /** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoInputFieldType { SEARCH = 'search', TEXT = 'text', NUMBER = 'number', } export type BentoInputFieldValue = string | number; type DropdownProps = BentoDropdownProps; export interface BentoInputDropdownProps { /** * Text that labels the dropdown element */ 'aria-label'?: string; /** * Enables dynamic filtering */ dynamicFiltering?: DropdownProps['dynamicFiltering']; /** * List of available items */ items: DropdownProps['items']; /** * Sets the dropdown in read-only mode */ readonly?: DropdownProps['readonly']; /** * Selected value of the dropdown * @deprecated Since v2.0.0. Use `model-value` instead. */ value?: DropdownProps['value']; /** * Selected value of the dropdown */ modelValue?: DropdownProps['modelValue']; } /** * @deprecated Since v2.0.0. Use `@<emit name>` instead. */ export enum BentoInputFieldEvent { INPUT = 'input', ESCAPE_PRESSED = 'escape-pressed', UPDATE_DROPDOWN = 'update:dropdown', DROPDOWN_INPUT = 'dropdown-input', FOCUS = 'focus', CHANGE = 'change', BLUR = 'blur', KEYDOWN = 'keydown', } export interface BentoInputFieldProps extends FormField { /** * Indicates whether the element is exposed to an accessibility API. */ ariaHidden?: boolean; /** * Text that labels an the input element * * @deprecated Since v2.0.0. Use `aria-label` instead. */ ariaLabel?: string; /** * Reduces height of the input */ condensed?: boolean; /** * Dropdown props for the small selectable dropdown menu. * Properties supported: * * - `aria-label`: Text that labels the dropdown element * - `dynamicFiltering`: Enables dynamic filtering * - `items`: List of available items * - `readonly`: Sets the dropdown in read-only mode * - `value`: Selected value of the dropdown */ dropdown?: BentoInputDropdownProps; /** * Position of the dropdown before or after input value */ dropdownPosition?: BentoInputFieldElementPosition | `${BentoInputFieldElementPosition}`; /** * Adds error validation class for outlining input-field. * * @deprecated Since v2.0.0. Use `errorMessage` instead. */ error?: boolean; /** * Text displayed as the value of the input-field */ modelValue?: BentoInputFieldValue; /** * Renders a font numeric style with slashed zeros instead of normal ones. */ slashedZero?: boolean; /** * Static text that displayed before or after input value * Applicable to the Static Value variant */ staticValue?: string; /** * Position of Static Value before or after input value */ staticValuePosition?: BentoInputFieldElementPosition | `${BentoInputFieldElementPosition}`; /** * A string specifying the type of control to render. * All `input` types are available but only `search`, `text` and `number` are fully supported. * * If `type` is `search`, use `bento-search-bar` instead. */ type?: BentoInputFieldType | `${BentoInputFieldType}` | HTMLInputElement['type']; /** * Text displayed as the value of the input-field * @deprecated Since v2.0.0. Use `v-model` or `model-value` instead. */ value?: BentoInputFieldValue; /** * Describes the different variants for the input element */ variant?: BentoInputFieldVariant | `${BentoInputFieldVariant}`; }
1
+ import { type BentoDropdownProps } from '../dropdown/dropdown.types'; import { type FormField } from '../../types/form-field'; /** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoInputFieldVariant { DEFAULT = 'default', PAYMENT_METHOD = 'payment-method', STATIC_VALUE = 'static-value', DROPDOWN = 'dropdown', } /** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoInputFieldElementPosition { START = 'start', END = 'end', } export enum BentoInputFieldStateClass { CONDENSED = 'condensed', DISABLED = 'disabled', READONLY = 'readonly', ERROR = 'error', } /** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoInputFieldType { SEARCH = 'search', TEXT = 'text', NUMBER = 'number', } export type BentoInputFieldValue = string | number; export interface BentoInputDropdownProps { /** * Text that labels the dropdown element */ 'aria-label'?: string; /** * Indicates that dropdown items are loading (shows a loading skeleton) */ componentLoading?: BentoDropdownProps['componentLoading']; /** * Enables dynamic filtering */ dynamicFiltering?: BentoDropdownProps['dynamicFiltering']; /** * Empty state displayed when no search results are found */ emptyState?: BentoDropdownProps['emptyState']; /** * Enables value/label pair handling for selected values */ enableValueLabelPair?: BentoDropdownProps['enableValueLabelPair']; /** * Indicates whether there are more items to load */ hasMoreItems?: BentoDropdownProps['hasMoreItems']; /** * Function that determines which options are disabled from interaction */ isOptionDisabled?: BentoDropdownProps['isOptionDisabled']; /** * List of available items */ items: BentoDropdownProps['items']; /** * The type of lazy load: "automatic" for infinite scrolling, "button" for a show-more button */ lazyLoadType?: BentoDropdownProps['lazyLoadType']; /** * Indicates if new options are lazy loading */ loading?: BentoDropdownProps['loading']; /** * Selected value of the dropdown */ modelValue?: BentoDropdownProps['modelValue']; /** * Sets the dropdown in read-only mode */ readonly?: BentoDropdownProps['readonly']; /** * Dropdown search options including debounce time and placeholder */ search?: BentoDropdownProps['search']; /** * Displays non-selectable, static categories in the dropdown */ staticCategories?: BentoDropdownProps['staticCategories']; /** * Selected value of the dropdown * @deprecated Since v2.0.0. Use `model-value` instead. */ value?: BentoDropdownProps['value']; /** * Enables virtual scrolling for large lists of items */ virtualScroll?: BentoDropdownProps['virtualScroll']; } /** * @deprecated Since v2.0.0. Use `@<emit name>` instead. */ export enum BentoInputFieldEvent { INPUT = 'input', ESCAPE_PRESSED = 'escape-pressed', UPDATE_DROPDOWN = 'update:dropdown', DROPDOWN_INPUT = 'dropdown-input', FOCUS = 'focus', CHANGE = 'change', BLUR = 'blur', KEYDOWN = 'keydown', } export interface BentoInputFieldProps extends FormField { /** * Indicates whether the element is exposed to an accessibility API. */ ariaHidden?: boolean; /** * Text that labels an the input element * * @deprecated Since v2.0.0. Use `aria-label` instead. */ ariaLabel?: string; /** * Reduces height of the input */ condensed?: boolean; /** * Dropdown props for the single-select dropdown menu embedded in the input field. * Only a subset of `BentoDropdownProps` is supported (multi-select is not available). * * @see {@link BentoInputDropdownProps} */ dropdown?: BentoInputDropdownProps; /** * Position of the dropdown before or after input value */ dropdownPosition?: BentoInputFieldElementPosition | `${BentoInputFieldElementPosition}`; /** * Adds error validation class for outlining input-field. * * @deprecated Since v2.0.0. Use `errorMessage` instead. */ error?: boolean; /** * Text displayed as the value of the input-field */ modelValue?: BentoInputFieldValue; /** * Renders a font numeric style with slashed zeros instead of normal ones. */ slashedZero?: boolean; /** * Static text that displayed before or after input value * Applicable to the Static Value variant */ staticValue?: string; /** * Position of Static Value before or after input value */ staticValuePosition?: BentoInputFieldElementPosition | `${BentoInputFieldElementPosition}`; /** * A string specifying the type of control to render. * All `input` types are available but only `search`, `text` and `number` are fully supported. * * If `type` is `search`, use `bento-search-bar` instead. */ type?: BentoInputFieldType | `${BentoInputFieldType}` | HTMLInputElement['type']; /** * Text displayed as the value of the input-field * @deprecated Since v2.0.0. Use `v-model` or `model-value` instead. */ value?: BentoInputFieldValue; /** * Describes the different variants for the input element */ variant?: BentoInputFieldVariant | `${BentoInputFieldVariant}`; }
@@ -1 +1 @@
1
- <template> <bento-typography class="b-input-field" v-bind="getRootAttributes($attrs)" :data-testid="getDataTestId($attrs)" :class="inputFieldConditionalClasses" el="div" variant="body" > <field-label v-if="hasSlot('default') || label" :for="inputFieldId" data-testid="input-field-label" :tooltip-text="tooltipText" :optional="optional" :required="required" :label="label" @click="focusInput" > <slot></slot> </field-label> <div class="b-input-field__input-box" data-testid="input-box" :aria-disabled="disabled" @click="focusInput"> <!-- Optional Icon for Default text variant --> <span v-if="isDefaultVariant && (hasSlot('defaultIconBefore') || hasSlot('iconBefore'))" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-before" > <slot v-if="hasSlot('iconBefore')" name="iconBefore" /> <slot v-if="hasSlot('defaultIconBefore') && !hasSlot('iconBefore')" name="defaultIconBefore" /> </span> <!-- Mandatory Icon for Payment Method variant --> <span v-if="isPaymentMethodVariant && hasSlot('paymentMethod')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__payment-method" > <slot name="paymentMethod" /> </span> <!-- Dropdown at start --> <div v-if="shouldDisplayDropdownAtStart" :id="dropdownId" class="b-input-field__dropdown b-input-field__dropdown--start" @click.stop > <bento-dropdown v-if="dropdown" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> <!-- Static Value at start --> <bento-typography v-if="shouldDisplayStaticValueAtStart" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--start" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <div class="b-input-field__input-container"> <input :id="inputFieldId" v-bind="getInputAttributes($attrs)" ref="inputFieldElement" class="b-input-field__input" :aria-label="computedAriaLabel" :aria-describedby="computedAriaDescribedBy" :aria-owns="computedAriaOwns" :aria-invalid="shouldShowError" :placeholder="placeholder" :required="required" :aria-required="required" :type="type" :value="modelValue ?? value" :disabled="disabled" :readonly="isReadOnly" @input="onInput" @change="emit('change')" @focus="onFocus" @blur="onBlur" @keydown="emit('keydown', $event)" @keyup="emit('keyup', $event)" @keydown.esc="emit('escape-pressed')" @click="emit('click', $event)" /> <!-- Hint at the end --> <bento-typography v-if="hint && isFocused && (modelValue || value)" el="span" class="b-input-field__hint" > {{ hint }} </bento-typography> </div> <span v-if="hasSlot('iconAfter')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-after" data-testid="input-text__icon-after" > <slot name="iconAfter" /> </span> <!-- Static Value at the end --> <bento-typography v-if="shouldDisplayStaticValueAtEnd" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--end" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <!-- Dropdown at end --> <div v-if="shouldDisplayDropdownAtEnd" :id="dropdownId" data-testid="input-field-dropdown-container-end" class="b-input-field__dropdown b-input-field__dropdown--end" @click.stop > <bento-dropdown v-if="dropdown" :id="dropdownId" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> </div> <error-message v-if="shouldShowError && !!errorMessage" :id="errorId" :error-message="errorMessage" class="b-input-field__error-message" /> <span v-if="hasSlot('description') || description" :id="descriptionId" class="b-input-field__description" @click="focusInput" > <bento-typography el="span" variant="body"> <slot id="description" name="description"> {{ description }} </slot> </bento-typography> </span> </bento-typography> </template> <script setup lang="ts"> import { computed, inject, provide, type Ref, ref, toRef, toRefs, useAttrs, useSlots } from 'vue'; // Components import { BentoDropdown } from '@/components/dropdown'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { generateUid } from '@/core/utils/ts'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { INPUT_FIELD_COMPONENT_INJECTION_KEY, INPUT_FIELD_HINT_INJECTION_KEY, INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, } from '@/components/input-field/input-field.keys'; import { type BentoListboxSelectedValue } from '@/types/listbox'; // Composables import { useFormLayoutFieldLoading, useHasSlot } from '@/composables'; // Types import { type BentoInputDropdownProps, BentoInputFieldElementPosition, type BentoInputFieldProps, BentoInputFieldStateClass, BentoInputFieldType, type BentoInputFieldValue, BentoInputFieldVariant, } from './input-field.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const props = withDefaults(defineProps<BentoInputFieldProps>(), { ariaHidden: true, ariaLabel: undefined, condensed: false, description: '', disabled: false, dropdown: undefined, dropdownPosition: BentoInputFieldElementPosition.START, error: false, errorMessage: null, label: '', modelValue: undefined, optional: false, placeholder: '', required: false, readonly: false, slashedZero: false, staticValue: '', staticValuePosition: BentoInputFieldElementPosition.START, tooltipText: null, type: BentoInputFieldType.TEXT, value: undefined, variant: BentoInputFieldVariant.DEFAULT, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the input is clicked */ (e: 'click', event: MouseEvent); /** * Emitted when the user modifies the element's value. Unlike the 'input' event, the change event is not necessarily fired for each alteration to an element's value */ (e: 'change'): void; /** * Emitted when the Input Field's internal dropdown changes it's value */ (e: 'dropdown-input', selectedValue: BentoListboxSelectedValue): void; /** * Emitted when the "ESC" key is pressed */ (e: 'escape-pressed'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when a key is pressed and lifted up */ (e: 'keyup', event: KeyboardEvent): void; /** * Emitted when the component's model changes * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', inputValue: string): void; /** * Emitted when the Input Field's internal dropdown changes it's value. Contains every dropdown prop passed with the updated selected value as 'value' */ (e: 'update:dropdown', updatedDropdownProps: BentoInputDropdownProps): void; /** * Emitted when the component's model changes */ (e: 'update:model-value', inputValue: BentoInputFieldValue): void; }>(); const { emitValue } = useFormFieldEmits<BentoInputFieldValue>(emit); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const hasSlot = useHasSlot(slots); const inputFieldElement = ref(null); const inputDropdownElement = ref(null); const inputFieldId = generateUid('input'); const dropdownId = generateUid('input-dropdown'); const staticValueId = generateUid('input-static-value'); const descriptionId = generateUid('input-description'); const errorId = generateUid('input-error'); const isFocused = ref(false); /** * Gets the overriden or the default data-testid attribute * @param attributes - the live attributes object */ function getDataTestId(attributes: Record<string, unknown>) { return (attributes['data-testid'] as string) ?? 'input-text-container'; } /** * Filters out all aria attributes for the root element. * @param attributes - the live attributes object */ function getRootAttributes(attributes: Record<string, unknown>) { return Object.fromEntries( Object.entries(attributes).filter(([key]) => !key.startsWith('aria-') && key !== 'data-testid') ); } /** * Filters out the data-testid attribute for the input element. * @param attributes - the live attributes object */ function getInputAttributes(attributes: Record<string, unknown>) { return Object.fromEntries(Object.entries(attributes).filter(([key]) => key !== 'data-testid')); } // Provide INPUT_FIELD_COMPONENT_INJECTION_KEY as true, to set the input only dropdown size variant in the BentoDropdown comp provide(INPUT_FIELD_COMPONENT_INJECTION_KEY, true); // Renders the input with a hint if a string is provided const hint = inject<Ref<string | undefined>>(INPUT_FIELD_HINT_INJECTION_KEY, ref(undefined)); const onFocus = () => { isFocused.value = true; emit('focus'); }; const onBlur = () => { isFocused.value = false; emit('blur'); }; const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const isDropdownReadOnly = computed<BentoInputDropdownProps['readonly']>( () => isReadOnly.value || props.dropdown?.readonly ); const shouldShowStaticValue = computed( () => props.variant === BentoInputFieldVariant.STATIC_VALUE && !!slots.staticValue ); const isDefaultVariant = computed(() => props.variant === BentoInputFieldVariant.DEFAULT); const isPaymentMethodVariant = computed(() => props.variant === BentoInputFieldVariant.PAYMENT_METHOD); const shouldDisplayStaticValueAtStart = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.START ); const shouldDisplayStaticValueAtEnd = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.END ); const shouldShowDropdown = computed(() => props.variant === BentoInputFieldVariant.DROPDOWN && !!props.dropdown); const shouldDisplayDropdownAtStart = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.START ); const shouldDisplayDropdownAtEnd = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.END ); const shouldShowError = computed( () => !props.disabled && !isReadOnly.value && (!!props.errorMessage || props.error) ); const computedLabel = computed(() => (getSlotText(slots)('default') as string) || props.label); const computedDescription = computed(() => (getSlotText(slots)('description') as string) || props.description); const computedAriaOwns = computed(() => { if (shouldShowDropdown.value === true && shouldShowStaticValue.value === true) { return `${staticValueId} ${dropdownId}`; } if (shouldShowDropdown.value === true) { return `${dropdownId}`; } if (shouldShowStaticValue.value === true) { return `${staticValueId}`; } return null; }); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => attrs?.['aria-labelledby'] ? undefined : t('ariaLabelFallback') ); const computedAriaLabelDropdowFallback = computed(() => t('ariaLabelDropdownFallback', { inputLabel: computedAriaLabel.value }) ); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel } = toRefs(props); const computedAriaLabel = useAriaLabel({ ariaLabel: ariaLabelAttribute.value as string, // TODO: take this line away when the aria-label props is removed label: ariaLabel, defaultFallback: computedLabel.value || computedAriaLabelFallbackMessage, }); const computedAriaDescribedBy = computed( () => `${computedDescription.value ? descriptionId : ''} ${ shouldShowError.value && !!props.errorMessage ? errorId : '' }` ); const inputFieldConditionalClasses = computed(() => ({ [`b-input-field--${BentoInputFieldStateClass.CONDENSED}`]: props.condensed, [`b-input-field--${BentoInputFieldStateClass.DISABLED}`]: props.disabled, [`b-input-field--${BentoInputFieldStateClass.READONLY}`]: isReadOnly.value, [`b-input-field--${BentoInputFieldStateClass.ERROR}`]: shouldShowError.value, [`b-input-field--slashed-zero`]: props.slashedZero, })); const inputFieldDropdownProps = computed(() => ({ 'aria-label': computedAriaLabelDropdowFallback.value, ...props.dropdown, })); const onInput = (event: Event) => { const inputValue = (event.target as HTMLInputElement).value; if (!props.disabled && !isReadOnly.value) { emitValue(inputValue); } }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { emit('dropdown-input', selectedValue); emit('update:dropdown', { ...props?.dropdown, value: selectedValue }); }; const focusInput = () => { inputFieldElement.value.focus(); }; const focus = () => { if (props.variant === 'dropdown') { inputDropdownElement.value.focus(); } else { focusInput(); } }; // Expose method so that other components can focus on it in Vue3 defineExpose({ focusInput, focus, }); const inputFieldParentComponent = inject(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, ''); if (!Object.values(BentoInputFieldType).includes(props.type as BentoInputFieldType) && !inputFieldParentComponent) { if (props.type === 'tel') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-phone-number" instead.` ); } else if (props.type === 'password') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-password" instead.` ); } else { printDevelopmentWarning(`"bento-input-field" dropdown does not support the type '${props.type}'`); } } /** * Deprecations */ if (props.error) { deprecate( 'BentoInputField "error" property', `Use the BentoInputField "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-dropdown errorMessage="Error message text" />`, '2.0.0' ); } if (hasSlot('defaultIconBefore')) { deprecate( 'BentoInputField "defaultIconBefore" slot', `Use the BentoInputField "iconBefore" slot instead to add an icon at the left side of the input field <bento-input-field> \t<template #iconBefore> \t\t<my-icon-goes-here /> \t</template> </bento-input-field>`, '2.0.0' ); } if (props.value) { deprecate( 'BentoInputField "value" property', `The use of "value" prop in "BentoInputField" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field component is used to create interactive controls for web-based * forms in order to accept data from the user. * * @example * import { BentoInputField } from '@adyen/bento-vue2' * * const inputValue = ref(''); * * export default { * components: { BentoInputField }, * tempate: ` * <bento-input-field * :model-value="inputValue" * :@update:model-value="newValue => inputValue.value = newValue" * > * } */ export default { name: 'bento-input-field', inheritAttrs: false, model: { prop: 'modelValue' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./input-field.scss" />
1
+ <template> <bento-typography class="b-input-field" v-bind="getRootAttributes($attrs)" :data-testid="getDataTestId($attrs)" :class="inputFieldConditionalClasses" el="div" variant="body" > <field-label v-if="hasSlot('default') || label" :for="inputFieldId" data-testid="input-field-label" :tooltip-text="tooltipText" :optional="optional" :required="required" :label="label" @click="focusInput" > <slot></slot> </field-label> <div class="b-input-field__input-box" data-testid="input-box" :aria-disabled="disabled" @click="focusInput"> <!-- Optional Icon for Default text variant --> <span v-if="isDefaultVariant && (hasSlot('defaultIconBefore') || hasSlot('iconBefore'))" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-before" > <slot v-if="hasSlot('iconBefore')" name="iconBefore" /> <slot v-if="hasSlot('defaultIconBefore') && !hasSlot('iconBefore')" name="defaultIconBefore" /> </span> <!-- Mandatory Icon for Payment Method variant --> <span v-if="isPaymentMethodVariant && hasSlot('paymentMethod')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__payment-method" > <slot name="paymentMethod" /> </span> <!-- Dropdown at start --> <div v-if="shouldDisplayDropdownAtStart" :id="dropdownId" class="b-input-field__dropdown b-input-field__dropdown--start" @click.stop > <bento-dropdown v-if="dropdown" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> <!-- Static Value at start --> <bento-typography v-if="shouldDisplayStaticValueAtStart" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--start" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <div class="b-input-field__input-container"> <input :id="inputFieldId" v-bind="$attrs" ref="inputFieldElement" class="b-input-field__input" :aria-label="computedAriaLabel" :aria-describedby="computedAriaDescribedBy" :aria-owns="computedAriaOwns" :aria-invalid="shouldShowError" :placeholder="placeholder" :required="required" :aria-required="required" :type="type" :value="modelValue ?? value" :disabled="disabled" :readonly="isReadOnly" @input="onInput" @change="emit('change')" @focus="onFocus" @blur="onBlur" @keydown="emit('keydown', $event)" @keyup="emit('keyup', $event)" @keydown.esc="emit('escape-pressed')" @click="emit('click', $event)" /> <!-- Hint at the end --> <bento-typography v-if="hint && isFocused && (modelValue || value)" el="span" class="b-input-field__hint" > {{ hint }} </bento-typography> </div> <span v-if="hasSlot('iconAfter')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-after" data-testid="input-text__icon-after" > <slot name="iconAfter" /> </span> <!-- Static Value at the end --> <bento-typography v-if="shouldDisplayStaticValueAtEnd" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--end" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <!-- Dropdown at end --> <div v-if="shouldDisplayDropdownAtEnd" :id="dropdownId" data-testid="input-field-dropdown-container-end" class="b-input-field__dropdown b-input-field__dropdown--end" @click.stop > <bento-dropdown v-if="dropdown" :id="dropdownId" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> </div> <error-message v-if="shouldShowError && !!errorMessage" :id="errorId" :error-message="errorMessage" class="b-input-field__error-message" /> <span v-if="hasSlot('description') || description" :id="descriptionId" class="b-input-field__description" @click="focusInput" > <bento-typography el="span" variant="body"> <slot id="description" name="description"> {{ description }} </slot> </bento-typography> </span> </bento-typography> </template> <script setup lang="ts"> import { computed, inject, provide, type Ref, ref, toRef, toRefs, useAttrs, useSlots } from 'vue'; // Components import { BentoDropdown } from '@/components/dropdown'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { generateUid } from '@/core/utils/ts'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { INPUT_FIELD_COMPONENT_INJECTION_KEY, INPUT_FIELD_HINT_INJECTION_KEY, INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, } from '@/components/input-field/input-field.keys'; import { type BentoListboxSelectedValue } from '@/types/listbox'; // Composables import { useFormLayoutFieldLoading, useHasSlot } from '@/composables'; // Types import { type BentoInputDropdownProps, BentoInputFieldElementPosition, type BentoInputFieldProps, BentoInputFieldStateClass, BentoInputFieldType, type BentoInputFieldValue, BentoInputFieldVariant, } from './input-field.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const props = withDefaults(defineProps<BentoInputFieldProps>(), { ariaHidden: true, ariaLabel: undefined, condensed: false, description: '', disabled: false, dropdown: undefined, dropdownPosition: BentoInputFieldElementPosition.START, error: false, errorMessage: null, label: '', modelValue: undefined, optional: false, placeholder: '', required: false, readonly: false, slashedZero: false, staticValue: '', staticValuePosition: BentoInputFieldElementPosition.START, tooltipText: null, type: BentoInputFieldType.TEXT, value: undefined, variant: BentoInputFieldVariant.DEFAULT, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the input is clicked */ (e: 'click', event: MouseEvent); /** * Emitted when the user modifies the element's value. Unlike the 'input' event, the change event is not necessarily fired for each alteration to an element's value */ (e: 'change'): void; /** * Emitted when the Input Field's internal dropdown changes it's value */ (e: 'dropdown-input', selectedValue: BentoListboxSelectedValue): void; /** * Emitted when the "ESC" key is pressed */ (e: 'escape-pressed'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when a key is pressed and lifted up */ (e: 'keyup', event: KeyboardEvent): void; /** * Emitted when the component's model changes * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', inputValue: string): void; /** * Emitted when the Input Field's internal dropdown changes it's value. Contains every dropdown prop passed with the updated selected value as 'value' */ (e: 'update:dropdown', updatedDropdownProps: BentoInputDropdownProps): void; /** * Emitted when the component's model changes */ (e: 'update:model-value', inputValue: BentoInputFieldValue): void; }>(); const { emitValue } = useFormFieldEmits<BentoInputFieldValue>(emit); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const hasSlot = useHasSlot(slots); const inputFieldElement = ref(null); const inputDropdownElement = ref(null); const inputFieldId = generateUid('input'); const dropdownId = generateUid('input-dropdown'); const staticValueId = generateUid('input-static-value'); const descriptionId = generateUid('input-description'); const errorId = generateUid('input-error'); const isFocused = ref(false); /** * Gets the overriden or the default data-testid attribute * @param attributes - the live attributes object */ function getDataTestId(attributes: Record<string, unknown>) { return (attributes['data-testid'] as string) ?? 'input-text-container'; } /** * Filters out all aria attributes for the root element. * @param attributes - the live attributes object */ function getRootAttributes(attributes: Record<string, unknown>) { return Object.fromEntries( Object.entries(attributes).filter(([key]) => !key.startsWith('aria-') && key !== 'data-testid') ); } // Provide INPUT_FIELD_COMPONENT_INJECTION_KEY as true, to set the input only dropdown size variant in the BentoDropdown comp provide(INPUT_FIELD_COMPONENT_INJECTION_KEY, true); // Renders the input with a hint if a string is provided const hint = inject<Ref<string | undefined>>(INPUT_FIELD_HINT_INJECTION_KEY, ref(undefined)); const onFocus = () => { isFocused.value = true; emit('focus'); }; const onBlur = () => { isFocused.value = false; emit('blur'); }; const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const isDropdownReadOnly = computed<BentoInputDropdownProps['readonly']>( () => isReadOnly.value || props.dropdown?.readonly ); const shouldShowStaticValue = computed( () => props.variant === BentoInputFieldVariant.STATIC_VALUE && !!slots.staticValue ); const isDefaultVariant = computed(() => props.variant === BentoInputFieldVariant.DEFAULT); const isPaymentMethodVariant = computed(() => props.variant === BentoInputFieldVariant.PAYMENT_METHOD); const shouldDisplayStaticValueAtStart = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.START ); const shouldDisplayStaticValueAtEnd = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.END ); const shouldShowDropdown = computed(() => props.variant === BentoInputFieldVariant.DROPDOWN && !!props.dropdown); const shouldDisplayDropdownAtStart = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.START ); const shouldDisplayDropdownAtEnd = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.END ); const shouldShowError = computed( () => !props.disabled && !isReadOnly.value && (!!props.errorMessage || props.error) ); const computedLabel = computed(() => (getSlotText(slots)('default') as string) || props.label); const computedDescription = computed(() => (getSlotText(slots)('description') as string) || props.description); const computedAriaOwns = computed(() => { if (shouldShowDropdown.value === true && shouldShowStaticValue.value === true) { return `${staticValueId} ${dropdownId}`; } if (shouldShowDropdown.value === true) { return `${dropdownId}`; } if (shouldShowStaticValue.value === true) { return `${staticValueId}`; } return null; }); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => attrs?.['aria-labelledby'] ? undefined : t('ariaLabelFallback') ); const computedAriaLabelDropdowFallback = computed(() => t('ariaLabelDropdownFallback', { inputLabel: computedAriaLabel.value }) ); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel } = toRefs(props); const computedAriaLabel = useAriaLabel({ ariaLabel: ariaLabelAttribute.value as string, // TODO: take this line away when the aria-label props is removed label: ariaLabel, defaultFallback: computedLabel.value || computedAriaLabelFallbackMessage, }); const computedAriaDescribedBy = computed( () => `${computedDescription.value ? descriptionId : ''} ${ shouldShowError.value && !!props.errorMessage ? errorId : '' }` ); const inputFieldConditionalClasses = computed(() => ({ [`b-input-field--${BentoInputFieldStateClass.CONDENSED}`]: props.condensed, [`b-input-field--${BentoInputFieldStateClass.DISABLED}`]: props.disabled, [`b-input-field--${BentoInputFieldStateClass.READONLY}`]: isReadOnly.value, [`b-input-field--${BentoInputFieldStateClass.ERROR}`]: shouldShowError.value, [`b-input-field--slashed-zero`]: props.slashedZero, })); const inputFieldDropdownProps = computed(() => ({ 'aria-label': computedAriaLabelDropdowFallback.value, ...props.dropdown, })); const onInput = (event: Event) => { const inputValue = (event.target as HTMLInputElement).value; if (!props.disabled && !isReadOnly.value) { emitValue(inputValue); } }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { emit('dropdown-input', selectedValue); emit('update:dropdown', { ...props?.dropdown, value: selectedValue }); }; const focusInput = () => { inputFieldElement.value.focus(); }; const focus = () => { if (props.variant === 'dropdown') { inputDropdownElement.value.focus(); } else { focusInput(); } }; // Expose method so that other components can focus on it in Vue3 defineExpose({ focusInput, focus, }); const inputFieldParentComponent = inject(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, ''); if (!Object.values(BentoInputFieldType).includes(props.type as BentoInputFieldType) && !inputFieldParentComponent) { if (props.type === 'tel') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-phone-number" instead.` ); } else if (props.type === 'password') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-password" instead.` ); } else { printDevelopmentWarning(`"bento-input-field" dropdown does not support the type '${props.type}'`); } } /** * Deprecations */ if (props.error) { deprecate( 'BentoInputField "error" property', `Use the BentoInputField "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-dropdown errorMessage="Error message text" />`, '2.0.0' ); } if (hasSlot('defaultIconBefore')) { deprecate( 'BentoInputField "defaultIconBefore" slot', `Use the BentoInputField "iconBefore" slot instead to add an icon at the left side of the input field <bento-input-field> \t<template #iconBefore> \t\t<my-icon-goes-here /> \t</template> </bento-input-field>`, '2.0.0' ); } if (props.value) { deprecate( 'BentoInputField "value" property', `The use of "value" prop in "BentoInputField" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field component is used to create interactive controls for web-based * forms in order to accept data from the user. * * @example * import { BentoInputField } from '@adyen/bento-vue2' * * const inputValue = ref(''); * * export default { * components: { BentoInputField }, * tempate: ` * <bento-input-field * :model-value="inputValue" * :@update:model-value="newValue => inputValue.value = newValue" * > * } */ export default { name: 'bento-input-field', inheritAttrs: false, model: { prop: 'modelValue' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./input-field.scss" />
@@ -1 +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" />