@adyen/bento-mcp 0.5.2 → 0.5.4

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 (85) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/assets/components/alert/alert.docs.mdx +17 -17
  3. package/dist/assets/components/alert/alert.vue +1 -1
  4. package/dist/assets/components/country/country.vue +1 -1
  5. package/dist/assets/components/dashed-underline/dashed-underline.vue +1 -1
  6. package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.types.ts +1 -1
  7. package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.vue +1 -1
  8. package/dist/assets/components/data-grid/data-grid.docs.mdx +3 -0
  9. package/dist/assets/components/data-grid/data-grid.vue +1 -1
  10. package/dist/assets/components/date/date.vue +1 -1
  11. package/dist/assets/components/date-picker/composables/use-date-picker-single-calendar-text.types.ts +1 -1
  12. package/dist/assets/components/date-picker/date-picker.docs.mdx +6 -0
  13. package/dist/assets/components/date-picker/date-picker.stories.ts +1 -1
  14. package/dist/assets/components/date-picker/date-picker.types.ts +1 -1
  15. package/dist/assets/components/date-picker/date-picker.vue +1 -1
  16. package/dist/assets/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.vue +1 -1
  17. package/dist/assets/components/date-range-picker/components/date-time-input/date-time-input.types.ts +1 -0
  18. package/dist/assets/components/date-range-picker/components/date-time-input/date-time-input.vue +1 -0
  19. package/dist/assets/components/date-range-picker/date-range-picker.docs.mdx +6 -0
  20. package/dist/assets/components/date-range-picker/date-range-picker.stories.ts +1 -1
  21. package/dist/assets/components/date-range-picker/date-range-picker.types.ts +1 -1
  22. package/dist/assets/components/date-range-picker/date-range-picker.vue +1 -1
  23. package/dist/assets/components/date-time-picker/date-time-picker.docs.mdx +129 -0
  24. package/dist/assets/components/date-time-picker/date-time-picker.stories.ts +1 -0
  25. package/dist/assets/components/date-time-picker/date-time-picker.types.ts +1 -0
  26. package/dist/assets/components/date-time-picker/date-time-picker.vue +1 -0
  27. package/dist/assets/components/draggable/draggable.vue +1 -1
  28. package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue +1 -1
  29. package/dist/assets/components/dropdown/components/dropdown-options-container/dropdown-options-container.vue +1 -1
  30. package/dist/assets/components/dropdown/components/dropdown-small-textbox/dropdown-small-textbox.vue +1 -1
  31. package/dist/assets/components/dropdown/components/variants/dropdown-country/dropdown-country.types.ts +1 -1
  32. package/dist/assets/components/dropdown/dropdown.types.ts +1 -1
  33. package/dist/assets/components/dropdown/dropdown.vue +1 -1
  34. package/dist/assets/components/file-uploader/file-uploader.vue +1 -1
  35. package/dist/assets/components/filter-bar/components/checkbox-group-filter/checkbox-group-filter.vue +1 -1
  36. package/dist/assets/components/filter-bar/components/date-filter/date-filter.vue +1 -1
  37. package/dist/assets/components/filter-bar/components/date-range-filter/date-range-filter.vue +1 -1
  38. package/dist/assets/components/filter-bar/filter-bar.docs.mdx +8 -2
  39. package/dist/assets/components/filter-bar/filter-bar.stories.ts +1 -1
  40. package/dist/assets/components/filter-bar/filter-bar.types.ts +1 -1
  41. package/dist/assets/components/filter-bar/filter-bar.vue +1 -1
  42. package/dist/assets/components/input-field/input-field.vue +1 -1
  43. package/dist/assets/components/input-field-password/input-field-password.vue +1 -1
  44. package/dist/assets/components/internal/calendar/calendar.stories.ts +1 -1
  45. package/dist/assets/components/internal/calendar/calendar.types.ts +1 -1
  46. package/dist/assets/components/internal/calendar/calendar.vue +1 -1
  47. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.types.ts +1 -1
  48. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.vue +1 -1
  49. package/dist/assets/components/internal/calendar/components/calendar-month/components/calendar-month-day/calendar-month-day.vue +1 -1
  50. package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.types.ts +1 -1
  51. package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.vue +1 -1
  52. package/dist/assets/components/internal/calendar/components/calendar-year-selector/calendar-year-selector.types.ts +1 -0
  53. package/dist/assets/components/internal/calendar/components/calendar-year-selector/calendar-year-selector.vue +1 -0
  54. package/dist/assets/components/internal/header-meta/header-meta.vue +1 -1
  55. package/dist/assets/components/internal/info-icon-with-popover/info-icon-with-popover.vue +1 -1
  56. package/dist/assets/components/internal/listbox/components/listbox-multi-select/listbox-multi-select.vue +1 -1
  57. package/dist/assets/components/internal/listbox/components/listbox-single-select/listbox-single-select.vue +1 -1
  58. package/dist/assets/components/link/link.vue +1 -1
  59. package/dist/assets/components/list/components/list-item/list-item.types.ts +1 -1
  60. package/dist/assets/components/list/components/list-item/list-item.vue +1 -1
  61. package/dist/assets/components/list/list.docs.mdx +62 -0
  62. package/dist/assets/components/list/list.stories.ts +1 -1
  63. package/dist/assets/components/list/list.types.ts +1 -1
  64. package/dist/assets/components/list/list.vue +1 -1
  65. package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.stories.ts +1 -1
  66. package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.vue +1 -1
  67. package/dist/assets/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.stories.ts +1 -1
  68. package/dist/assets/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.vue +1 -1
  69. package/dist/assets/components/navigation-menu/navigation-menu.docs.mdx +7 -43
  70. package/dist/assets/components/navigation-menu/navigation-menu.stories.ts +1 -1
  71. package/dist/assets/components/navigation-menu/navigation-menu.types.ts +1 -1
  72. package/dist/assets/components/navigation-menu/navigation-menu.vue +1 -1
  73. package/dist/assets/components/popover/popover.docs.mdx +78 -15
  74. package/dist/assets/components/popover/popover.stories.ts +1 -1
  75. package/dist/assets/components/selection-card/components/selection-card-group/selection-card-group.types.ts +1 -1
  76. package/dist/assets/components/summary-grid/summary-grid.vue +1 -1
  77. package/dist/assets/components/tabs/tabs.docs.mdx +6 -1
  78. package/dist/assets/components.json +1 -0
  79. package/dist/assets/composables/use-bento-delayed-hover/use-bento-delayed-hover.docs.mdx +77 -0
  80. package/dist/assets/composables/use-bento-delayed-hover/use-bento-delayed-hover.stories.ts +1 -0
  81. package/dist/assets/deprecations.md +156 -156
  82. package/dist/assets/index.ts +1 -1
  83. package/dist/assets/usage.json +75 -74
  84. package/dist/main.js +135 -48
  85. package/package.json +1 -1
