@adyen/bento-mcp 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/assets/components/accordion/components/accordion-item.vue +1 -1
- package/dist/assets/components/card/card.vue +1 -1
- package/dist/assets/components/dashed-underline/dashed-underline.vue +1 -1
- package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.types.ts +1 -1
- package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.vue +1 -1
- package/dist/assets/components/date-picker/composables/use-date-picker-single-calendar-text.types.ts +1 -1
- package/dist/assets/components/date-picker/date-picker.vue +1 -1
- package/dist/assets/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.vue +1 -1
- package/dist/assets/components/date-time-picker/date-time-picker.docs.mdx +129 -0
- package/dist/assets/components/date-time-picker/date-time-picker.stories.ts +1 -0
- package/dist/assets/components/date-time-picker/date-time-picker.types.ts +1 -0
- package/dist/assets/components/date-time-picker/date-time-picker.vue +1 -0
- package/dist/assets/components/draggable/draggable.docs.mdx +76 -135
- package/dist/assets/components/draggable/draggable.stories.ts +1 -1
- package/dist/assets/components/draggable/draggable.types.ts +1 -1
- package/dist/assets/components/draggable/draggable.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-small-textbox/dropdown-small-textbox.vue +1 -1
- package/dist/assets/components/dropdown/dropdown.vue +1 -1
- package/dist/assets/components/file-uploader/file-uploader.vue +1 -1
- package/dist/assets/components/filter-bar/components/base-filter/base-filter.vue +1 -1
- package/dist/assets/components/input-field/input-field.vue +1 -1
- package/dist/assets/components/internal/calendar/calendar.types.ts +1 -1
- package/dist/assets/components/internal/calendar/calendar.vue +1 -1
- package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.stories.ts +1 -1
- package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.types.ts +1 -1
- package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.vue +1 -1
- package/dist/assets/components/internal/calendar/components/calendar-month/composables/granular-min-max-date.types.ts +1 -1
- package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.types.ts +1 -1
- package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.vue +1 -1
- package/dist/assets/components/internal/info-icon-with-popover/info-icon-with-popover.vue +1 -1
- package/dist/assets/components/internal/teleport/teleport.vue +1 -1
- package/dist/assets/components/link/link.vue +1 -1
- package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.stories.ts +1 -1
- package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.vue +1 -1
- package/dist/assets/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.stories.ts +1 -1
- package/dist/assets/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.vue +1 -1
- package/dist/assets/components/navigation-menu/navigation-menu.docs.mdx +7 -43
- package/dist/assets/components/navigation-menu/navigation-menu.stories.ts +1 -1
- package/dist/assets/components/navigation-menu/navigation-menu.types.ts +1 -1
- package/dist/assets/components/navigation-menu/navigation-menu.vue +1 -1
- package/dist/assets/components/popover/popover.docs.mdx +74 -15
- package/dist/assets/components/popover/popover.stories.ts +1 -1
- package/dist/assets/components/secondary-nav/components/secondary-nav-category/secondary-nav-category.vue +1 -1
- package/dist/assets/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue +1 -1
- package/dist/assets/components/secondary-nav/secondary-nav.stories.ts +1 -1
- package/dist/assets/components/secondary-nav/secondary-nav.types.ts +1 -1
- package/dist/assets/components/secondary-nav/secondary-nav.vue +1 -1
- package/dist/assets/components/selection-card/components/selection-card-group/selection-card-group.types.ts +1 -1
- package/dist/assets/components/tabs/components/tab.types.ts +1 -1
- package/dist/assets/components/tabs/components/tab.vue +1 -1
- package/dist/assets/components/tabs/tabs.stories.ts +1 -1
- package/dist/assets/components/tabs/tabs.types.ts +1 -1
- package/dist/assets/components/tabs/tabs.vue +1 -1
- package/dist/assets/components.json +2 -0
- package/dist/assets/composables/use-bento-delayed-hover/use-bento-delayed-hover.docs.mdx +73 -0
- package/dist/assets/composables/use-bento-delayed-hover/use-bento-delayed-hover.stories.ts +1 -0
- package/dist/assets/index.ts +1 -1
- package/dist/assets/usage.json +12 -10
- package/dist/main.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <a v-if="external || isNotRouting" :class="conditionalClasses" v-bind="$attrs" :aria-describedby="externalLinkId" :href="$props.to.toString()" :title="getAttributeTitle($attrs.title)" :target="computedTarget" @click="onClick" > <bento-typography v-if="!disableTypography" class="b-link__text" el="span" variant="body"> <slot /> </bento-typography> <slot v-else /> <span v-if="showExternalLinkIcon" aria-hidden="true" class="b-link__external-link-icon"> <external-link-icon /> </span> <span v-if="external" :id="externalLinkId" class="b-link--visually-hidden">{{ t('opensInNewTab') }}</span> </a> <router-link v-else v-bind="{ ...$attrs, ...$props, ...nonForwardedProps }" :class="conditionalClasses" :target="target" :to="props.to" custom @click.native="onClick" > <bento-typography v-if="!disableTypography" class="b-link__text" el="span" variant="body"> <slot /> </bento-typography> <slot v-else /> </router-link> </template> <script setup lang="ts"> import { RouterLink } from 'vue-router'; import { computed } from 'vue'; import { generateUid } from '@/core/utils/ts'; import { BentoTypography } from '@/components/typography'; import { type BentoLinkProps, BentoLinkVariant } from './link.types'; import { useI18n } from '@/utils/ts/i18n'; import type { RouterLinkSlotArgument } from 'vue-router/types/router.d.ts'; import ExternalLinkIcon from '@adyen/ui-assets-icons-16/vue/external-link'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoLinkProps>(), { target: null, variant: BentoLinkVariant.DEFAULT, }); // Props that should not be forwarded to the underlying anchor element const nonForwardedProps = { variant: undefined, isButton: undefined, truncate: undefined, disableTypography: undefined, external: undefined, isNotRouting: undefined, }; const externalLinkId = generateUid('external-link'); const getAttributeTitle = (attributesTitle: unknown) => attributesTitle as string; const conditionalClasses = computed(() => ({ 'b-link': !props.isButton, [`b-link--${BentoLinkVariant.QUIET}`]: props.variant === BentoLinkVariant.QUIET, [`b-link--truncate`]: props.truncate, })); const computedTarget = computed(() => (props.external ? (props.target ?? '_blank') : props.target)); const showExternalLinkIcon = computed(() => !(props.isButton || props.isNotRouting)); const emit = defineEmits<{ (e: 'click', clickEvent: MouseEvent): void; }>(); const onClick = (e: MouseEvent, navigateCallback?: RouterLinkSlotArgument['navigate']) => { emit('click', e); if (navigateCallback) { navigateCallback(); } }; </script> <script lang="ts"> /** * Extends the `router-link` from the `vue-router` NPM package. * * @example * import { BentoLink } from '@adyen/bento-vue2'; * * export default { * components: { BentoLink }, * template: ` * <bento-link to="/"> * link * </bento-link> * ` * } */ export default { name: 'bento-link', inheritAttrs: false, i18n: { messages }, }; </script> <style lang="scss" scoped src="@/core/components/link/link.scss" />
|
|
1
|
+
<template> <a v-if="external || isNotRouting" :class="conditionalClasses" v-bind="$attrs" :aria-describedby="external ? externalLinkId : null" :href="$props.to.toString()" :title="getAttributeTitle($attrs.title)" :target="computedTarget" @click="onClick" > <bento-typography v-if="!disableTypography" class="b-link__text" el="span" variant="body"> <slot /> </bento-typography> <slot v-else /> <span v-if="showExternalLinkIcon" aria-hidden="true" class="b-link__external-link-icon"> <external-link-icon /> </span> <span v-if="external" :id="externalLinkId" class="b-link--visually-hidden">{{ t('opensInNewTab') }}</span> </a> <router-link v-else v-bind="{ ...$attrs, ...$props, ...nonForwardedProps }" :class="conditionalClasses" :target="target" :to="props.to" custom @click.native="onClick" > <bento-typography v-if="!disableTypography" class="b-link__text" el="span" variant="body"> <slot /> </bento-typography> <slot v-else /> </router-link> </template> <script setup lang="ts"> import { RouterLink } from 'vue-router'; import { computed } from 'vue'; import { generateUid } from '@/core/utils/ts'; import { BentoTypography } from '@/components/typography'; import { type BentoLinkProps, BentoLinkVariant } from './link.types'; import { useI18n } from '@/utils/ts/i18n'; import type { RouterLinkSlotArgument } from 'vue-router/types/router.d.ts'; import ExternalLinkIcon from '@adyen/ui-assets-icons-16/vue/external-link'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoLinkProps>(), { target: null, variant: BentoLinkVariant.DEFAULT, }); // Props that should not be forwarded to the underlying anchor element const nonForwardedProps = { variant: undefined, isButton: undefined, truncate: undefined, disableTypography: undefined, external: undefined, isNotRouting: undefined, }; const externalLinkId = generateUid('external-link'); const getAttributeTitle = (attributesTitle: unknown) => attributesTitle as string; const conditionalClasses = computed(() => ({ 'b-link': !props.isButton, [`b-link--${BentoLinkVariant.QUIET}`]: props.variant === BentoLinkVariant.QUIET, [`b-link--truncate`]: props.truncate, })); const computedTarget = computed(() => (props.external ? (props.target ?? '_blank') : props.target)); const showExternalLinkIcon = computed(() => !(props.isButton || props.isNotRouting)); const emit = defineEmits<{ (e: 'click', clickEvent: MouseEvent): void; }>(); const onClick = (e: MouseEvent, navigateCallback?: RouterLinkSlotArgument['navigate']) => { emit('click', e); if (navigateCallback) { navigateCallback(); } }; </script> <script lang="ts"> /** * Extends the `router-link` from the `vue-router` NPM package. * * @example * import { BentoLink } from '@adyen/bento-vue2'; * * export default { * components: { BentoLink }, * template: ` * <bento-link to="/"> * link * </bento-link> * ` * } */ export default { name: 'bento-link', inheritAttrs: false, i18n: { messages }, }; </script> <style lang="scss" scoped src="@/core/components/link/link.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import BentoNavigationMenuGroup from './navigation-menu-group.vue'; import BentoNavigationMenu from '../../navigation-menu.vue'; import BentoNavigationMenuItem from '../navigation-menu-item/navigation-menu-item.vue'; import { BentoButton } from '@/components/button'; import type { Meta, StoryObj } from '@storybook/vue'; import NavPaymentsIcon from '@adyen/ui-assets-icons-16/vue/nav-payments'; import StarIcon from '@adyen/ui-assets-icons-16/vue/star'; import { isVue2 } from 'vue-demi'; const meta: Meta = { title: 'Navigation Menu/Navigation Menu Group', component: BentoNavigationMenuGroup, argTypes: { showIcon: { table: { category: 'Storybook controls', }, control: { type: 'boolean', }, }, icon: { table: { disable: true, }, }, default: { table: { disable: true, }, }, }, }; export default meta; type Story = StoryObj<typeof BentoNavigationMenuGroup>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoNavigationMenu, BentoNavigationMenuGroup, BentoNavigationMenuItem, NavPaymentsIcon, StarIcon, }, props: Object.keys(argTypes), setup(props) { return { args: isVue2 ? props : _args, }; }, template: ` <div style="width: 248px;"> <bento-navigation-menu label="Pages"> <bento-navigation-menu-group :label="args.label" :is-expanded="args.isExpanded" > <template v-if="args.showIcon" #icon> <nav-payments-icon /> </template> <bento-navigation-menu-item nested value="payments" label="Payments" :link="{ to: '#payments', isNotRouting: true }"> <template v-if="args.showAction" #action> <bento-button variant="tertiary"> <template #iconLeft> <star-icon /> </template> </bento-button> </template> </bento-navigation-menu-item> <bento-navigation-menu-item nested value="preauth" label="Pre-authorized" :link="{ to: '#preauth', isNotRouting: true }"> <template v-if="args.showAction" #action> <bento-button variant="tertiary"> <template #iconLeft> <star-icon /> </template> </bento-button> </template> </bento-navigation-menu-item> </bento-navigation-menu-group> </bento-navigation-menu> </div> `, }), args: { label: 'Payments', isExpanded: false, showIcon: true, }, };
|
|
1
|
+
import BentoNavigationMenuGroup from './navigation-menu-group.vue'; import BentoNavigationMenu from '../../navigation-menu.vue'; import BentoNavigationMenuItem from '../navigation-menu-item/navigation-menu-item.vue'; import { BentoButton } from '@/components/button'; import type { Meta, StoryObj } from '@storybook/vue'; import NavPaymentsIcon from '@adyen/ui-assets-icons-16/vue/nav-payments'; import StarIcon from '@adyen/ui-assets-icons-16/vue/star'; import { isVue2 } from 'vue-demi'; const meta: Meta = { title: 'Navigation Menu/Navigation Menu Group', component: BentoNavigationMenuGroup, argTypes: { showIcon: { table: { category: 'Storybook controls', }, control: { type: 'boolean', }, }, icon: { table: { disable: true, }, }, default: { table: { disable: true, }, }, }, }; export default meta; type Story = StoryObj<typeof BentoNavigationMenuGroup>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoNavigationMenu, BentoNavigationMenuGroup, BentoNavigationMenuItem, NavPaymentsIcon, StarIcon, }, props: Object.keys(argTypes), setup(props) { return { args: isVue2 ? props : _args, }; }, template: ` <div style="width: 248px;"> <bento-navigation-menu label="Pages"> <bento-navigation-menu-group :label="args.label" :is-expanded="args.isExpanded" > <template v-if="args.showIcon" #icon> <nav-payments-icon /> </template> <bento-navigation-menu-item nested value="payments" label="Payments" :link="{ to: '#payments', isNotRouting: true }"> <template v-if="args.showAction" #action> <bento-button variant="tertiary"> <template #iconLeft> <star-icon 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 }"> <template v-if="args.showAction" #action> <bento-button variant="tertiary"> <template #iconLeft> <star-icon svg-title="Add to favorites" /> </template> </bento-button> </template> </bento-navigation-menu-item> </bento-navigation-menu-group> </bento-navigation-menu> </div> `, }), args: { label: 'Payments', isExpanded: false, showIcon: true, }, };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <li class="b-navigation-menu-group"> <button :id="buttonId" type="button" class="b-navigation-menu-group__header" :class="{ ...headerClasses, 'b-navigation-menu-group__header--no-icon': !hasSlot('icon'), }" :aria-expanded="`${isOpen && !isSemiCollapsed}`" :aria-controls="contentId" :aria-description="headerAriaDescription" @click="toggleGroup" > <div v-if="hasSlot('icon')" class="b-navigation-menu-group__icon"> <slot name="icon" /> </div> <bento-typography ref="labelRef" v-bento-tooltip-directive="isTruncated ? label : undefined" el="span" variant="body" stronger class="b-navigation-menu-group__label" > {{ label }} </bento-typography> <div class="b-navigation-menu-group__icon b-navigation-menu-group__icon--chevron"> <chevron-up-icon v-if="isOpen && !isSemiCollapsed"
|
|
1
|
+
<template> <li class="b-navigation-menu-group"> <button :id="buttonId" type="button" class="b-navigation-menu-group__header" :class="{ ...headerClasses, 'b-navigation-menu-group__header--no-icon': !hasSlot('icon'), }" :aria-expanded="`${isOpen && !isSemiCollapsed}`" :aria-controls="contentId" :aria-description="headerAriaDescription" @click="toggleGroup" > <div v-if="hasSlot('icon')" class="b-navigation-menu-group__icon" 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-group__label" > {{ label }} </bento-typography> <div class="b-navigation-menu-group__icon b-navigation-menu-group__icon--chevron" aria-hidden="true"> <chevron-up-icon v-if="isOpen && !isSemiCollapsed" /> <chevron-down-icon v-else /> </div> </button> <Transition name="b-navigation-menu-group__animation--content"> <div v-show="isOpen" :id="contentId" class="b-navigation-menu-group__content-wrapper" role="region" :aria-labelledby="buttonId" :style="contentMaxHeight" > <ul ref="contentDiv" class="b-navigation-menu-group__content"> <slot /> </ul> </div> </Transition> </li> </template> <script setup lang="ts"> import { computed, inject, onMounted, onUnmounted, provide, ref, useSlots, watch } from 'vue'; import { useResizeObserver } from '@vueuse/core'; import { BentoTypography } from '@/components/typography'; import { useExpandableContentHeight } from '@/composables/use-expandable-content-height/use-expandable-content-height'; import { useHasSlot } from '@/composables'; import { BentoTooltipDirective as vBentoTooltipDirective } from '@/directives/tooltip'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import ChevronUpIcon from '@adyen/ui-assets-icons-16/vue/chevron-up'; import type { BentoNavigationMenuGroupProps, BentoNavigationMenuGroupState, BentoNavigationMenuItemRegistration, } from './navigation-menu-group.types'; import { NAVIGATION_MENU_STATE_INJECTION_KEY } from '../../navigation-menu.keys'; import { NAVIGATION_MENU_GROUP_STATE_INJECTION_KEY } from './navigation-menu-group.keys'; import { type BentoNavigationMenuState } from '../../navigation-menu.types'; import messages from '../../messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoNavigationMenuGroupProps>(), { isExpanded: false, }); const slots = useSlots(); const hasSlot = useHasSlot(slots); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const headerClasses = computed(() => ({ 'b-navigation-menu-group__header--expanded': isOpen.value && !isSemiCollapsed.value, })); const headerAriaDescription = computed(() => (isSemiCollapsed.value ? t('showingCurrentPage') : undefined)); const labelRef = ref(null); const isTruncated = ref(false); useResizeObserver(labelRef, () => { const el = labelRef.value?.$el ?? labelRef.value; if (el) { isTruncated.value = el.scrollWidth > el.clientWidth; } }); const groupId = generateUid('navigation-menu-group'); const buttonId = generateUid('navigation-menu-group-button'); const contentId = generateUid('navigation-menu-group-content'); const navigationMenuState = inject<BentoNavigationMenuState | null>(NAVIGATION_MENU_STATE_INJECTION_KEY, null); const nestedItems = ref<Array<BentoNavigationMenuItemRegistration>>([]); const registerItem = (itemId: string, isSelected: () => boolean) => { nestedItems.value.push({ id: itemId, isSelected }); }; const unregisterItem = (itemId: string) => { nestedItems.value = nestedItems.value.filter(item => item.id !== itemId); }; const isSemiCollapsed = ref(false); provide<BentoNavigationMenuGroupState>(NAVIGATION_MENU_GROUP_STATE_INJECTION_KEY, { registerItem, unregisterItem, isSemiCollapsed, }); const hasActiveItem = computed(() => nestedItems.value.some(item => item.isSelected())); const emit = defineEmits<{ /** * Emits update event when group is toggled */ (e: 'update:is-expanded', value: boolean): void; }>(); const isOpen = ref(props.isExpanded); const contentDiv = ref<HTMLUListElement>(null); const { contentHeight } = useExpandableContentHeight(contentDiv, isOpen); const contentMaxHeight = computed(() => (contentHeight.value ? { 'max-height': contentHeight.value } : {})); const toggleGroup = () => { if (isOpen.value && !isSemiCollapsed.value) { if (hasActiveItem.value) { isSemiCollapsed.value = true; } else { isOpen.value = false; } } else if (isSemiCollapsed.value) { isSemiCollapsed.value = false; } else { isOpen.value = true; } emit('update:is-expanded', isOpen.value && !isSemiCollapsed.value); }; const setOpen = (open: boolean) => { if (open) { isOpen.value = true; isSemiCollapsed.value = false; } else if (hasActiveItem.value) { isSemiCollapsed.value = true; } else { isSemiCollapsed.value = false; isOpen.value = false; } emit('update:is-expanded', open); }; watch( () => props.isExpanded, value => { if (!isSemiCollapsed.value) { isOpen.value = value; } } ); onMounted(() => { if (hasActiveItem.value && !isOpen.value) { isOpen.value = true; emit('update:is-expanded', true); } if (navigationMenuState) { navigationMenuState.registerGroup(groupId, setOpen, () => isOpen.value && !isSemiCollapsed.value); } }); onUnmounted(() => navigationMenuState?.unregisterGroup(groupId)); </script> <script lang="ts"> /** * Navigation menu group component for creating collapsible sections. * * @usage * import { BentoNavigationMenuGroup, BentoNavigationMenuItem } from '@adyen/bento-vue2'; * * export default { * components: { BentoNavigationMenuGroup, BentoNavigationMenuItem }, * template: ` * <bento-navigation-menu-group label="Section" :is-expanded="true"> * <template #icon><folder-icon /></template> * <bento-navigation-menu-item nested value="item-1" label="Item 1" :link="{ to: '/section/item-1' }" /> * <bento-navigation-menu-item nested value="item-2" label="Item 2" :link="{ to: '/section/item-2' }" /> * </bento-navigation-menu-group> * ` * } */ export default { name: 'bento-navigation-menu-group', i18n: { messages }, }; </script> <style lang="scss" scoped src="./navigation-menu-group.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import BentoNavigationMenuItem from './navigation-menu-item.vue'; import BentoNavigationMenu from '../../navigation-menu.vue'; import { BentoButton } from '@/components/button'; import type { Meta, StoryObj } from '@storybook/vue'; import NavHomeIcon from '@adyen/ui-assets-icons-16/vue/nav-home'; import StarIcon from '@adyen/ui-assets-icons-16/vue/star'; import { isVue2 } from 'vue-demi'; const meta: Meta = { title: 'Navigation Menu/Navigation Menu Item', component: BentoNavigationMenuItem, argTypes: { showIcon: { table: { category: 'Storybook controls', }, control: { type: 'boolean', }, }, showAction: { table: { category: 'Storybook controls', }, control: { type: 'boolean', }, }, icon: { table: { disable: true, }, }, action: { table: { disable: true, }, }, default: { table: { disable: true, }, }, }, }; export default meta; type Story = StoryObj<typeof BentoNavigationMenuItem>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoNavigationMenu, BentoNavigationMenuItem, NavHomeIcon, StarIcon }, props: Object.keys(argTypes), setup(props) { return { args: isVue2 ? props : _args, }; }, template: ` <div style="width: 248px;"> <bento-navigation-menu label="Pages"> <bento-navigation-menu-item :value="args.value" :label="args.label" :link="args.link" :nested="args.nested" :always-show-actions="args.alwaysShowActions" > <template v-if="args.showIcon" #icon> <nav-home-icon /> </template> <template v-if="args.showAction" #action> <bento-button variant="tertiary"> <template #iconLeft> <star-icon /> </template> </bento-button> </template> </bento-navigation-menu-item> </bento-navigation-menu> </div> `, }), args: { value: 'home', label: 'Home', link: { to: '#home', isNotRouting: true }, nested: false, alwaysShowActions: false, showIcon: true, showAction: true, }, };
|
|
1
|
+
import BentoNavigationMenuItem from './navigation-menu-item.vue'; import BentoNavigationMenu from '../../navigation-menu.vue'; import { BentoButton } from '@/components/button'; import type { Meta, StoryObj } from '@storybook/vue'; import NavHomeIcon from '@adyen/ui-assets-icons-16/vue/nav-home'; import StarIcon from '@adyen/ui-assets-icons-16/vue/star'; import { isVue2 } from 'vue-demi'; const meta: Meta = { title: 'Navigation Menu/Navigation Menu Item', component: BentoNavigationMenuItem, argTypes: { showIcon: { table: { category: 'Storybook controls', }, control: { type: 'boolean', }, }, showAction: { table: { category: 'Storybook controls', }, control: { type: 'boolean', }, }, icon: { table: { disable: true, }, }, action: { table: { disable: true, }, }, default: { table: { disable: true, }, }, }, }; export default meta; type Story = StoryObj<typeof BentoNavigationMenuItem>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoNavigationMenu, BentoNavigationMenuItem, NavHomeIcon, StarIcon }, props: Object.keys(argTypes), setup(props) { return { args: isVue2 ? props : _args, }; }, template: ` <div style="width: 248px;"> <bento-navigation-menu label="Pages"> <bento-navigation-menu-item :value="args.value" :label="args.label" :link="args.link" :nested="args.nested" :always-show-actions="args.alwaysShowActions" > <template v-if="args.showIcon" #icon> <nav-home-icon /> </template> <template v-if="args.showAction" #action> <bento-button variant="tertiary"> <template #iconLeft> <star-icon svg-title="Add to favorites" /> </template> </bento-button> </template> </bento-navigation-menu-item> </bento-navigation-menu> </div> `, }), args: { value: 'home', label: 'Home', link: { to: '#home', isNotRouting: true }, nested: false, alwaysShowActions: false, showIcon: true, showAction: true, }, };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <li
|
|
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
|
|
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
|
|
149
|
-
patterns above.
|
|
116
|
+
as analytics tracking, syncing state to a store, or triggering other UI updates.
|
|
150
117
|
|
|
151
|
-
>
|
|
152
|
-
>
|
|
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
|
|
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<
|
|
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
|
-
-
|
|
18
|
-
-
|
|
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
|
-
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
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,65 @@ 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
|
+
<Source
|
|
75
|
+
format={'html'}
|
|
76
|
+
dark
|
|
77
|
+
code={`
|
|
78
|
+
<span ref="triggerRef" v-on="hoverEvents">Hover me</span>
|
|
79
|
+
|
|
80
|
+
<bento-popover v-on="hoverEvents" :open="isOpen" :target-element="triggerRef">
|
|
81
|
+
Popover content
|
|
82
|
+
</bento-popover>
|
|
83
|
+
`}
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
When the popover is rendered as a descendant of the trigger element, this is not necessary — the trigger's own
|
|
87
|
+
`mouseleave` will not fire while the cursor is over its descendants.
|
|
88
|
+
|
|
89
|
+
> **Vue 2 only:** when binding on a Vue component (e.g. `bento-button` or `bento-popover`) rather than a native HTML
|
|
90
|
+
> element, `v-on` with object syntax does not attach the listeners to the underlying DOM element — the `.native`
|
|
91
|
+
> modifier is required for that, and it cannot be combined with the object form of `v-on`. To work around this, either
|
|
92
|
+
> wrap the component in a native element and use `v-on="hoverEvents"` on the wrapper, or break the object up and bind
|
|
93
|
+
> the handlers individually with `.native`. Neither workaround is needed in Vue 3, where listeners on a component
|
|
94
|
+
> automatically fall through to the root element.
|
|
95
|
+
|
|
96
|
+
<Source
|
|
97
|
+
format={'html'}
|
|
98
|
+
dark
|
|
99
|
+
code={`
|
|
100
|
+
<!-- Option A: wrap in a native element -->
|
|
101
|
+
<span v-on="hoverEvents" tabindex="0">
|
|
102
|
+
<bento-tag ref="triggerRef" label="Hover me" />
|
|
103
|
+
</span>
|
|
104
|
+
|
|
105
|
+
<!-- Option B: bind events individually with .native -->
|
|
106
|
+
|
|
107
|
+
<bento-tag ref="triggerRef" label="Hover me" tabindex="0" @mouseenter.native="open" @mouseleave.native="close"
|
|
108
|
+
@focusin.native="open" @focusout.native="close" /> `} />
|
|
109
|
+
|
|
51
110
|
## Accessibility 🚧
|
|
52
111
|
|
|
53
112
|
NOTE: The focus trap within the popover does not function as it should right now, this issue is currently being worked
|
|
@@ -71,12 +130,12 @@ documentation provided.
|
|
|
71
130
|
|
|
72
131
|
#### Please note that:
|
|
73
132
|
|
|
74
|
-
-
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
-
|
|
78
|
-
|
|
79
|
-
|
|
133
|
+
- The element that shows and hides the content has role button, e.g. use the `BentoButton` component.
|
|
134
|
+
- When the Popover is visible, the element with role button has `aria-expanded` set to `true`. When the content area is
|
|
135
|
+
hidden, it is set to `false`.
|
|
136
|
+
- When the Popover is visible, the popover needs to be identified by an `id` and the trigger element with role button
|
|
137
|
+
needs to have `aria-controls` set to the popover `id`. When the popover is hidden, the `aria-controls` attribute needs
|
|
138
|
+
to be set to `null`.
|
|
80
139
|
|
|
81
140
|
| ARIA attribute | is automatic? | fallback used | manual override |
|
|
82
141
|
| --------------- | ------------------------------------------------------------------------ | ------------- | --------------- |
|
|
@@ -86,4 +145,4 @@ documentation provided.
|
|
|
86
145
|
|
|
87
146
|
## Resources
|
|
88
147
|
|
|
89
|
-
-
|
|
148
|
+
- [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
|
-
<template> <section class="b-secondary-nav-category" :aria-label="label"> <bento-typography el="h1" class="b-secondary-nav-category__label" variant="caption" stronger>{{ label }}</bento-typography> <ul class="b-secondary-nav__list"> <li v-for="item in items" :key="item.value"> <secondary-nav-item :label="item.label" :value="item.value" :items="item.items" /> </li> </ul> </section> </template> <script lang="ts" setup> import { BentoTypography } from '@/components/typography'; import { SecondaryNavItem } from '../secondary-nav-item'; import { type BentoSecondaryNavItemsItem } from '../../secondary-nav.types'; defineProps<{ /** * The display name of the category */ label: string; /** * A list of {@see BentoSecondaryNavItemsItem} that belongs to this category */ items: Array<BentoSecondaryNavItemsItem>; }>(); </script> <script lang="ts"> /** * This component shouldn't be used directly. It is internally used by {@see BentoSecondaryNav} */ export default {}; </script> <style lang="scss" scoped src="./secondary-nav-category.scss" />
|
|
1
|
+
<template> <section class="b-secondary-nav-category" :aria-label="label"> <bento-typography el="h1" class="b-secondary-nav-category__label" variant="caption" stronger>{{ label }}</bento-typography> <ul class="b-secondary-nav__list"> <li v-for="item in items" :key="item.value"> <secondary-nav-item :label="item.label" :value="item.value" :items="item.items" :error-counter="item.errorCounter" /> </li> </ul> </section> </template> <script lang="ts" setup> import { BentoTypography } from '@/components/typography'; import { SecondaryNavItem } from '../secondary-nav-item'; import { type BentoSecondaryNavItemsItem } from '../../secondary-nav.types'; defineProps<{ /** * The display name of the category */ label: string; /** * A list of {@see BentoSecondaryNavItemsItem} that belongs to this category */ items: Array<BentoSecondaryNavItemsItem>; }>(); </script> <script lang="ts"> /** * This component shouldn't be used directly. It is internally used by {@see BentoSecondaryNav} */ export default {}; </script> <style lang="scss" scoped src="./secondary-nav-category.scss" />
|
package/dist/assets/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div class="b-secondary-nav-item"> <div class="b-secondary-nav-item__wrapper"> <button class="b-secondary-nav-item__option" :class="conditionalClasses" :aria-current="isItemSelected ? 'page' : undefined" :aria-expanded="expandleItemIsToggleOnly ? `${isOpen}` : undefined" :aria-controls="expandleItemIsToggleOnly ? subItemsId : undefined" @click="expandleItemIsToggleOnly ? onToggleClick() : onClick($event)" > <bento-typography class="b-secondary-nav-item__title" variant="body" stronger> {{ label }} </bento-typography> </button> <bento-button v-if="hasSubItems" :aria-hidden="expandleItemIsToggleOnly" :tabindex="expandleItemIsToggleOnly ? -1 : undefined" class="b-secondary-nav-item__toggle" variant="tertiary" :aria-label="isOpen ? `Collapse ${label}` : `Expand ${label}`" :aria-expanded="!expandleItemIsToggleOnly ? `${isOpen}` : undefined" :aria-controls="!expandleItemIsToggleOnly ? subItemsId : undefined" @click.stop="onToggleClick" > <template #iconLeft> <chevron-up-icon v-if="isOpen" class="b-secondary-nav-item__icon" aria-hidden="true" /> <chevron-down-icon v-else class="b-secondary-nav-item__icon" aria-hidden="true" /> </template> </bento-button> </div> <Transition :name="CONTENT_TRANSITION_NAME"> <div v-if="hasSubItems && isOpen" :id="subItemsId" :style="subItemsMaxHeight"> <ul ref="subItemsRef" class="b-secondary-nav-item__sub-items"> <li v-for="item in items" :key="item.value"> <secondary-nav-item :label="item.label" :value="item.value" :items="item.items" is-sub-item /> </li> </ul> </div> </Transition> </div> </template> <script lang="ts" setup> import { SECONDARY_NAV_STATE } from '../../secondary-nav.keys'; import { BentoButton } from '@/components/button'; import { BentoTypography } from '@/components/typography'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import ChevronUpIcon from '@adyen/ui-assets-icons-16/vue/chevron-up'; import { computed, inject, ref, watch } from 'vue'; import {
|
|
1
|
+
<template> <div class="b-secondary-nav-item"> <div class="b-secondary-nav-item__wrapper"> <button class="b-secondary-nav-item__option" :class="conditionalClasses" :aria-current="isItemSelected ? 'page' : undefined" :aria-expanded="expandleItemIsToggleOnly ? `${isOpen}` : undefined" :aria-controls="expandleItemIsToggleOnly ? subItemsId : undefined" @click="expandleItemIsToggleOnly ? onToggleClick() : onClick($event)" > <bento-typography class="b-secondary-nav-item__title" variant="body" stronger> {{ label }} </bento-typography> <bento-tag v-if="errorCounter" class="b-secondary-nav-item__error-counter" variant="red"> <template #icon> <cross-circle-fill-icon aria-hidden="true" /> </template> <span>{{ errorCounter }}</span> <span class="b-secondary-nav-item__visually-hidden">{{ tc('errorCount', errorCounter) }}</span> </bento-tag> </button> <bento-button v-if="hasSubItems" :aria-hidden="expandleItemIsToggleOnly" :tabindex="expandleItemIsToggleOnly ? -1 : undefined" class="b-secondary-nav-item__toggle" variant="tertiary" :aria-label="isOpen ? `Collapse ${label}` : `Expand ${label}`" :aria-expanded="!expandleItemIsToggleOnly ? `${isOpen}` : undefined" :aria-controls="!expandleItemIsToggleOnly ? subItemsId : undefined" @click.stop="onToggleClick" > <template #iconLeft> <chevron-up-icon v-if="isOpen" class="b-secondary-nav-item__icon" aria-hidden="true" /> <chevron-down-icon v-else class="b-secondary-nav-item__icon" aria-hidden="true" /> </template> </bento-button> </div> <Transition :name="CONTENT_TRANSITION_NAME"> <div v-if="hasSubItems && isOpen" :id="subItemsId" :style="subItemsMaxHeight"> <ul ref="subItemsRef" class="b-secondary-nav-item__sub-items"> <li v-for="item in items" :key="item.value"> <secondary-nav-item :label="item.label" :value="item.value" :items="item.items" :error-counter="item.errorCounter" is-sub-item /> </li> </ul> </div> </Transition> </div> </template> <script lang="ts" setup> import { SECONDARY_NAV_STATE } from '../../secondary-nav.keys'; import { BentoButton } from '@/components/button'; import { BentoTag } from '@/components/tag'; import { BentoTypography } from '@/components/typography'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import ChevronUpIcon from '@adyen/ui-assets-icons-16/vue/chevron-up'; import CrossCircleFillIcon from '@adyen/ui-assets-icons-16/vue/cross-circle-fill'; import { computed, inject, ref, watch } from 'vue'; import { useI18n } from '@/utils/ts/i18n'; import { generateUid } from '@/core/utils/ts'; import { type BentoSecondaryNavItemsItem } from '../../secondary-nav.types'; import { useExpandableContentHeight } from '@/composables/use-expandable-content-height/use-expandable-content-height'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { tc } = useI18n<{ message: MessageSchema }>({ messages }); const { activeItemValue, updateActiveItemValue, allowExpandableItemNavigation } = inject(SECONDARY_NAV_STATE); const CONTENT_TRANSITION_NAME = 'b-secondary-nav-item__animation--content'; const props = defineProps<{ /** * The display name of the navigation item */ label: string; /** * The value of this item. */ value: string; /** * Nested sub-items for this item. */ items?: Array<BentoSecondaryNavItemsItem>; /** * Defines if the item is a subitem */ isSubItem?: boolean; /** * Defines the number of errors pertaining to this item */ errorCounter?: number; }>(); const isOpen = ref(false); const subItemsId = generateUid('secondary-nav-sub-items'); const subItemsRef = ref<HTMLUListElement>(null); const hasSubItems = computed(() => !!props.items?.length); const isItemSelected = computed(() => activeItemValue.value === props.value); const expandleItemIsToggleOnly = computed(() => hasSubItems.value && !allowExpandableItemNavigation.value); const { contentHeight } = useExpandableContentHeight(subItemsRef, isOpen); const subItemsMaxHeight = computed(() => (contentHeight.value ? { 'max-height': contentHeight.value } : {})); const conditionalClasses = computed(() => ({ 'b-secondary-nav-item__option--active': isItemSelected.value, 'b-secondary-nav-item__option--expandable': hasSubItems.value, 'b-secondary-nav-item__option--sub-item': props.isSubItem, })); const emit = defineEmits<{ /** * Event emitted when this element is clicked */ (e: 'click', clickEvent: MouseEvent): void; }>(); const onClick = (e: MouseEvent) => { emit('click', e); updateActiveItemValue(props.value); }; const onToggleClick = () => { isOpen.value = !isOpen.value; }; const hasActiveDescendant = (items: Array<BentoSecondaryNavItemsItem>, value: string): boolean => { return items.some( item => item.value === value || (item.items ? hasActiveDescendant(item.items, value) : false) ); }; const hasDescendantErrors = (items: Array<BentoSecondaryNavItemsItem>): boolean => items.some(item => !!item.errorCounter || (item.items ? hasDescendantErrors(item.items) : false)); watch( [activeItemValue, () => props.items], ([activeValue]) => { if (hasSubItems.value && props.items) { if (hasActiveDescendant(props.items, activeValue) || hasDescendantErrors(props.items)) { isOpen.value = true; } } }, { immediate: true, deep: true } ); </script> <script lang="ts"> /** * This component shouldn't be used directly. It is internally used by {@see BentoSecondaryNav} */ export default { i18n: { messages } }; </script> <style lang="scss" scoped src="./secondary-nav-item.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoSecondaryNav from './secondary-nav.vue'; import WithSubItemsCodeSample from './__tests__/secondary-nav-with-sub-items.vue?raw'; import type { Meta, StoryObj } from '@storybook/vue'; import { type BentoSecondaryNavItems } from '@/components/secondary-nav/secondary-nav.types'; const meta: Meta = { title: 'Secondary Nav', component: BentoSecondaryNav, }; export default meta; type Story = StoryObj<typeof BentoSecondaryNav>; const DEFAULT_ITEMS: BentoSecondaryNavItems = [ { label: 'Option 1', value: 'option_1', }, { label: 'Option 2', value: 'option_2', }, { label: 'Option 3', value: 'option_3', }, ]; const ITEMS_WITH_CATEGORIES: BentoSecondaryNavItems = [ { label: 'Category 1', items: [ { label: 'Option 1', value: 'option_1', }, { label: 'Option 2', value: 'option_2', }, ], }, { label: 'Category 2', items: [ { label: 'Option 3', value: 'option_3', }, { label: 'Option 4', value: 'option_4', }, { label: 'Option 5', value: 'option_5', }, ], }, ]; const ITEMS_WITH_SUB_ITEMS: BentoSecondaryNavItems = ITEMS_WITH_CATEGORIES.map(category => ({ ...category, items: category.items.map(item => { if (item.value === 'option_2') { return { ...item, items: [ { label: 'Option 2.1', value: 'option_2_1', }, { label: 'Option 2.2', value: 'option_2_2', }, ], }; } if (item.value === 'option_4') { return { ...item, items: [ { label: 'Option 4.1', value: 'option_4_1', }, { label: 'Option 4.2', value: 'option_4_2', }, ], }; } return item; }), }));
|
|
1
|
+
import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoSecondaryNav from './secondary-nav.vue'; import DefaultCodeSample from './__tests__/secondary-nav-default-example.vue?raw'; import WithCategoriesCodeSample from './__tests__/secondary-nav-with-categories-example.vue?raw'; import WithSubItemsCodeSample from './__tests__/secondary-nav-with-sub-items-example.vue?raw'; import type { Meta, StoryObj } from '@storybook/vue'; import { type BentoSecondaryNavItems } from '@/components/secondary-nav/secondary-nav.types'; const meta: Meta = { title: 'Secondary Nav', component: BentoSecondaryNav, }; export default meta; type Story = StoryObj<typeof BentoSecondaryNav>; const DEFAULT_ITEMS: BentoSecondaryNavItems = [ { label: 'Option 1', value: 'option_1', }, { label: 'Option 2', value: 'option_2', errorCounter: 3, }, { label: 'Option 3', value: 'option_3', }, ]; const ITEMS_WITH_CATEGORIES: BentoSecondaryNavItems = [ { label: 'Category 1', items: [ { label: 'Option 1', value: 'option_1', errorCounter: 1, }, { label: 'Option 2', value: 'option_2', }, ], }, { label: 'Category 2', items: [ { label: 'Option 3', value: 'option_3', }, { label: 'Option 4', value: 'option_4', }, { label: 'Option 5', value: 'option_5', }, ], }, ]; const ITEMS_WITH_SUB_ITEMS: BentoSecondaryNavItems = ITEMS_WITH_CATEGORIES.map(category => ({ ...category, items: category.items.map(item => { if (item.value === 'option_2') { return { ...item, items: [ { label: 'Option 2.1', value: 'option_2_1', }, { label: 'Option 2.2', value: 'option_2_2', }, ], }; } if (item.value === 'option_4') { return { ...item, items: [ { label: 'Option 4.1', value: 'option_4_1', errorCounter: 2, }, { label: 'Option 4.2', value: 'option_4_2', }, ], }; } return item; }), })); export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoSecondaryNav, }, props: Object.keys(argTypes), template: ` <bento-secondary-nav v-bind="args" @change="clickedAction" /> `, setup(props) { const clickedAction = value => action('change')(value); return { // Values args: isVue2 ? props : _args, // Events clickedAction, }; }, }), args: { items: DEFAULT_ITEMS, value: 'option_1', }, parameters: storybookDocsParameter(DefaultCodeSample), }; export const WithCategories: Story = { render: (_args, { argTypes }) => ({ components: { BentoSecondaryNav, }, props: Object.keys(argTypes), template: ` <bento-secondary-nav v-bind="args" @change="clickedAction" /> `, setup(props) { const clickedAction = value => action('change')(value); return { // Values args: isVue2 ? props : _args, // Events clickedAction, }; }, }), args: { items: ITEMS_WITH_CATEGORIES, }, parameters: storybookDocsParameter(WithCategoriesCodeSample), }; export const WithSubItems: Story = { render: (_args, { argTypes }) => ({ components: { BentoSecondaryNav, }, props: Object.keys(argTypes), template: ` <bento-secondary-nav v-bind="args" @change="clickedAction" /> `, setup(props) { const clickedAction = value => action('change')(value); return { args: isVue2 ? props : _args, clickedAction, }; }, }), args: { value: 'option_2_1', items: ITEMS_WITH_SUB_ITEMS, }, parameters: storybookDocsParameter(WithSubItemsCodeSample), };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import type { Ref } from 'vue'; export interface BentoSecondaryNavState { activeItemValue: Ref<string>; updateActiveItemValue: (id: string) => void; allowExpandableItemNavigation: Ref<boolean>; } export interface BentoSecondaryNavItemsItem { label: string; value: string; items?: Array<BentoSecondaryNavItemsItem>; } export interface BentoSecondaryNavItemsCategory { label: string; items: Array<BentoSecondaryNavItemsItem>; } export type BentoSecondaryNavItems = Array<BentoSecondaryNavItemsItem | BentoSecondaryNavItemsCategory>;
|
|
1
|
+
import type { Ref } from 'vue'; export interface BentoSecondaryNavState { activeItemValue: Ref<string>; updateActiveItemValue: (id: string) => void; allowExpandableItemNavigation: Ref<boolean>; } export interface BentoSecondaryNavItemsItem { label: string; value: string; items?: Array<BentoSecondaryNavItemsItem>; errorCounter?: number; } export interface BentoSecondaryNavItemsCategory { label: string; items: Array<BentoSecondaryNavItemsItem>; } export type BentoSecondaryNavItems = Array<BentoSecondaryNavItemsItem | BentoSecondaryNavItemsCategory>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <nav class="b-secondary-nav"> <bento-dropdown v-if="isMobile" :aria-label="t('mobileDropdownAriaLabel')" :items="mobileDropdownItems" :static-categories="hasCategories" :model-value="activeItemValue" :is-option-disabled="isMobileOptionDisabled" @update:model-value="updateActiveItemValue" > <template #display-value="{ label }"> <bento-typography el="span"> {{ label }} </bento-typography> </template> <template #default="{ label, data }"> <bento-typography el="
|
|
1
|
+
<template> <nav class="b-secondary-nav"> <bento-dropdown v-if="isMobile" :aria-label="t('mobileDropdownAriaLabel')" :items="mobileDropdownItems" :static-categories="hasCategories" :model-value="activeItemValue" :is-option-disabled="isMobileOptionDisabled" @update:model-value="updateActiveItemValue" > <template #display-value="{ label }"> <bento-typography el="span"> {{ label }} </bento-typography> </template> <template #default="{ label, data }"> <bento-typography el="div" :class="{ 'b-secondary-nav__option': true, 'b-secondary-nav__nested-option': !!data?.level }" > {{ label }} <bento-tag v-if="data?.errorCounter" class="b-secondary-nav__error-counter" variant="red"> <template #icon> <cross-circle-fill-icon aria-hidden="true" /> </template> <span>{{ data.errorCounter }}</span> <span class="b-secondary-nav__visually-hidden">{{ tc('errorCount', data.errorCounter) }}</span> </bento-tag> </bento-typography> </template> </bento-dropdown> <ul v-else class="b-secondary-nav__list"> <li v-for="item in items" :key="item.label"> <secondary-nav-category v-if="'items' in item && !('value' in item)" :label="item.label" :items="item.items" /> <secondary-nav-item v-else :label="item.label" :value="item.value" :items="item.items" :error-counter="item.errorCounter" /> </li> </ul> </nav> </template> <script setup lang="ts"> import { computed, provide, readonly, ref, toRef, watch } from 'vue'; import { SecondaryNavItem } from './components/secondary-nav-item'; import { SecondaryNavCategory } from './components/secondary-nav-category'; import { SECONDARY_NAV_STATE } from './secondary-nav.keys'; import { type BentoSecondaryNavItems, type BentoSecondaryNavItemsCategory, type BentoSecondaryNavItemsItem, type BentoSecondaryNavState, } from './secondary-nav.types'; import { BentoDropdown } from '@/components/dropdown'; import { BentoTypography } from '@/components/typography'; import { BentoTag } from '@/components/tag'; import CrossCircleFillIcon from '@adyen/ui-assets-icons-16/vue/cross-circle-fill'; import { useBreakpoints } from '@vueuse/core'; import { useI18n } from '@/utils/ts/i18n'; import { type BentoListboxOptionItem, type BentoListboxOptions } from '@/types/listbox'; import { BMediaQuerySMax } from '@adyen/bento-design-tokens/dist/js/bento/es6'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t, tc } = useI18n<{ message: MessageSchema }>({ messages }); const breakpoints = useBreakpoints({ s: BMediaQuerySMax, }); const isMobile = breakpoints.smallerOrEqual('s'); const props = defineProps<{ /** * An array containing all the items on the nav. The array can have either {@see BentoSecondaryNavItemsCategory }, * which is used to group different items or {@see BentoSecondaryNavItemsItem } if you don't need to group them. */ items: BentoSecondaryNavItems; /** * The value of the selected item. * If nothing is passed, it defaults to the first item value in items. */ value?: string; /** * When true, items that contain subitems are also clickable and can be selected. * When false, those items only act as expand/collapse toggles. */ allowExpandableItemNavigation?: boolean; }>(); const emit = defineEmits<{ /** * Emmited when the current selected item changes */ (e: 'change', category: string): void; }>(); const isCategory = (item: BentoSecondaryNavItems[number]): item is BentoSecondaryNavItemsCategory => 'items' in item && !('value' in item); const mapNestedItems = (items: Array<BentoSecondaryNavItemsItem>, level = 0): BentoListboxOptions => items.flatMap((item): BentoListboxOptions => { const currentItem: BentoListboxOptionItem = { label: item.label, value: item.value, data: { level, hasSubItems: !!item.items?.length, errorCounter: item.errorCounter }, }; if (item.items?.length) { return [currentItem, ...mapNestedItems(item.items, level + 1)]; } return [currentItem]; }); const isMobileOptionDisabled = (option: BentoListboxOptionItem): boolean => !props.allowExpandableItemNavigation && (option.data as { hasSubItems?: boolean })?.hasSubItems; const hasCategories = computed(() => props.items.some(item => isCategory(item))); const mobileDropdownItems = computed<BentoListboxOptions>(() => { return props.items.flatMap(item => { if (isCategory(item)) { return [ { label: item.label, value: `category-${item.label}`, items: mapNestedItems(item.items), } satisfies BentoListboxOptionItem, ]; } if (item.items?.length) { return mapNestedItems([item]); } return [ { label: item.label, value: item.value, data: { level: 0, errorCounter: item.errorCounter }, } satisfies BentoListboxOptionItem, ]; }); }); const flattenNestedItems = (items: Array<BentoSecondaryNavItemsItem>): Array<BentoSecondaryNavItemsItem> => items.flatMap(item => [item, ...(item.items?.length ? flattenNestedItems(item.items) : [])]); const findFirstSelectableNestedItemValue = (items: Array<BentoSecondaryNavItemsItem>): string => flattenNestedItems(items).find(item => props.allowExpandableItemNavigation || !item.items?.length)?.value ?? ''; const activeItemValueFallback = computed(() => { if (!props.items.length) { return ''; } return findFirstSelectableNestedItemValue( props.items.flatMap(item => isCategory(item) ? flattenNestedItems(item.items) : flattenNestedItems([item]) ) ); }); const activeItemValue = ref(props.value ?? activeItemValueFallback.value); const updateActiveItemValue = (value: string) => { activeItemValue.value = value; emit('change', value); }; // The component's state is inject on `secondary-nav-item` to determine the // current active item and to change its value const state: BentoSecondaryNavState = { activeItemValue: readonly(activeItemValue), updateActiveItemValue, allowExpandableItemNavigation: toRef(props, 'allowExpandableItemNavigation'), }; watch( () => props.value, value => { if (value !== undefined) { activeItemValue.value = value; } } ); provide(SECONDARY_NAV_STATE, state); </script> <script lang="ts"> /** * Secondary Nav offers users a structured way to access information. * It also provides a clear hierarchy for users to understand and navigate through various information. * * @example * import { BentoSecondaryNav } from '@adyen/bento-vue2'; * * export default { * components: { BentoSecondaryNav }, * template: ` * <bento-secondary-nav :value="value" :items="items" /> * `, * setup() { * const value = ref(2) // Active item value * const items: BentoSecondaryNavItemInfo[] = [ * { label: 'Option 1', value: 1 }, * { label: 'Option 2', value: 2 }, * ]; * return { items, value }; * } * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./secondary-nav.scss" />
|