@@ -1 +1 @@
1
- <template> <li> <component :is="wrapperComponent" v-bind="wrapperProps" class="b-navigation-menu-item" :class="{ ...conditionalClasses, 'b-navigation-menu-item--no-icon': !hasSlot('icon'), }" :aria-current="isSelected && link ? 'page' : undefined" :aria-hidden="!isVisible ? 'true' : undefined" :tabindex="!isVisible ? -1 : undefined" :inert="!isVisible ? '' : undefined" @click="handleClick" > <div v-if="!nested && hasSlot('icon')" class="b-navigation-menu-item__icon"> <slot name="icon" /> </div> <bento-typography ref="labelRef" v-bento-tooltip-directive="isTruncated ? label : undefined" el="span" variant="body" stronger class="b-navigation-menu-item__label" > {{ label }} </bento-typography> <div v-if="hasSlot('action')" class="b-navigation-menu-item__action"> <slot name="action" /> </div> </component> </li> </template> <script setup lang="ts"> import { computed, inject, onBeforeUnmount, onMounted, ref, useSlots } from 'vue'; import { useResizeObserver } from '@vueuse/core'; import { BentoLink } from '@/components/link'; import { BentoTypography } from '@/components/typography'; import { generateUid } from '@/core/utils/ts'; import { useHasSlot } from '@/composables'; import { BentoTooltipDirective as vBentoTooltipDirective } from '@/directives/tooltip'; import type { BentoNavigationMenuItemProps } from './navigation-menu-item.types'; import type { BentoNavigationMenuGroupState } from '../navigation-menu-group/navigation-menu-group.types'; import { NAVIGATION_MENU_GROUP_STATE_INJECTION_KEY } from '../navigation-menu-group/navigation-menu-group.keys'; import type { BentoNavigationMenuState } from '../../navigation-menu.types'; import { NAVIGATION_MENU_STATE_INJECTION_KEY } from '../../navigation-menu.keys'; const props = withDefaults(defineProps<BentoNavigationMenuItemProps>(), { nested: false, link: undefined, alwaysShowActions: false, }); const slots = useSlots(); const hasSlot = useHasSlot(slots); const wrapperComponent = computed(() => (props.link ? BentoLink : 'div')); const wrapperProps = computed(() => (props.link ? { ...props.link, disableTypography: true } : {})); const labelRef = ref(null); const isTruncated = ref(false); useResizeObserver(labelRef, () => { const el = labelRef.value?.$el ?? labelRef.value; if (el) { isTruncated.value = el.scrollWidth > el.clientWidth; } }); const itemId = generateUid('navigation-menu-item'); const groupState = inject<BentoNavigationMenuGroupState | null>(NAVIGATION_MENU_GROUP_STATE_INJECTION_KEY, null); const menuState = inject<BentoNavigationMenuState | null>(NAVIGATION_MENU_STATE_INJECTION_KEY, null); const isSelected = computed(() => menuState?.activePage?.value === props.value); const isVisible = computed(() => { if (!groupState) { return true; } if (!groupState.isSemiCollapsed.value) { return true; } return isSelected.value; }); const conditionalClasses = computed(() => ({ 'b-navigation-menu-item--selected': isSelected.value, 'b-navigation-menu-item--nested': props.nested, 'b-navigation-menu-item--hidden': !isVisible.value, 'b-navigation-menu-item--always-show-actions': props.alwaysShowActions, })); onMounted(() => { if (groupState && props.nested) { groupState.registerItem(itemId, () => isSelected.value); } }); const handleClick = () => { menuState?.updateActivePage(props.value); }; onBeforeUnmount(() => { if (groupState && props.nested) { groupState.unregisterItem(itemId); } }); </script> <script lang="ts"> /** * Navigation menu item component for rendering individual navigation links. * Can be used standalone or nested within a navigation-menu-group. * * @usage * import { BentoNavigationMenuItem } from '@adyen/bento-vue2'; * * export default { * components: { BentoNavigationMenuItem }, * template: ` * <bento-navigation-menu-item value="home" label="Home"> * <template #icon><home-icon /></template> * <template #action><star-icon /></template> * </bento-navigation-menu-item> * ` * } */ export default { name: 'bento-navigation-menu-item', }; </script> <style lang="scss" scoped src="./navigation-menu-item.scss" />
1
+ <template> <li class="b-navigation-menu-item" :class="{ ...conditionalClasses, 'b-navigation-menu-item--no-icon': !hasSlot('icon'), }" :aria-hidden="!isVisible ? 'true' : undefined" > <component :is="componentType" v-bind="componentProps" class="b-navigation-menu-item__link" :aria-current="isSelected && link ? 'page' : undefined" :tabindex="!isVisible ? -1 : undefined" :inert="!isVisible ? '' : undefined" @click="handleClick" > <div v-if="!nested && hasSlot('icon')" class="b-navigation-menu-item__icon" aria-hidden="true"> <slot name="icon" /> </div> <bento-typography ref="labelRef" v-bento-tooltip-directive="isTruncated ? label : undefined" el="span" variant="body" stronger class="b-navigation-menu-item__label" > {{ label }} </bento-typography> </component> <div v-if="hasSlot('action')" class="b-navigation-menu-item__action"> <slot name="action" /> </div> </li> </template> <script setup lang="ts"> import { computed, inject, onBeforeUnmount, onMounted, ref, useSlots } from 'vue'; import { useResizeObserver } from '@vueuse/core'; import { BentoLink } from '@/components/link'; import { BentoTypography } from '@/components/typography'; import { generateUid } from '@/core/utils/ts'; import { useHasSlot } from '@/composables'; import { BentoTooltipDirective as vBentoTooltipDirective } from '@/directives/tooltip'; import type { BentoNavigationMenuItemProps } from './navigation-menu-item.types'; import type { BentoNavigationMenuGroupState } from '../navigation-menu-group/navigation-menu-group.types'; import { NAVIGATION_MENU_GROUP_STATE_INJECTION_KEY } from '../navigation-menu-group/navigation-menu-group.keys'; import type { BentoNavigationMenuState } from '../../navigation-menu.types'; import { NAVIGATION_MENU_STATE_INJECTION_KEY } from '../../navigation-menu.keys'; const props = withDefaults(defineProps<BentoNavigationMenuItemProps>(), { nested: false, alwaysShowActions: false, }); const slots = useSlots(); const hasSlot = useHasSlot(slots); const componentType = computed(() => (props.link ? BentoLink : 'div')); const componentProps = computed(() => (props.link ? { ...props.link, disableTypography: true } : {})); const labelRef = ref(null); const isTruncated = ref(false); useResizeObserver(labelRef, () => { const el = labelRef.value?.$el ?? labelRef.value; if (el) { isTruncated.value = el.scrollWidth > el.clientWidth; } }); const itemId = generateUid('navigation-menu-item'); const groupState = inject<BentoNavigationMenuGroupState | null>(NAVIGATION_MENU_GROUP_STATE_INJECTION_KEY, null); const menuState = inject<BentoNavigationMenuState | null>(NAVIGATION_MENU_STATE_INJECTION_KEY, null); const isSelected = computed(() => menuState?.activePage?.value === props.value); const isVisible = computed(() => { if (!groupState) { return true; } if (!groupState.isSemiCollapsed.value) { return true; } return isSelected.value; }); const conditionalClasses = computed(() => ({ 'b-navigation-menu-item--selected': isSelected.value, 'b-navigation-menu-item--nested': props.nested, 'b-navigation-menu-item--hidden': !isVisible.value, 'b-navigation-menu-item--always-show-actions': props.alwaysShowActions, })); onMounted(() => { if (groupState && props.nested) { groupState.registerItem(itemId, () => isSelected.value); } }); const handleClick = () => { menuState?.updateActivePage(props.value); }; onBeforeUnmount(() => { if (groupState && props.nested) { groupState.unregisterItem(itemId); } }); </script> <script lang="ts"> /** * Navigation menu item component for rendering individual navigation links. * Can be used standalone or nested within a navigation-menu-group. * * @usage * import { BentoNavigationMenuItem } from '@adyen/bento-vue2'; * * export default { * components: { BentoNavigationMenuItem }, * template: ` * <bento-navigation-menu-item value="home" label="Home" :link="{ to: '/home' }"> * <template #icon><home-icon /></template> * <template #action><star-icon svg-title="Add to favorites" /></template> * </bento-navigation-menu-item> * ` * } */ export default { name: 'bento-navigation-menu-item', }; </script> <style lang="scss" scoped src="./navigation-menu-item.scss" />
@@ -52,8 +52,8 @@ navigation items that can be logically grouped.
52
52
  code={`<bento-navigation-menu>
53
53
  <bento-navigation-menu-group label="Settings" :is-expanded="true">
54
54
  <template #icon><folder-icon /></template>
55
- <bento-navigation-menu-item nested value="general" label="General" />
56
- <bento-navigation-menu-item nested value="security" label="Security" />
55
+ <bento-navigation-menu-item nested value="general" label="General" :link="{ to: '/settings/general' }" />
56
+ <bento-navigation-menu-item nested value="security" label="Security" :link="{ to: '/settings/security' }" />
57
57
  </bento-navigation-menu-group>
58
58
  </bento-navigation-menu>`}
59
59
  />
@@ -71,7 +71,7 @@ Use the `headerActions` slot to add interactive elements like search or filters
71
71
  code={`<bento-navigation-menu label="Pages">
72
72
  <template #headerActions>
73
73
  <bento-button variant="tertiary-with-background">
74
- <template #iconLeft><search-icon /></template>
74
+ <template #iconLeft><search-icon svg-title="Search" /></template>
75
75
  </bento-button>
76
76
  </template>
77
77
  <!-- menu items -->
@@ -93,13 +93,7 @@ keep the action visible at all times.
93
93
 
94
94
  ## Behavior
95
95
 
96
- ### Navigation patterns
97
-
98
- > **⚠️ Important**: Every `bento-navigation-menu-item` **must** be rendered as (or wrapped in) a real link. Without a
99
- > link, the item is not keyboard-accessible, cannot be opened in a new tab, does not expose a URL to screen readers, and
100
- > fails core navigation accessibility requirements. Use **one** of the two patterns below.
101
-
102
- #### 1. Controlled navigation with the `link` prop
96
+ ### Navigation
103
97
 
104
98
  Pass a `link` object to `bento-navigation-menu-item` to integrate with routing. The `link` prop accepts the same props
105
99
  as `bento-link`, allowing seamless integration with Vue Router or other routing solutions.
@@ -116,41 +110,13 @@ as `bento-link`, allowing seamless integration with Vue Router or other routing
116
110
  </bento-navigation-menu-item>`}
117
111
  />
118
112
 
119
- This is the recommended approach as it provides the best accessibility, SEO, and user experience (right-click to open in
120
- new tab, copy link, etc.) out of the box.
121
-
122
- #### 2. Wrapping with a custom link component
123
-
124
- If you need full control over the link behavior, wrap the item with any link component (such as `router-link`,
125
- `bento-link`, or a custom link component). The wrapping element **must** be a real, focusable link.
126
-
127
- <Source
128
- dark
129
- language="html"
130
- code={`<router-link to="/profile">
131
- <bento-navigation-menu-item
132
- value="profile"
133
- label="Profile"
134
- >
135
- <template #icon><profile-icon /></template>
136
- </bento-navigation-menu-item>
137
- </router-link>`}
138
- />
139
-
140
- > **Note**: When wrapping with a custom link component, `aria-current` is **not** applied automatically by the menu item
141
- > — you must set `aria-current="page"` on the wrapping element yourself (many routers, like Vue Router, handle this
142
- > automatically via `router-link-active` / `router-link-exact-active`). `aria-current` is only applied automatically
143
- > when you use the `link` prop.
144
-
145
113
  #### Tracking navigation with the `update:active-page` event
146
114
 
147
115
  The `bento-navigation-menu` emits `update:active-page` whenever an item is clicked. This is useful for side effects such
148
- as analytics tracking, syncing state to a store, or triggering other UI updates — **in addition to** one of the two link
149
- patterns above.
116
+ as analytics tracking, syncing state to a store, or triggering other UI updates.
150
117
 
151
- > **⚠️ Do not** use `update:active-page` as the **only** navigation mechanism. The event does not provide a focusable,
152
- > keyboard-accessible link — items without a `link` prop or link wrapper are inaccessible to keyboard and screen reader
153
- > users. Always pair it with one of the patterns above.
118
+ > **Do not** use `update:active-page` as the only navigation mechanism. Always provide the `link` prop for accessible
119
+ > navigation.
154
120
 
155
121
  ### Group expansion state
156
122
 
@@ -227,8 +193,6 @@ footer nav), each landmark needs an accessible name so screen reader users can t
227
193
  | | `aria-current="page"` | `a` | Applied automatically when the item is selected **and** the `link` prop is used |
228
194
  | `link` | | `a` | Applied automatically when using the `link` prop (via `bento-link`) |
229
195
 
230
- > When wrapping the item with a custom link component, set `aria-current="page"` on the wrapping element yourself.
231
-
232
196
  #### Navigation Menu Group
233
197
 
234
198
  | Role | Attribute | Element | Usage |
@@ -1 +1 @@
1
- import { ref } from 'vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoNavigationMenu from './navigation-menu.vue'; import BentoNavigationMenuItem from './components/navigation-menu-item/navigation-menu-item.vue'; import BentoNavigationMenuGroup from './components/navigation-menu-group/navigation-menu-group.vue'; import { BentoButton } from '@/components/button'; import { BentoLink } from '@/components/link'; import type { Meta, StoryObj } from '@storybook/vue'; import StarIcon from '@adyen/ui-assets-icons-16/vue/star'; import StarFilledIcon from '@adyen/ui-assets-icons-16/vue/star-filled'; import SearchIcon from '@adyen/ui-assets-icons-16/vue/search'; import NavHomeIcon from '@adyen/ui-assets-icons-16/vue/nav-home'; import NavPaymentsIcon from '@adyen/ui-assets-icons-16/vue/nav-payments'; import NavReportsIcon from '@adyen/ui-assets-icons-16/vue/nav-reports'; import NavSettingsIcon from '@adyen/ui-assets-icons-16/vue/nav-settings'; import NavAccountHoldersIcon from '@adyen/ui-assets-icons-16/vue/nav-account-holders'; import { isVue2 } from 'vue-demi'; import NavigationMenuEverythingBagelExample from './__tests__/navigation-menu-everything-bagel-example.vue?raw'; import NavigationMenuSimpleListExample from './__tests__/navigation-menu-simple-list-example.vue?raw'; export default { title: 'Navigation Menu', component: BentoNavigationMenu, argTypes: { default: { control: false, table: { type: { summary: 'Navigation items and groups' }, }, }, headerActions: { control: false, table: { type: { summary: 'Action buttons' }, }, }, }, } as Meta; type Story = StoryObj<typeof BentoNavigationMenu>; export const EverythingBagel: Story = { render: (_args, { argTypes }) => ({ components: { BentoNavigationMenu, BentoNavigationMenuItem, BentoNavigationMenuGroup, BentoButton, BentoLink, StarIcon, StarFilledIcon, SearchIcon, NavHomeIcon, NavPaymentsIcon, NavReportsIcon, NavSettingsIcon, NavAccountHoldersIcon, }, props: Object.keys(argTypes), setup(props) { const activePage = ref('home'); const paymentsExpanded = ref(true); const favorites = ref({}); const toggleFavorite = (value: string) => { favorites.value = { ...favorites.value, [value]: !favorites.value[value] }; }; return { activePage, paymentsExpanded, favorites, toggleFavorite, args: isVue2 ? props : _args, }; }, template: ` <div style="width: 248px;"> <bento-navigation-menu v-bind="args" :active-page="activePage" @update:active-page="activePage = $event"> <template #headerActions> <bento-button variant="tertiary-with-background"> <template #iconLeft> <search-icon /> </template> </bento-button> </template> <bento-navigation-menu-item value="home" label="Home" :link="{ to: '#home', isNotRouting: true }" > <template #icon> <nav-home-icon /> </template> </bento-navigation-menu-item> <bento-navigation-menu-group label="Payments" :is-expanded="paymentsExpanded" @update:is-expanded="paymentsExpanded = $event" > <template #icon> <nav-payments-icon /> </template> <bento-navigation-menu-item nested value="payments" label="Payments" :link="{ to: '#payments', isNotRouting: true }" :always-show-actions="!!favorites.payments || activePage === 'payments'" > <template #action> <bento-button variant="tertiary" @click.stop.prevent="toggleFavorite('payments')" > <template #iconLeft> <star-filled-icon style="color: var(--b-color-decorative-green);" v-if="favorites.payments" /> <star-icon v-else /> </template> </bento-button> </template> </bento-navigation-menu-item> <bento-navigation-menu-item nested value="preauth" label="Pre-authorized" :link="{ to: '#preauth', isNotRouting: true }" :always-show-actions="!!favorites.preauth || activePage === 'preauth'" > <template #action> <bento-button variant="tertiary" @click.stop.prevent="toggleFavorite('preauth')" > <template #iconLeft> <star-filled-icon style="color: var(--b-color-decorative-green);" v-if="favorites.preauth" /> <star-icon v-else /> </template> </bento-button> </template> </bento-navigation-menu-item> <bento-navigation-menu-item nested value="links" label="Payment links" :link="{ to: '#links', isNotRouting: true }" :always-show-actions="!!favorites.links || activePage === 'links'" > <template #action> <bento-button variant="tertiary" @click.stop.prevent="toggleFavorite('links')" > <template #iconLeft> <star-filled-icon style="color: var(--b-color-decorative-green);" v-if="favorites.links" /> <star-icon v-else /> </template> </bento-button> </template> </bento-navigation-menu-item> <bento-navigation-menu-item nested value="methods" label="Payment methods" :link="{ to: '#methods', isNotRouting: true }" :always-show-actions="!!favorites.methods || activePage === 'methods'" > <template #action> <bento-button variant="tertiary" @click.stop.prevent="toggleFavorite('methods')" > <template #iconLeft> <star-filled-icon style="color: var(--b-color-decorative-green);" v-if="favorites.methods" /> <star-icon v-else /> </template> </bento-button> </template> </bento-navigation-menu-item> </bento-navigation-menu-group> <bento-navigation-menu-group label="Reports" > <template #icon> <nav-reports-icon /> </template> <bento-navigation-menu-item nested value="analytics" label="Analytics" :link="{ to: '#analytics', isNotRouting: true }" :always-show-actions="!!favorites.analytics || activePage === 'analytics'" > <template #action> <bento-button variant="tertiary" @click.stop.prevent="toggleFavorite('analytics')" > <template #iconLeft> <star-filled-icon style="color: var(--b-color-decorative-green);" v-if="favorites.analytics" /> <star-icon v-else /> </template> </bento-button> </template> </bento-navigation-menu-item> <bento-navigation-menu-item nested value="transactions" label="Transactions" :link="{ to: '#transactions', isNotRouting: true }" :always-show-actions="!!favorites.transactions || activePage === 'transactions'" > <template #action> <bento-button variant="tertiary" @click.stop.prevent="toggleFavorite('transactions')" > <template #iconLeft> <star-filled-icon style="color: var(--b-color-decorative-green);" v-if="favorites.transactions" /> <star-icon v-else /> </template> </bento-button> </template> </bento-navigation-menu-item> </bento-navigation-menu-group> <bento-navigation-menu-item value="settings" label="Settings" :link="{ to: '#settings', isNotRouting: true }" :always-show-actions="!!favorites.settings || activePage === 'settings'" > <template #icon> <nav-settings-icon /> </template> <template #action> <bento-button variant="tertiary" @click.stop.prevent="toggleFavorite('settings')" > <template #iconLeft> <star-filled-icon style="color: var(--b-color-decorative-green);" v-if="favorites.settings" /> <star-icon v-else /> </template> </bento-button> </template> </bento-navigation-menu-item> <bento-link to="#profile" is-not-routing disable-typography> <bento-navigation-menu-item value="profile" label="Profile" :always-show-actions="!!favorites.profile || activePage === 'profile'" > <template #icon> <nav-account-holders-icon /> </template> <template #action> <bento-button variant="tertiary" @click.stop.prevent="toggleFavorite('profile')" > <template #iconLeft> <star-filled-icon style="color: var(--b-color-decorative-green);" v-if="favorites.profile" /> <star-icon v-else /> </template> </bento-button> </template> </bento-navigation-menu-item> </bento-link> </bento-navigation-menu> </div> `, }), args: { label: 'Pages', showExpandAllButton: true, }, parameters: storybookDocsParameter(NavigationMenuEverythingBagelExample), }; export const SimpleList: Story = { render: (_args, { argTypes }) => ({ components: { BentoNavigationMenu, BentoNavigationMenuItem, StarIcon, }, props: Object.keys(argTypes), setup(props) { const activePage = ref('home'); return { activePage, args: isVue2 ? props : _args }; }, template: ` <div style="width: 248px;"> <bento-navigation-menu v-bind="args" :active-page="activePage" @update:active-page="activePage = $event"> <bento-navigation-menu-item value="home" label="Home" /> <bento-navigation-menu-item value="analytics" label="Analytics" /> <bento-navigation-menu-item value="settings" label="Settings" /> <bento-navigation-menu-item value="profile" label="Profile" /> </bento-navigation-menu> </div> `, }), args: { label: 'Pages', }, parameters: storybookDocsParameter(NavigationMenuSimpleListExample), };
1
+ import BentoNavigationMenu from './navigation-menu.vue'; import BentoNavigationMenuItem from './components/navigation-menu-item/navigation-menu-item.vue'; import BentoNavigationMenuGroup from './components/navigation-menu-group/navigation-menu-group.vue'; import { BentoButton } from '@/components/button'; import type { Meta, StoryObj } from '@storybook/vue'; import StarIcon from '@adyen/ui-assets-icons-16/vue/star'; import StarFilledIcon from '@adyen/ui-assets-icons-16/vue/star-filled'; import SearchIcon from '@adyen/ui-assets-icons-16/vue/search'; import NavHomeIcon from '@adyen/ui-assets-icons-16/vue/nav-home'; import NavPaymentsIcon from '@adyen/ui-assets-icons-16/vue/nav-payments'; import NavReportsIcon from '@adyen/ui-assets-icons-16/vue/nav-reports'; import NavSettingsIcon from '@adyen/ui-assets-icons-16/vue/nav-settings'; import NavAccountHoldersIcon from '@adyen/ui-assets-icons-16/vue/nav-account-holders'; import { ref } from 'vue'; import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import NavigationMenuEverythingBagelExample from './__tests__/navigation-menu-everything-bagel-example.vue?raw'; import NavigationMenuSimpleListExample from './__tests__/navigation-menu-simple-list-example.vue?raw'; export default { title: 'Navigation Menu', component: BentoNavigationMenu, argTypes: { default: { control: false, table: { type: { summary: 'Navigation items and groups' }, }, }, headerActions: { control: false, table: { type: { summary: 'Action buttons' }, }, }, }, } as Meta; type Story = StoryObj<typeof BentoNavigationMenu>; export const EverythingBagel: Story = { render: (_args, { argTypes }) => ({ components: { BentoNavigationMenu, BentoNavigationMenuItem, BentoNavigationMenuGroup, BentoButton, StarIcon, StarFilledIcon, SearchIcon, NavHomeIcon, NavPaymentsIcon, NavReportsIcon, NavSettingsIcon, NavAccountHoldersIcon, }, props: Object.keys(argTypes), setup(props) { const activePage = ref('home'); const paymentsExpanded = ref(true); const favorites = ref({}); const toggleFavorite = (value: string) => { favorites.value = { ...favorites.value, [value]: !favorites.value[value] }; }; const onExpandAll = () => action('expand-all')(); const onCollapseAll = () => action('collapse-all')(); return { activePage, paymentsExpanded, favorites, toggleFavorite, onExpandAll, onCollapseAll, args: isVue2 ? props : _args, }; }, template: ` <div style="width: 248px;"> <bento-navigation-menu v-bind="args" :active-page="activePage" @update:active-page="activePage = $event" @expand-all="onExpandAll" @collapse-all="onCollapseAll"> <template #headerActions> <bento-button variant="tertiary-with-background"> <template #iconLeft> <search-icon svg-title="Search" /> </template> </bento-button> </template> <bento-navigation-menu-item value="home" label="Home" :link="{ to: '#home', isNotRouting: true }" > <template #icon> <nav-home-icon /> </template> </bento-navigation-menu-item> <bento-navigation-menu-group label="Payments" :is-expanded="paymentsExpanded" @update:is-expanded="paymentsExpanded = $event" > <template #icon> <nav-payments-icon /> </template> <bento-navigation-menu-item nested value="payments" label="Payments" :link="{ to: '#payments', isNotRouting: true }" :always-show-actions="!!favorites.payments || activePage === 'payments'" > <template #action> <bento-button variant="tertiary" @click.stop.prevent="toggleFavorite('payments')" > <template #iconLeft> <star-filled-icon style="color: var(--b-color-decorative-green);" v-if="favorites.payments" svg-title="Remove from favorites" /> <star-icon v-else svg-title="Add to favorites" /> </template> </bento-button> </template> </bento-navigation-menu-item> <bento-navigation-menu-item nested value="preauth" label="Pre-authorized" :link="{ to: '#preauth', isNotRouting: true }" :always-show-actions="!!favorites.preauth || activePage === 'preauth'" > <template #action> <bento-button variant="tertiary" @click.stop.prevent="toggleFavorite('preauth')" > <template #iconLeft> <star-filled-icon style="color: var(--b-color-decorative-green);" v-if="favorites.preauth" svg-title="Remove from favorites" /> <star-icon v-else svg-title="Add to favorites" /> </template> </bento-button> </template> </bento-navigation-menu-item> <bento-navigation-menu-item nested value="links" label="Payment links" :link="{ to: '#links', isNotRouting: true }" :always-show-actions="!!favorites.links || activePage === 'links'" > <template #action> <bento-button variant="tertiary" @click.stop.prevent="toggleFavorite('links')" > <template #iconLeft> <star-filled-icon style="color: var(--b-color-decorative-green);" v-if="favorites.links" svg-title="Remove from favorites" /> <star-icon v-else svg-title="Add to favorites" /> </template> </bento-button> </template> </bento-navigation-menu-item> <bento-navigation-menu-item nested value="methods" label="Payment methods" :link="{ to: '#methods', isNotRouting: true }" :always-show-actions="!!favorites.methods || activePage === 'methods'" > <template #action> <bento-button variant="tertiary" @click.stop.prevent="toggleFavorite('methods')" > <template #iconLeft> <star-filled-icon style="color: var(--b-color-decorative-green);" v-if="favorites.methods" svg-title="Remove from favorites" /> <star-icon v-else svg-title="Add to favorites" /> </template> </bento-button> </template> </bento-navigation-menu-item> </bento-navigation-menu-group> <bento-navigation-menu-group label="Reports" > <template #icon> <nav-reports-icon /> </template> <bento-navigation-menu-item nested value="analytics" label="Analytics" :link="{ to: '#analytics', isNotRouting: true }" :always-show-actions="!!favorites.analytics || activePage === 'analytics'" > <template #action> <bento-button variant="tertiary" @click.stop.prevent="toggleFavorite('analytics')" > <template #iconLeft> <star-filled-icon style="color: var(--b-color-decorative-green);" v-if="favorites.analytics" svg-title="Remove from favorites" /> <star-icon v-else svg-title="Add to favorites" /> </template> </bento-button> </template> </bento-navigation-menu-item> <bento-navigation-menu-item nested value="transactions" label="Transactions" :link="{ to: '#transactions', isNotRouting: true }" :always-show-actions="!!favorites.transactions || activePage === 'transactions'" > <template #action> <bento-button variant="tertiary" @click.stop.prevent="toggleFavorite('transactions')" > <template #iconLeft> <star-filled-icon style="color: var(--b-color-decorative-green);" v-if="favorites.transactions" svg-title="Remove from favorites" /> <star-icon v-else svg-title="Add to favorites" /> </template> </bento-button> </template> </bento-navigation-menu-item> </bento-navigation-menu-group> <bento-navigation-menu-item value="settings" label="Settings" :link="{ to: '#settings', isNotRouting: true }" :always-show-actions="!!favorites.settings || activePage === 'settings'" > <template #icon> <nav-settings-icon /> </template> <template #action> <bento-button variant="tertiary" @click.stop.prevent="toggleFavorite('settings')" > <template #iconLeft> <star-filled-icon style="color: var(--b-color-decorative-green);" v-if="favorites.settings" svg-title="Remove from favorites" /> <star-icon v-else svg-title="Add to favorites" /> </template> </bento-button> </template> </bento-navigation-menu-item> <bento-navigation-menu-item value="profile" label="Profile" :link="{ to: '#profile', isNotRouting: true }" :always-show-actions="!!favorites.profile || activePage === 'profile'" > <template #icon> <nav-account-holders-icon /> </template> <template #action> <bento-button variant="tertiary" @click.stop.prevent="toggleFavorite('profile')" > <template #iconLeft> <star-filled-icon style="color: var(--b-color-decorative-green);" v-if="favorites.profile" svg-title="Remove from favorites" /> <star-icon v-else svg-title="Add to favorites" /> </template> </bento-button> </template> </bento-navigation-menu-item> </bento-navigation-menu> </div> `, }), args: { label: 'Pages', showExpandAllButton: true, }, parameters: storybookDocsParameter(NavigationMenuEverythingBagelExample), }; export const SimpleList: Story = { render: (_args, { argTypes }) => ({ components: { BentoNavigationMenu, BentoNavigationMenuItem, StarIcon, }, props: Object.keys(argTypes), setup(props) { const activePage = ref('home'); return { activePage, args: isVue2 ? props : _args }; }, template: ` <div style="width: 248px;"> <bento-navigation-menu v-bind="args" :active-page="activePage" @update:active-page="activePage = $event"> <bento-navigation-menu-item value="home" label="Home" :link="{ to: '#home', isNotRouting: true }" /> <bento-navigation-menu-item value="analytics" label="Analytics" :link="{ to: '#analytics', isNotRouting: true }" /> <bento-navigation-menu-item value="settings" label="Settings" :link="{ to: '#settings', isNotRouting: true }" /> <bento-navigation-menu-item value="profile" label="Profile" :link="{ to: '#profile', isNotRouting: true }" /> </bento-navigation-menu> </div> `, }), args: { label: 'Pages', }, parameters: storybookDocsParameter(NavigationMenuSimpleListExample), };
@@ -1 +1 @@
1
- import { type Ref } from 'vue'; export interface BentoNavigationMenuProps { /** * Label text for the navigation menu header */ label?: string; /** * Whether to show the expand/collapse all button */ showExpandAllButton?: boolean; /** * The value of the currently active navigation item */ activePage?: string; } /** * @private */ export interface BentoNavigationMenuGroupRegistration { id: string; setOpen: (open: boolean) => void; getIsOpen: () => boolean; } /** * @private */ export interface BentoNavigationMenuState { registerGroup: (groupId: string, setOpen: (open: boolean) => void, getIsOpen: () => boolean) => void; unregisterGroup: (groupId: string) => void; activePage: Ref<string | undefined>; updateActivePage: (value: string) => void; }
1
+ import { type Ref } from 'vue'; export interface BentoNavigationMenuProps { /** * Label text for the navigation menu header */ label?: string; /** * Whether to show the expand/collapse all button */ showExpandAllButton?: boolean; /** * The value of the currently active navigation item */ activePage?: string; } export interface BentoNavigationMenuEmits { /** * Emits update event when active page is changed */ (e: 'update:active-page', value: string): void; /** * Emits event when expand all button is clicked */ (e: 'expand-all'): void; /** * Emits event when collapse all button is clicked */ (e: 'collapse-all'): void; } /** * @private */ export interface BentoNavigationMenuGroupRegistration { id: string; setOpen: (open: boolean) => void; getIsOpen: () => boolean; } /** * @private */ export interface BentoNavigationMenuState { registerGroup: (groupId: string, setOpen: (open: boolean) => void, getIsOpen: () => boolean) => void; unregisterGroup: (groupId: string) => void; activePage: Ref<string | undefined>; updateActivePage: (value: string) => void; }
@@ -1 +1 @@
1
- <template> <nav class="b-navigation-menu" :aria-labelledby="label ? titleId : undefined"> <div v-if="showHeader" class="b-navigation-menu__header"> <bento-typography v-if="label" :id="titleId" el="p" variant="caption" stronger class="b-navigation-menu__title" > {{ label }} </bento-typography> <div class="b-navigation-menu__actions-container"> <div v-if="hasSlot('headerActions')" class="b-navigation-menu__actions"> <slot name="headerActions" /> </div> <bento-button v-if="computedShowExpandAll" v-bento-tooltip-directive="expandButtonLabel" variant="tertiary-with-background" :aria-label="expandButtonLabel" @click="toggleAllGroups" > <template #iconLeft> <menu-collapse-icon v-if="allExpanded" /> <menu-expand-icon v-else /> </template> </bento-button> </div> </div> <ul class="b-navigation-menu__content"> <slot /> </ul> </nav> </template> <script setup lang="ts"> import { computed, provide, ref, toRef, useSlots } from 'vue'; import { BentoTypography } from '@/components/typography'; import { BentoButton } from '@/components/button'; import MenuCollapseIcon from '@adyen/ui-assets-icons-16/vue/menu-collapse'; import MenuExpandIcon from '@adyen/ui-assets-icons-16/vue/menu-expand'; import { useHasSlot } from '@/composables'; import { BentoTooltipDirective as vBentoTooltipDirective } from '@/directives/tooltip'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import type { BentoNavigationMenuGroupRegistration, BentoNavigationMenuProps, BentoNavigationMenuState, } from './navigation-menu.types'; import { NAVIGATION_MENU_STATE_INJECTION_KEY } from './navigation-menu.keys'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoNavigationMenuProps>(), { label: undefined, showExpandAllButton: false, activePage: undefined, }); const emit = defineEmits<{ /** * Emits update event when active page is changed */ (e: 'update:active-page', value: string): void; }>(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const titleId = generateUid('navigation-menu-title'); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const groups = ref<Array<BentoNavigationMenuGroupRegistration>>([]); const registerGroup = (groupId: string, setOpen: (open: boolean) => void, getIsOpen: () => boolean) => { groups.value.push({ id: groupId, setOpen, getIsOpen }); }; const unregisterGroup = (groupId: string) => { groups.value = groups.value.filter(g => g.id !== groupId); }; const allExpanded = computed(() => groups.value.length > 0 && groups.value.every(group => group.getIsOpen())); const computedShowExpandAll = computed(() => groups.value.length > 0 && props.showExpandAllButton); const showHeader = computed(() => props.label || hasSlot('headerActions') || computedShowExpandAll.value); const expandButtonLabel = computed(() => (allExpanded.value ? t('hideAllPages') : t('showAllPages'))); const toggleAllGroups = () => { const shouldExpand = !allExpanded.value; groups.value.forEach(({ setOpen }) => setOpen(shouldExpand)); }; const updateActivePage = (value: string) => { emit('update:active-page', value); }; provide<BentoNavigationMenuState>(NAVIGATION_MENU_STATE_INJECTION_KEY, { registerGroup, unregisterGroup, activePage: toRef(props, 'activePage'), updateActivePage, }); </script> <script lang="ts"> /** * Navigation menu container component with optional header and action buttons. * * @usage * import { BentoNavigationMenu, BentoNavigationMenuItem, BentoNavigationMenuGroup } from '@adyen/bento-vue2'; * * export default { * components: { BentoNavigationMenu, BentoNavigationMenuItem, BentoNavigationMenuGroup }, * template: ` * <bento-navigation-menu label="Pages" active-page="home"> * <template #headerActions> * <button>Search</button> * </template> * <bento-navigation-menu-item value="home" label="Home"> * <template #icon><home-icon /></template> * </bento-navigation-menu-item> * <bento-navigation-menu-group label="Settings"> * <template #icon><folder-icon /></template> * <bento-navigation-menu-item nested value="general" label="General" /> * </bento-navigation-menu-group> * </bento-navigation-menu> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./navigation-menu.scss" />
1
+ <template> <nav class="b-navigation-menu" :aria-labelledby="label ? titleId : undefined"> <div v-if="showHeader" class="b-navigation-menu__header"> <bento-typography v-if="label" :id="titleId" el="p" variant="caption" stronger class="b-navigation-menu__title" > {{ label }} </bento-typography> <div class="b-navigation-menu__actions-container"> <div v-if="hasSlot('headerActions')" class="b-navigation-menu__actions"> <slot name="headerActions" /> </div> <bento-button v-if="computedShowExpandAll" v-bento-tooltip-directive="expandButtonLabel" variant="tertiary-with-background" :aria-label="expandButtonLabel" @click="toggleAllGroups" > <template #iconLeft> <menu-collapse-icon v-if="allExpanded" aria-hidden="true" /> <menu-expand-icon v-else aria-hidden="true" /> </template> </bento-button> </div> </div> <ul class="b-navigation-menu__content"> <slot /> </ul> </nav> </template> <script setup lang="ts"> import { computed, provide, ref, toRef, useSlots } from 'vue'; import { BentoTypography } from '@/components/typography'; import { BentoButton } from '@/components/button'; import MenuCollapseIcon from '@adyen/ui-assets-icons-16/vue/menu-collapse'; import MenuExpandIcon from '@adyen/ui-assets-icons-16/vue/menu-expand'; import { useHasSlot } from '@/composables'; import { BentoTooltipDirective as vBentoTooltipDirective } from '@/directives/tooltip'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import type { BentoNavigationMenuEmits, BentoNavigationMenuGroupRegistration, BentoNavigationMenuProps, BentoNavigationMenuState, } from './navigation-menu.types'; import { NAVIGATION_MENU_STATE_INJECTION_KEY } from './navigation-menu.keys'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoNavigationMenuProps>(), { label: undefined, showExpandAllButton: false, activePage: undefined, }); const emit = defineEmits<BentoNavigationMenuEmits>(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const titleId = generateUid('navigation-menu-title'); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const groups = ref<Array<BentoNavigationMenuGroupRegistration>>([]); const registerGroup = (groupId: string, setOpen: (open: boolean) => void, getIsOpen: () => boolean) => { groups.value.push({ id: groupId, setOpen, getIsOpen }); }; const unregisterGroup = (groupId: string) => { groups.value = groups.value.filter(g => g.id !== groupId); }; const allExpanded = computed(() => groups.value.length > 0 && groups.value.every(group => group.getIsOpen())); const computedShowExpandAll = computed(() => groups.value.length > 0 && props.showExpandAllButton); const showHeader = computed(() => props.label || hasSlot('headerActions') || computedShowExpandAll.value); const expandButtonLabel = computed(() => (allExpanded.value ? t('hideAllPages') : t('showAllPages'))); const toggleAllGroups = () => { const shouldExpand = !allExpanded.value; groups.value.forEach(({ setOpen }) => setOpen(shouldExpand)); if (shouldExpand) { emit('expand-all'); } else { emit('collapse-all'); } }; const updateActivePage = (value: string) => { emit('update:active-page', value); }; provide<BentoNavigationMenuState>(NAVIGATION_MENU_STATE_INJECTION_KEY, { registerGroup, unregisterGroup, activePage: toRef(props, 'activePage'), updateActivePage, }); </script> <script lang="ts"> /** * Navigation menu container component with optional header and action buttons. * * @usage * import { BentoNavigationMenu, BentoNavigationMenuItem, BentoNavigationMenuGroup } from '@adyen/bento-vue2'; * * export default { * components: { BentoNavigationMenu, BentoNavigationMenuItem, BentoNavigationMenuGroup }, * template: ` * <bento-navigation-menu label="Pages" active-page="home"> * <template #headerActions> * <button>Search</button> * </template> * <bento-navigation-menu-item value="home" label="Home" :link="{ to: '/home' }"> * <template #icon><home-icon /></template> * </bento-navigation-menu-item> * <bento-navigation-menu-group label="Settings"> * <template #icon><folder-icon /></template> * <bento-navigation-menu-item nested value="general" label="General" :link="{ to: '/settings/general' }" /> * </bento-navigation-menu-group> * </bento-navigation-menu> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./navigation-menu.scss" />
@@ -1,4 +1,4 @@
1
- import { Canvas, Story, Meta } from '@storybook/blocks';
1
+ import { Canvas, Source, Story, Meta } from '@storybook/blocks';
2
2
  import * as ComponentStories from './popover.stories';
3
3
 
4
4
  <Meta of={ComponentStories} />
@@ -14,8 +14,8 @@ control or interactive area.
14
14
 
15
15
  Use popover when you need:
16
16
 
17
- - Provide a piece of non-critical extra information (informative, educative).
18
- - Provide contextual to trigger functionality.
17
+ - Provide a piece of non-critical extra information (informative, educative).
18
+ - Provide contextual to trigger functionality.
19
19
 
20
20
  ### Do not use
21
21
 
@@ -36,11 +36,11 @@ Using the `title` prop and `headerIcon` slot we can create a header for the Popo
36
36
  Using the `actions` prop we can provide an array of action objects (see the Button Actions story). The `actionsLayout`
37
37
  prop can be used to define the layout of buttons and can take up one of the following values:
38
38
 
39
- - `'buttons-end'`
40
- - `'fill-container'`
41
- - `'space-between'`
42
- - `'vertical-stack'`
43
- - `'buttons-start'`
39
+ - `'buttons-end'`
40
+ - `'fill-container'`
41
+ - `'space-between'`
42
+ - `'vertical-stack'`
43
+ - `'buttons-start'`
44
44
 
45
45
  ### Image
46
46
 
@@ -48,6 +48,69 @@ prop can be used to define the layout of buttons and can take up one of the foll
48
48
 
49
49
  Using the `image` slot you can set a header image.
50
50
 
51
+ ### Triggering on hover
52
+
53
+ <Canvas of={ComponentStories.OnHover} />
54
+
55
+ The popover can be opened on hover by using the `useBentoDelayedHover` composable. It returns the `isOpen` ref, the
56
+ individual handlers (`open`, `close`), and a `hoverEvents` object that bundles them as `mouseenter`/`mouseleave`/
57
+ `focusin`/`focusout` listeners. Mouse and focus events are handled, so the popover also opens when the trigger receives
58
+ keyboard focus, and a small closing delay is applied to prevent flickering when the user moves the cursor or focus from
59
+ the trigger to the popover content.
60
+
61
+ Bind the handlers on the trigger via `v-on="hoverEvents"`:
62
+
63
+ <Source
64
+ format={'html'}
65
+ dark
66
+ code={`
67
+ <span ref="triggerRef" v-on="hoverEvents">Hover me</span>
68
+ `}
69
+ />
70
+
71
+ When the popover is **NOT** a descendant of the trigger element (for example, rendered as a sibling or teleported
72
+ elsewhere in the DOM), also bind `hoverEvents` on the popover so that hovering or focusing its content keeps it open:
73
+
74
+ For hover-triggered popovers, pass `disable-focus-trap`. A focus trap can move focus back into the popover while the
75
+ delayed hover handlers are trying to close it, which can cause unexpected open/close behavior when multiple
76
+ hover-triggered popovers are present.
77
+
78
+ <Source
79
+ format={'html'}
80
+ dark
81
+ code={`
82
+ <span ref="triggerRef" v-on="hoverEvents">Hover me</span>
83
+
84
+ <bento-popover v-on="hoverEvents" :open="isOpen" :target-element="triggerRef" disable-focus-trap>
85
+ Popover content
86
+ </bento-popover>
87
+ `}
88
+ />
89
+
90
+ When the popover is rendered as a descendant of the trigger element, this is not necessary — the trigger's own
91
+ `mouseleave` will not fire while the cursor is over its descendants.
92
+
93
+ > **Vue 2 only:** when binding on a Vue component (e.g. `bento-button` or `bento-popover`) rather than a native HTML
94
+ > element, `v-on` with object syntax does not attach the listeners to the underlying DOM element — the `.native`
95
+ > modifier is required for that, and it cannot be combined with the object form of `v-on`. To work around this, either
96
+ > wrap the component in a native element and use `v-on="hoverEvents"` on the wrapper, or break the object up and bind
97
+ > the handlers individually with `.native`. Neither workaround is needed in Vue 3, where listeners on a component
98
+ > automatically fall through to the root element.
99
+
100
+ <Source
101
+ format={'html'}
102
+ dark
103
+ code={`
104
+ <!-- Option A: wrap in a native element -->
105
+ <span v-on="hoverEvents" tabindex="0">
106
+ <bento-tag ref="triggerRef" label="Hover me" />
107
+ </span>
108
+
109
+ <!-- Option B: bind events individually with .native -->
110
+
111
+ <bento-tag ref="triggerRef" label="Hover me" tabindex="0" @mouseenter.native="open" @mouseleave.native="close"
112
+ @focusin.native="open" @focusout.native="close" /> `} />
113
+
51
114
  ## Accessibility 🚧
52
115
 
53
116
  NOTE: The focus trap within the popover does not function as it should right now, this issue is currently being worked
@@ -71,12 +134,12 @@ documentation provided.
71
134
 
72
135
  #### Please note that:
73
136
 
74
- - The element that shows and hides the content has role button, e.g. use the `BentoButton` component.
75
- - When the Popover is visible, the element with role button has `aria-expanded` set to `true`. When the content area
76
- is hidden, it is set to `false`.
77
- - When the Popover is visible, the popover needs to be identified by an `id` and the trigger element with role button
78
- needs to have `aria-controls` set to the popover `id`. When the popover is hidden, the `aria-controls` attribute
79
- needs to be set to `null`.
137
+ - The element that shows and hides the content has role button, e.g. use the `BentoButton` component.
138
+ - When the Popover is visible, the element with role button has `aria-expanded` set to `true`. When the content area is
139
+ hidden, it is set to `false`.
140
+ - When the Popover is visible, the popover needs to be identified by an `id` and the trigger element with role button
141
+ needs to have `aria-controls` set to the popover `id`. When the popover is hidden, the `aria-controls` attribute needs
142
+ to be set to `null`.
80
143
 
81
144
  | ARIA attribute | is automatic? | fallback used | manual override |
82
145
  | --------------- | ------------------------------------------------------------------------ | ------------- | --------------- |
@@ -86,4 +149,4 @@ documentation provided.
86
149
 
87
150
  ## Resources
88
151
 
89
- - [Figma link](https://www.figma.com/file/uLabwF3243jdMDsNSP7U9I/branch/xxUamzpDM5NlukATVD9JFT/Bento---Components?type=design&node-id=1913%3A27764&mode=design&t=cY18II3hTxXGJeoI-1)
152
+ - [Figma link](https://www.figma.com/file/uLabwF3243jdMDsNSP7U9I/branch/xxUamzpDM5NlukATVD9JFT/Bento---Components?type=design&node-id=1913%3A27764&mode=design&t=cY18II3hTxXGJeoI-1)
@@ -1 +1 @@
1
- import { onMounted, ref, watch } from 'vue'; import { isVue2 } from 'vue-demi'; import { type Meta, type StoryObj } from '@storybook/vue'; import { storybookDocsParameter } from '../../utils/ts/storybook'; import { action } from '@storybook/addon-actions'; import { BentoButton, BentoButtonActionsLayoutBasic } from '@/components/button'; import { BentoTypography } from '@/components/typography'; import { BentoPopoverImage } from './components/popover-image'; import BentoPopover from './popover.vue'; import InfoFilledIcon from '@adyen/ui-assets-icons-16/vue/info-filled'; import { BentoPopoverPositions } from './popover.types'; import { popoverImageSrcStub } from '../../__tests__/img-stub.json'; import './popover.stories.scss?module'; const DEFAULT_TEXT = 'Add your text here'; const TITLE_TEXT = 'Title'; const IMAGE_SRC = popoverImageSrcStub; const IMAGE_ALT = 'Example image'; const ID = 'popover-id'; const ACTIONS = [ { title: 'Primary', event: () => action('Primary')(), }, { title: 'Secondary', event: () => action('Secondary')(), }, ]; const DEFAULT_PROPS = { position: 'bottom-start', disableFocusTrap: true, default: DEFAULT_TEXT, }; export default { title: 'Popover', component: BentoPopover, parameters: { layout: 'centered', }, argTypes: { // Slots default: { defaultValue: '', description: 'Popover content', table: { type: { summary: 'VNode[]' }, }, control: { type: 'text' }, }, headerIcon: { defaultValue: '', description: 'Vue Icon from the library @adyen/ui-assets-icons-16.', table: { type: { summary: 'VNode[]' }, }, control: { disable: true }, }, image: { defaultValue: '', description: 'Popover image container', table: { type: { summary: 'BentoPopoverImage' }, }, control: { disable: true }, }, // Enum props actionsLayout: { options: Object.values(BentoButtonActionsLayoutBasic), mapping: BentoButtonActionsLayoutBasic, control: { type: 'select', }, }, position: { options: Object.values(BentoPopoverPositions), mapping: BentoPopoverPositions, control: { type: 'select', }, }, }, } as Meta; type Story = StoryObj<typeof BentoPopover>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoPopover, BentoTypography, }, props: Object.keys(argTypes), template: ` <div class="b-popover-story--default"> <bento-popover v-if="targetElement" :open="isOpen" v-bind="args" :targetElement="targetElement" @placement="onPlacementUpdate" > <bento-typography el="span"> {{args.default}} </bento-typography> </bento-popover> <bento-button ref="targetElement" @click="togglePopover" :aria-expanded="isOpen" aria-controls="${ID}" > Open popover </bento-button> </div> `, setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(false); const targetElement = ref(null); const togglePopover = () => { isOpen.value = !isOpen.value; }; watch( () => args.open, (val: boolean) => { isOpen.value = val; } ); const show = () => action('input'); onMounted(() => (isOpen.value = args.open === undefined ? true : args.open)); const onPlacementUpdate = action('placement'); return { // Values args, isOpen, targetElement, // Methods onPlacementUpdate, show, togglePopover, }; }, }), args: { ...DEFAULT_PROPS, id: ID }, parameters: storybookDocsParameter(` <div> <bento-popover v-if="targetElement" id="${ID}" :open="isOpen" position="bottom-start" :targetElement="targetElement" > <bento-typography el="span"> ${DEFAULT_TEXT} </bento-typography> </bento-popover> <bento-button ref="targetElement" :aria-expanded="isOpen" @click="togglePopover" aria-controls="${ID}" > Open popover </bento-button> </div> `), }; export const WithHeader: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoPopover, BentoTypography, InfoFilledIcon, }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(false); const targetElement = ref(null); const togglePopover = () => { isOpen.value = !isOpen.value; }; watch( () => args.open, (val: boolean) => { isOpen.value = val; } ); const show = () => action('input'); const dismissPopover = () => { action('dismiss')(); isOpen.value = false; }; onMounted(() => (isOpen.value = args.open === undefined ? true : args.open)); return { // Values args, isOpen, targetElement, // Methods togglePopover, show, dismissPopover, }; }, template: ` <div class="b-popover-story--with-header"> <bento-popover v-if="targetElement" :open="isOpen" v-bind="args" :targetElement="targetElement" @dismiss="dismissPopover" > <template #headerIcon> <info-filled-icon /> </template> <bento-typography el="span"> {{args.default}} </bento-typography> </bento-popover> <bento-button ref="targetElement" @click="togglePopover" :aria-expanded="isOpen" aria-controls="${ID}" > Open popover </bento-button> </div> `, }), args: { ...DEFAULT_PROPS, title: TITLE_TEXT, id: ID }, parameters: storybookDocsParameter(` <div> <bento-popover v-if="targetElement" id="${ID}" :open="isOpen" title="${TITLE_TEXT}" position="bottom-start" :targetElement="targetElement" @dismiss="dismissPopover" > <template #headerIcon> <info-filled-icon /> </template> <bento-typography el="span"> ${DEFAULT_TEXT} </bento-typography> </bento-popover> <bento-button ref="targetElement" :aria-expanded="isOpen" @click="togglePopover" aria-controls="${ID}" > Open popover </bento-button> </div> `), }; export const WithActions: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoPopover, BentoTypography, InfoFilledIcon, }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(false); const targetElement = ref(null); const togglePopover = () => { isOpen.value = !isOpen.value; }; watch( () => args.open, (val: boolean) => { isOpen.value = val; } ); const show = () => action('input'); const dismissPopover = () => { action('dismiss')(); isOpen.value = false; }; onMounted(() => (isOpen.value = args.open === undefined ? true : args.open)); return { // Values args, isOpen, targetElement, // Methods togglePopover, show, dismissPopover, }; }, template: ` <div class="b-popover-story--with-actions"> <bento-popover v-if="targetElement" :open="isOpen" v-bind="args" :targetElement="targetElement" @dismiss="dismissPopover" > <bento-typography el="span"> {{args.default}} </bento-typography> </bento-popover> <bento-button ref="targetElement" @click="togglePopover" :aria-expanded="isOpen" aria-controls="${ID}" > Open popover </bento-button> </div> `, }), args: { ...DEFAULT_PROPS, title: TITLE_TEXT, actions: ACTIONS, id: ID, }, parameters: storybookDocsParameter(` <div> <bento-popover v-if="targetElement" id="${ID}" :open="isOpen" title="${TITLE_TEXT}" :actions="[ { title: 'Primary', event: () => action('Primary')(), }, { title: 'Secondary', event: () => action('Secondary')(), }, ]" position="bottom-start" :targetElement="targetElement" @dismiss="dismissPopover" > <template #headerIcon> <info-filled-icon /> </template> <bento-typography el="span"> ${DEFAULT_TEXT} </bento-typography> </bento-popover> <bento-button ref="targetElement" :aria-expanded="isOpen" @click="togglePopover" aria-controls="${ID}" > Open popover </bento-button> </div> `), }; export const WithImage: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoPopover, BentoPopoverImage, BentoTypography, InfoFilledIcon, }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(false); const targetElement = ref(null); const togglePopover = () => { isOpen.value = !isOpen.value; }; watch( () => args.open, (val: boolean) => { isOpen.value = val; } ); const show = () => action('input'); const dismissPopover = () => { action('dismiss')(); isOpen.value = false; }; onMounted(() => (isOpen.value = args.open === undefined ? true : args.open)); return { // Values args, isOpen, targetElement, // Methods togglePopover, show, dismissPopover, }; }, template: ` <div class="b-popover-story--with-image"> <bento-popover v-if="targetElement" :open="isOpen" v-bind="args" :targetElement="targetElement" @dismiss="dismissPopover" > <template #image> <bento-popover-image src="${IMAGE_SRC}" alt="${IMAGE_ALT}" /> </template> <bento-typography el="span"> {{args.default}} </bento-typography> </bento-popover> <bento-button ref="targetElement" @click="togglePopover" :aria-expanded="isOpen" aria-controls="${ID}" > Open popover </bento-button> </div> `, }), args: { ...DEFAULT_PROPS, title: TITLE_TEXT, actions: ACTIONS, id: ID, }, parameters: storybookDocsParameter(` <div> <bento-popover v-if="targetElement" id="${ID}" :open="isOpen" title="${TITLE_TEXT}" :actions="[ { title: 'Primary', event: () => action('Primary')(), }, { title: 'Secondary', event: () => action('Secondary')(), }, ]" position="bottom-start" :targetElement="targetElement" @dismiss="dismissPopover" > <template #image> <bento-popover-image src="${IMAGE_SRC}" alt="${IMAGE_ALT}" /> </template> <bento-typography el="span"> ${DEFAULT_TEXT} </bento-typography> </bento-popover> <bento-button ref="targetElement" :aria-expanded="isOpen" @click="togglePopover" aria-controls="${ID}" > Open popover </bento-button> </div> `), };
1
+ import { onMounted, ref, watch } from 'vue'; import { isVue2 } from 'vue-demi'; import { type Meta, type StoryObj } from '@storybook/vue'; import { storybookDocsParameter } from '../../utils/ts/storybook'; import { action } from '@storybook/addon-actions'; import { BentoButton, BentoButtonActionsLayoutBasic } from '@/components/button'; import { BentoLink } from '@/components/link'; import { BentoTag } from '@/components/tag'; import { BentoTypography } from '@/components/typography'; import { useBentoDelayedHover } from '@/composables'; import { BentoPopoverImage } from './components/popover-image'; import BentoPopover from './popover.vue'; import InfoFilledIcon from '@adyen/ui-assets-icons-16/vue/info-filled'; import { BentoPopoverPositions } from './popover.types'; import { popoverImageSrcStub } from '../../__tests__/img-stub.json'; import BentoDelayedHoverExample from '@/composables/use-bento-delayed-hover/__tests__/use-bento-delayed-hover-example.vue?raw'; import './popover.stories.scss?module'; const DEFAULT_TEXT = 'Add your text here'; const TITLE_TEXT = 'Title'; const IMAGE_SRC = popoverImageSrcStub; const IMAGE_ALT = 'Example image'; const ID = 'popover-id'; const ACTIONS = [ { title: 'Primary', event: () => action('Primary')(), }, { title: 'Secondary', event: () => action('Secondary')(), }, ]; const DEFAULT_PROPS = { position: 'bottom-start', disableFocusTrap: true, default: DEFAULT_TEXT, }; export default { title: 'Popover', component: BentoPopover, parameters: { layout: 'centered', }, argTypes: { // Slots default: { defaultValue: '', description: 'Popover content', table: { type: { summary: 'VNode[]' }, }, control: { type: 'text' }, }, headerIcon: { defaultValue: '', description: 'Vue Icon from the library @adyen/ui-assets-icons-16.', table: { type: { summary: 'VNode[]' }, }, control: { disable: true }, }, image: { defaultValue: '', description: 'Popover image container', table: { type: { summary: 'BentoPopoverImage' }, }, control: { disable: true }, }, // Enum props actionsLayout: { options: Object.values(BentoButtonActionsLayoutBasic), mapping: BentoButtonActionsLayoutBasic, control: { type: 'select', }, }, position: { options: Object.values(BentoPopoverPositions), mapping: BentoPopoverPositions, control: { type: 'select', }, }, }, } as Meta; type Story = StoryObj<typeof BentoPopover>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoPopover, BentoTypography, }, props: Object.keys(argTypes), template: ` <div class="b-popover-story--default"> <bento-popover v-if="targetElement" :open="isOpen" v-bind="args" :targetElement="targetElement" @placement="onPlacementUpdate" > <bento-typography el="span"> {{args.default}} </bento-typography> </bento-popover> <bento-button ref="targetElement" @click="togglePopover" :aria-expanded="isOpen" aria-controls="${ID}" > Open popover </bento-button> </div> `, setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(false); const targetElement = ref(null); const togglePopover = () => { isOpen.value = !isOpen.value; }; watch( () => args.open, (val: boolean) => { isOpen.value = val; } ); const show = () => action('input'); onMounted(() => (isOpen.value = args.open === undefined ? true : args.open)); const onPlacementUpdate = action('placement'); return { // Values args, isOpen, targetElement, // Methods onPlacementUpdate, show, togglePopover, }; }, }), args: { ...DEFAULT_PROPS, id: ID }, parameters: storybookDocsParameter(` <div> <bento-popover v-if="targetElement" id="${ID}" :open="isOpen" position="bottom-start" :targetElement="targetElement" > <bento-typography el="span"> ${DEFAULT_TEXT} </bento-typography> </bento-popover> <bento-button ref="targetElement" :aria-expanded="isOpen" @click="togglePopover" aria-controls="${ID}" > Open popover </bento-button> </div> `), }; export const WithHeader: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoPopover, BentoTypography, InfoFilledIcon, }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(false); const targetElement = ref(null); const togglePopover = () => { isOpen.value = !isOpen.value; }; watch( () => args.open, (val: boolean) => { isOpen.value = val; } ); const show = () => action('input'); const dismissPopover = () => { action('dismiss')(); isOpen.value = false; }; onMounted(() => (isOpen.value = args.open === undefined ? true : args.open)); return { // Values args, isOpen, targetElement, // Methods togglePopover, show, dismissPopover, }; }, template: ` <div class="b-popover-story--with-header"> <bento-popover v-if="targetElement" :open="isOpen" v-bind="args" :targetElement="targetElement" @dismiss="dismissPopover" > <template #headerIcon> <info-filled-icon /> </template> <bento-typography el="span"> {{args.default}} </bento-typography> </bento-popover> <bento-button ref="targetElement" @click="togglePopover" :aria-expanded="isOpen" aria-controls="${ID}" > Open popover </bento-button> </div> `, }), args: { ...DEFAULT_PROPS, title: TITLE_TEXT, id: ID }, parameters: storybookDocsParameter(` <div> <bento-popover v-if="targetElement" id="${ID}" :open="isOpen" title="${TITLE_TEXT}" position="bottom-start" :targetElement="targetElement" @dismiss="dismissPopover" > <template #headerIcon> <info-filled-icon /> </template> <bento-typography el="span"> ${DEFAULT_TEXT} </bento-typography> </bento-popover> <bento-button ref="targetElement" :aria-expanded="isOpen" @click="togglePopover" aria-controls="${ID}" > Open popover </bento-button> </div> `), }; export const WithActions: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoPopover, BentoTypography, InfoFilledIcon, }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(false); const targetElement = ref(null); const togglePopover = () => { isOpen.value = !isOpen.value; }; watch( () => args.open, (val: boolean) => { isOpen.value = val; } ); const show = () => action('input'); const dismissPopover = () => { action('dismiss')(); isOpen.value = false; }; onMounted(() => (isOpen.value = args.open === undefined ? true : args.open)); return { // Values args, isOpen, targetElement, // Methods togglePopover, show, dismissPopover, }; }, template: ` <div class="b-popover-story--with-actions"> <bento-popover v-if="targetElement" :open="isOpen" v-bind="args" :targetElement="targetElement" @dismiss="dismissPopover" > <bento-typography el="span"> {{args.default}} </bento-typography> </bento-popover> <bento-button ref="targetElement" @click="togglePopover" :aria-expanded="isOpen" aria-controls="${ID}" > Open popover </bento-button> </div> `, }), args: { ...DEFAULT_PROPS, title: TITLE_TEXT, actions: ACTIONS, id: ID, }, parameters: storybookDocsParameter(` <div> <bento-popover v-if="targetElement" id="${ID}" :open="isOpen" title="${TITLE_TEXT}" :actions="[ { title: 'Primary', event: () => action('Primary')(), }, { title: 'Secondary', event: () => action('Secondary')(), }, ]" position="bottom-start" :targetElement="targetElement" @dismiss="dismissPopover" > <template #headerIcon> <info-filled-icon /> </template> <bento-typography el="span"> ${DEFAULT_TEXT} </bento-typography> </bento-popover> <bento-button ref="targetElement" :aria-expanded="isOpen" @click="togglePopover" aria-controls="${ID}" > Open popover </bento-button> </div> `), }; export const OnHover: Story = { render: (_args, { argTypes }) => ({ components: { BentoLink, BentoPopover, BentoTag, BentoTypography, }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const triggerElement = ref(null); const { isOpen, open, close, hoverEvents } = useBentoDelayedHover(); return { args, isOpen, open, close, triggerElement, hoverEvents, }; }, template: ` <div class="b-popover-story--on-hover"> <bento-tag ref="triggerElement" label="Hover over me" tabindex="0" :aria-expanded="isOpen.toString()" aria-controls="${ID}" @mouseenter.native="open" @mouseleave.native="close" @focusin.native="open" @focusout.native="close" /> <span v-on="hoverEvents"> <bento-popover v-if="triggerElement" :open="isOpen" v-bind="args" :target-element="triggerElement" role="tooltip" disable-focus-trap > <bento-typography el="span"> {{ args.default }} <bento-link to="https://www.adyen.com/" external> Learn more here </bento-link> </bento-typography> </bento-popover> </span> </div> `, }), args: { ...DEFAULT_PROPS, id: ID }, parameters: storybookDocsParameter(BentoDelayedHoverExample), }; export const WithImage: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoPopover, BentoPopoverImage, BentoTypography, InfoFilledIcon, }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(false); const targetElement = ref(null); const togglePopover = () => { isOpen.value = !isOpen.value; }; watch( () => args.open, (val: boolean) => { isOpen.value = val; } ); const show = () => action('input'); const dismissPopover = () => { action('dismiss')(); isOpen.value = false; }; onMounted(() => (isOpen.value = args.open === undefined ? true : args.open)); return { // Values args, isOpen, targetElement, // Methods togglePopover, show, dismissPopover, }; }, template: ` <div class="b-popover-story--with-image"> <bento-popover v-if="targetElement" :open="isOpen" v-bind="args" :targetElement="targetElement" @dismiss="dismissPopover" > <template #image> <bento-popover-image src="${IMAGE_SRC}" alt="${IMAGE_ALT}" /> </template> <bento-typography el="span"> {{args.default}} </bento-typography> </bento-popover> <bento-button ref="targetElement" @click="togglePopover" :aria-expanded="isOpen" aria-controls="${ID}" > Open popover </bento-button> </div> `, }), args: { ...DEFAULT_PROPS, title: TITLE_TEXT, actions: ACTIONS, id: ID, }, parameters: storybookDocsParameter(` <div> <bento-popover v-if="targetElement" id="${ID}" :open="isOpen" title="${TITLE_TEXT}" :actions="[ { title: 'Primary', event: () => action('Primary')(), }, { title: 'Secondary', event: () => action('Secondary')(), }, ]" position="bottom-start" :targetElement="targetElement" @dismiss="dismissPopover" > <template #image> <bento-popover-image src="${IMAGE_SRC}" alt="${IMAGE_ALT}" /> </template> <bento-typography el="span"> ${DEFAULT_TEXT} </bento-typography> </bento-popover> <bento-button ref="targetElement" :aria-expanded="isOpen" @click="togglePopover" aria-controls="${ID}" > Open popover </bento-button> </div> `), };
@@ -1 +1 @@
1
- import { type GridLayoutColumnWidth } from '@/internal'; import type { BentoSelectionCardItem, BentoSelectionCardModelValue } from '@/components/selection-card'; import { type FormField, type FormFieldEmits } from '../../../../types/form-field'; export type BentoSelectionCardGroupVariant = 'checkbox' | 'radio'; export type BentoSelectionCardGroupLayout = 'horizontal' | 'vertical'; export type BentoSelectionCardGroupItemsPerRow = GridLayoutColumnWidth; export type BentoSelectionCardGroupEmits = FormFieldEmits<BentoSelectionCardModelValue>; export interface BentoSelectionCardGroupProps extends FormField { /** * The form id the selection card group is associated with. Defaults to null, as a selection card group can also be * used standalone. * @default null */ formId?: string; /** * Shows/hides the group label. * @default null */ hideLabel?: boolean; /** * The selection card group's layout. It can be vertical or horizontal. Defaults to vertical. * @values horizontal, vertical * @default vertical */ layout?: BentoSelectionCardGroupLayout; /** * The selection card group's items. */ items: Array<BentoSelectionCardItem>; /** * Sets the responsive number of items rendered in each row. * @default undefined */ itemsPerRow?: BentoSelectionCardGroupItemsPerRow; /** * Determines the initially selected value of the group. Corresponds to the BentoSelectionCardItem's value. * @default null */ modelValue?: BentoSelectionCardModelValue; /** * Renders the cards with a checkbox or a radio input. Selection logic will also apply in accordance. * @values checkbox, radio * @default checkbox */ variant?: BentoSelectionCardGroupVariant; }
1
+ import { type GridLayoutColumnWidth } from '@/internal'; import type { BentoSelectionCardItem, BentoSelectionCardModelValue } from '../../selection-card.types'; import { type FormField, type FormFieldEmits } from '../../../../types/form-field'; export type BentoSelectionCardGroupVariant = 'checkbox' | 'radio'; export type BentoSelectionCardGroupLayout = 'horizontal' | 'vertical'; export type BentoSelectionCardGroupItemsPerRow = GridLayoutColumnWidth; export type BentoSelectionCardGroupEmits = FormFieldEmits<BentoSelectionCardModelValue>; export interface BentoSelectionCardGroupProps extends FormField { /** * The form id the selection card group is associated with. Defaults to null, as a selection card group can also be * used standalone. * @default null */ formId?: string; /** * Shows/hides the group label. * @default null */ hideLabel?: boolean; /** * The selection card group's layout. It can be vertical or horizontal. Defaults to vertical. * @values horizontal, vertical * @default vertical */ layout?: BentoSelectionCardGroupLayout; /** * The selection card group's items. */ items: Array<BentoSelectionCardItem>; /** * Sets the responsive number of items rendered in each row. * @default undefined */ itemsPerRow?: BentoSelectionCardGroupItemsPerRow; /** * Determines the initially selected value of the group. Corresponds to the BentoSelectionCardItem's value. * @default null */ modelValue?: BentoSelectionCardModelValue; /** * Renders the cards with a checkbox or a radio input. Selection logic will also apply in accordance. * @values checkbox, radio * @default checkbox */ variant?: BentoSelectionCardGroupVariant; }
@@ -1 +1 @@
1
- <template> <div class="b-summary-grid"> <grid-layout v-for="index in numberOfRows" :key="`summary-grid-row-${index}`" :data-testid="`summary-grid-row-${index}`" :column-width="columnWidth?.[`row${index}`]" > <template v-if="hasSlot(`cell-${index}-1`)" #col-1> <slot :name="`cell-${index}-1`" /> </template> <template v-if="hasSlot(`cell-${index}-2`)" #col-2> <slot :name="`cell-${index}-2`" /> </template> <template v-if="hasSlot(`cell-${index}-3`)" #col-3> <slot :name="`cell-${index}-3`" /> </template> <template v-if="hasSlot(`cell-${index}-4`)" #col-4> <slot :name="`cell-${index}-4`" /> </template> <template v-if="hasSlot(`cell-${index}-5`)" #col-5> <slot :name="`cell-${index}-5`" /> </template> <template v-if="hasSlot(`cell-${index}-6`)" #col-6> <slot :name="`cell-${index}-6`" /> </template> </grid-layout> </div> </template> <script setup lang="ts"> import { onMounted, provide, toRef, useSlots } from 'vue'; import type { BentoSummaryGridLayout, BentoSummaryGridProps } from './summary-grid.types'; import { GridLayout } from '@/components/internal'; import { useHasSlot } from '@/composables'; import { SUMMARY_GRID_LAYOUT_INJECTION_KEY, SUMMARY_GRID_SLASHED_ZERO, } from '@/components/summary-grid/summary-grid.keys'; const props = withDefaults(defineProps<BentoSummaryGridProps>(), { columnWidth: null, slashedZero: false, }); const slots = useSlots(); const hasSlot = useHasSlot(slots); let numberOfRows = 1; if (slots) { const slotRows = Object.keys(slots) ?.filter(slotName => slotName.includes('cell-')) ?.map(slotName => parseInt(slotName.split('-')?.[1], 10)); numberOfRows = Math.max(...slotRows); } onMounted(() => { if (numberOfRows > 3) { throw new Error( 'BentoSummaryGrid must only have a maximum of 3 rows. Read the documentation for more information.' ); } }); const registerComponentsInSummaryGrid: BentoSummaryGridLayout = () => { const isSummaryGridLayout = true; return { isSummaryGridLayout }; }; provide(SUMMARY_GRID_LAYOUT_INJECTION_KEY, registerComponentsInSummaryGrid); provide(SUMMARY_GRID_SLASHED_ZERO, toRef(props, 'slashedZero')); </script> <script lang="ts"> /** * A summary grid component that displays a collection items. * * @example * import { BentoSummaryGrid, BentoSummaryGridItemAmount } from '@adyen/bento-vue2'; * * export default { * components: { BentoSummaryGrid, BentoSummaryGridItemAmount }, * template: ` * <bento-summary-grid> * <template #cell-2-2> * <bento-summary-grid-item-amount * title="Text Item" * label="14" * info-icon-tooltip-text="text for icon" * copy="copy this to clipboard" * /> * </template> * </bento-summary-grid> * ` * } */ export default {}; </script> <style lang="scss" scoped src="./summary-grid.scss" />
1
+ <template> <div class="b-summary-grid"> <grid-layout v-for="index in numberOfRows" :key="`summary-grid-row-${index}`" :data-testid="`summary-grid-row-${index}`" :column-width="columnWidth?.[`row${index}`]" > <template v-if="hasSlot(`cell-${index}-1`)" #col-1> <slot :name="`cell-${index}-1`" /> </template> <template v-if="hasSlot(`cell-${index}-2`)" #col-2> <slot :name="`cell-${index}-2`" /> </template> <template v-if="hasSlot(`cell-${index}-3`)" #col-3> <slot :name="`cell-${index}-3`" /> </template> <template v-if="hasSlot(`cell-${index}-4`)" #col-4> <slot :name="`cell-${index}-4`" /> </template> <template v-if="hasSlot(`cell-${index}-5`)" #col-5> <slot :name="`cell-${index}-5`" /> </template> <template v-if="hasSlot(`cell-${index}-6`)" #col-6> <slot :name="`cell-${index}-6`" /> </template> </grid-layout> </div> </template> <script setup lang="ts"> import { onMounted, provide, toRef, useSlots } from 'vue'; import type { BentoSummaryGridLayout, BentoSummaryGridProps } from './summary-grid.types'; import { GridLayout } from '@/components/internal/grid-layout'; import { useHasSlot } from '@/composables'; import { SUMMARY_GRID_LAYOUT_INJECTION_KEY, SUMMARY_GRID_SLASHED_ZERO, } from '@/components/summary-grid/summary-grid.keys'; const props = withDefaults(defineProps<BentoSummaryGridProps>(), { columnWidth: null, slashedZero: false, }); const slots = useSlots(); const hasSlot = useHasSlot(slots); let numberOfRows = 1; if (slots) { const slotRows = Object.keys(slots) ?.filter(slotName => slotName.includes('cell-')) ?.map(slotName => parseInt(slotName.split('-')?.[1], 10)); numberOfRows = Math.max(...slotRows); } onMounted(() => { if (numberOfRows > 3) { throw new Error( 'BentoSummaryGrid must only have a maximum of 3 rows. Read the documentation for more information.' ); } }); const registerComponentsInSummaryGrid: BentoSummaryGridLayout = () => { const isSummaryGridLayout = true; return { isSummaryGridLayout }; }; provide(SUMMARY_GRID_LAYOUT_INJECTION_KEY, registerComponentsInSummaryGrid); provide(SUMMARY_GRID_SLASHED_ZERO, toRef(props, 'slashedZero')); </script> <script lang="ts"> /** * A summary grid component that displays a collection items. * * @example * import { BentoSummaryGrid, BentoSummaryGridItemAmount } from '@adyen/bento-vue2'; * * export default { * components: { BentoSummaryGrid, BentoSummaryGridItemAmount }, * template: ` * <bento-summary-grid> * <template #cell-2-2> * <bento-summary-grid-item-amount * title="Text Item" * label="14" * info-icon-tooltip-text="text for icon" * copy="copy this to clipboard" * /> * </template> * </bento-summary-grid> * ` * } */ export default {}; </script> <style lang="scss" scoped src="./summary-grid.scss" />
@@ -23,12 +23,17 @@ page. Selecting another tab replaces content with the new tab’s content.
23
23
 
24
24
  ## Variations
25
25
 
26
+ <Canvas of={TabsStories.EverythingBagel} />
27
+
26
28
  ### Notification indicators
27
29
 
28
30
  If there’s something requiring user attention, the tab gets a blue notification dot. To set/unset this notification dot,
29
31
  set the `withNotificationIndicator` prop to `true` or `false` for the designated `bento-tab`.
30
32
 
31
- <Canvas of={TabsStories.WithNotificationIndicator} />
33
+ ### Error counter
34
+
35
+ To display an error count alongside a tab title, use the `error-counter` prop on `bento-tab`. This renders a red tag
36
+ with an error icon and the given count. It is useful for signalling validation errors or issues within a tab's content.
32
37
 
33
38
  ## Modifiers
34
39
 
@@ -28,6 +28,7 @@
28
28
  "date",
29
29
  "date-picker",
30
30
  "date-range-picker",
31
+ "date-time-picker",
31
32
  "divider",
32
33
  "draggable",
33
34
  "draggable-handle",
@@ -0,0 +1,77 @@
1
+ import { Canvas, Meta, Source } from '@storybook/blocks';
2
+ import * as useBentoDelayedHoverStories from './use-bento-delayed-hover.stories';
3
+
4
+ <Meta of={useBentoDelayedHoverStories} />
5
+
6
+ # useBentoDelayedHover
7
+
8
+ A composable that provides the logic to open and close an element on hover and focus with a closing delay.
9
+
10
+ <Canvas of={useBentoDelayedHoverStories.Default} />
11
+
12
+ ## Usage
13
+
14
+ `useBentoDelayedHover` returns an object with:
15
+
16
+ - `isOpen` — a `ref<boolean>` tracking the open state.
17
+ - `open` / `close` — individual handlers to open or close immediately / after the delay.
18
+ - `hoverEvents` — an object bundling `mouseenter`, `mouseleave`, `focusin`, and `focusout` handlers, ready to be spread
19
+ with `v-on`.
20
+
21
+ Bind `hoverEvents` on the trigger element:
22
+
23
+ <Source
24
+ format={'html'}
25
+ dark
26
+ code={`
27
+ <span ref="triggerRef" v-on="hoverEvents">Hover me</span>
28
+ `}
29
+ />
30
+
31
+ When the content is **not** a descendant of the trigger (e.g. rendered as a sibling or teleported elsewhere in the DOM),
32
+ also bind `hoverEvents` on a wrapper around the content so that hovering or focusing it cancels the pending close:
33
+
34
+ When the content is a `bento-popover`, always pass `disable-focus-trap`. A focus trap can move focus back into the
35
+ popover while the delayed hover handlers are trying to close it, which can make multiple hover-triggered popovers appear
36
+ to reopen or close each other unexpectedly.
37
+
38
+ <Source
39
+ format={'html'}
40
+ dark
41
+ code={`
42
+ <span ref="triggerRef" v-on="hoverEvents">Hover me</span>
43
+
44
+ <span v-on="hoverEvents">
45
+ <bento-popover :open="isOpen" :target-element="triggerRef" disable-focus-trap>
46
+ Popover content
47
+ </bento-popover>
48
+ </span>
49
+ `}
50
+ />
51
+
52
+ > **Vue 2 only:** `v-on` with object syntax does not attach listeners to the underlying DOM element of a Vue component —
53
+ > the `.native` modifier is required, but it cannot be combined with the object form of `v-on`. Either wrap the
54
+ > component in a native element and use `v-on="hoverEvents"` on the wrapper, or bind the handlers individually with
55
+ > `.native`:
56
+
57
+ <Source
58
+ format={'html'}
59
+ dark
60
+ code={`
61
+ <!-- Option A: wrap in a native element -->
62
+ <span v-on="hoverEvents" tabindex="0">
63
+ <bento-tag ref="triggerRef" label="Hover me" />
64
+ </span>
65
+
66
+ <!-- Option B: bind events individually with .native -->
67
+
68
+ <bento-tag ref="triggerRef" label="Hover me" tabindex="0" @mouseenter.native="open" @mouseleave.native="close"
69
+ @focusin.native="open" @focusout.native="close" /> `} />
70
+
71
+ ## Delay
72
+
73
+ A small closing delay (200 ms by default) is applied when the cursor or focus leaves the trigger. This prevents the
74
+ content from flickering when the user moves between the trigger and the content. The delay can be customised by passing
75
+ a value in milliseconds:
76
+
77
+ <Source language={'ts'} dark code={`const { isOpen, open, close, hoverEvents } = useBentoDelayedHover(400);`} />
@@ -0,0 +1 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue'; import { ref } from 'vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { BentoLink } from '@/components/link'; import { BentoPopover } from '@/components/popover'; import { BentoTag } from '@/components/tag'; import { BentoTypography } from '@/components/typography'; import { useBentoDelayedHover } from './use-bento-delayed-hover'; import UseBentoDelayedHoverExample from './__tests__/use-bento-delayed-hover-example.vue?raw'; const meta: Meta = { title: 'useBentoDelayedHover', }; export default meta; export const Default: StoryObj = { render: () => ({ components: { BentoLink, BentoPopover, BentoTag, BentoTypography }, setup() { const triggerElement = ref(null); const { isOpen, open, close, hoverEvents } = useBentoDelayedHover(); return { isOpen, open, close, triggerElement, hoverEvents }; }, template: ` <div> <bento-tag ref="triggerElement" label="Hover over me" tabindex="0" :aria-expanded="isOpen.toString()" aria-controls="delayed-hover-popover" @mouseenter.native="open" @mouseleave.native="close" @focusin.native="open" @focusout.native="close" /> <span v-on="hoverEvents"> <bento-popover v-if="triggerElement" id="delayed-hover-popover" :open="isOpen" :target-element="triggerElement" role="tooltip" disable-focus-trap > <bento-typography el="span"> Add your text here <bento-link to="https://www.adyen.com/" external>Learn more here</bento-link> </bento-typography> </bento-popover> </span> </div> `, }), parameters: storybookDocsParameter(UseBentoDelayedHoverExample), };