@adyen/bento-mcp 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +38 -12
  3. package/dist/assets/components/avatar/avatar.stories.ts +1 -1
  4. package/dist/assets/components/avatar/avatar.types.ts +1 -0
  5. package/dist/assets/components/avatar/avatar.vue +1 -1
  6. package/dist/assets/components/avatar/components/avatar-image/avatar-image.vue +1 -1
  7. package/dist/assets/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.vue +1 -1
  8. package/dist/assets/components/date-picker/date-picker.vue +1 -1
  9. package/dist/assets/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.vue +1 -1
  10. package/dist/assets/components/date-range-picker/date-range-picker.stories.ts +1 -1
  11. package/dist/assets/components/date-range-picker/date-range-picker.vue +1 -1
  12. package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue +1 -1
  13. package/dist/assets/components/dropdown/components/dropdown-default-textbox/dropdown-default-textbox.vue +1 -1
  14. package/dist/assets/components/dropdown/components/dropdown-options-container/dropdown-options-container.vue +1 -1
  15. package/dist/assets/components/dropdown/components/dropdown-small-textbox/dropdown-small-textbox.vue +1 -1
  16. package/dist/assets/components/dropdown/composables/use-keyboard-navigation.types.ts +1 -1
  17. package/dist/assets/components/dropdown/dropdown.vue +1 -1
  18. package/dist/assets/components/header-with-views/components/header-with-views-all-views/header-with-views-all-views.vue +1 -1
  19. package/dist/assets/components/header-with-views/header-with-views.vue +1 -1
  20. package/dist/assets/components/internal/calendar/composables/use-granularity-adjustments.types.ts +1 -1
  21. package/dist/assets/components/internal/listbox/components/listbox-option/listbox-option.vue +1 -1
  22. package/dist/assets/components/internal/listbox/components/listbox-single-select/listbox-single-select.vue +1 -1
  23. package/dist/assets/components/internal/listbox/components/listbox-single-select-option/listbox-single-select-option.vue +1 -1
  24. package/dist/assets/components/internal/listbox/listbox.vue +1 -1
  25. package/dist/assets/components/menu/menu.docs.mdx +8 -7
  26. package/dist/assets/components/menu/menu.vue +1 -1
  27. package/dist/assets/components/popover/popover.types.ts +1 -1
  28. package/dist/assets/components/popover/popover.vue +1 -1
  29. package/dist/assets/components/popper-container/popper-container.types.ts +1 -1
  30. package/dist/assets/components/popper-container/popper-container.vue +1 -1
  31. package/dist/assets/components/progress-bar/progress-bar.docs.mdx +102 -0
  32. package/dist/assets/components/progress-bar/progress-bar.stories.ts +1 -0
  33. package/dist/assets/components/progress-bar/progress-bar.types.ts +1 -0
  34. package/dist/assets/components/progress-bar/progress-bar.vue +1 -0
  35. package/dist/assets/components.json +1 -0
  36. package/dist/assets/deprecations.md +157 -157
  37. package/dist/assets/index.ts +1 -1
  38. package/dist/assets/usage.json +80 -79
  39. package/dist/assets/variables.css +42 -41
  40. package/dist/main.js +1 -1
  41. package/package.json +1 -1
  42. package/dist/assets/components/avatar/components/avatar-image/avatar-image.types.ts +0 -1
  43. package/dist/assets/components/internal/calendar/composables/use-range-pane.types.ts +0 -1
@@ -1 +1 @@
1
- <template> <div :id="id" ref="listboxRef" v-bento-keyboard-navigation-directive class="b-listbox" tabindex="-1" v-bind="listboxContainerAttributes" :style="containerProps?.style" data-testid="listbox-container" @scroll="onListboxContainerScroll" > <div v-bind="wrapperProps"> <!-- Loading filtered items loading --> <div v-if="componentLoading" class="b-listbox__loading-filtered-items"> <bento-loading-indicator /> </div> <!-- Empty search message --> <div v-else-if="isSearchResultEmpty" class="b-listbox__empty-search-message"> <bento-empty-state v-if="emptyState" v-bind="limitedEmptyStateProps" /> <bento-typography v-else el="span">{{ noResultsMessage }}</bento-typography> </div> <template v-else> <!-- Multi select --> <template v-if="multiple"> <bento-listbox-multi-select ref="listboxItemRef" :has-more-items="hasMoreItems" v-bind="ariaAttributes" :items="items" :lazy-load-type="lazyLoadType" :static-categories="staticCategories" :selected-values="selectedValue" :searching="searching" :is-option-disabled="isOptionDisabled" v-on="listboxListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-multi-select> </template> <!-- Single select --> <template v-else> <bento-listbox-single-select ref="listboxItemRef" :items="virtualisedItems" :total-item-count="totalItemCount" :scroll-to-index="scrollToIndex" :virtual-scroll-start-index="virtualScrollStartIndex" v-bind="ariaAttributes" :selected-value="selectedValue" :static-categories="staticCategories" :is-option-disabled="isOptionDisabled" v-on="listboxListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-single-select> </template> <!-- Lazy Loading --> <bento-listbox-lazy-load v-if="lazyLoadType !== 'none'" :has-more-items="hasMoreItems" :loading="loading" :type="lazyLoadType" @show-more="emitShowMore" /> </template> </div> </div> </template> <script lang="ts"> import { computed, defineComponent, type PropType, ref, toRef } from 'vue'; import { type BentoListboxEmptyStateProps, BentoListboxEvent, type BentoListboxIsOptionDisabled, BentoListboxLazyLoadType, type BentoListboxOptionItem, type BentoListboxOptions, type BentoListboxVirtulisationOptions, } from '@/types/listbox'; import { BentoKeyboardNavigationKeyDownEvent } from '@/types/keyboard-navigation'; import { BentoKeyboardNavigationDirective } from '@/directives'; import { BentoListboxSingleSelect } from './components/listbox-single-select'; import { BentoListboxMultiSelect } from './components/listbox-multi-select'; import { BentoTypography } from '@/components/typography'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoListboxLazyLoad } from './components/listbox-lazy-load'; import { BentoLoadingButtonState } from '@/components/button'; import { BentoEmptyState } from '@/components/empty-state'; import { debounce } from '@/utils/ts/debounce'; import { useVirtualList } from '@/composables/use-virtual-list'; import type { HTMLAttributes } from 'vue/types/jsx.d.ts'; const DEFAULT_DROPDOWN_ITEM_HEIGHT = 36; /** * Listbox container. * It lists all the available options. * * @example * <bento-listbox * v-slot="{label, value}" * :id="listboxOptionsContainerId" * :aria-label="ariaLabel" * :disabled="disabled" * :multiple="multiple" * :selected="value" * :isOptionDisabled="option => option.value === 2" * :items="[{ * { label: 'Option 1', value: 1 }, * { label: 'Option 2', value: 2 }, * }]" * @select="onOptionSelected" * > * </bento-listbox> */ export default defineComponent({ name: 'bento-listbox', components: { BentoEmptyState, BentoListboxSingleSelect, BentoListboxMultiSelect, BentoTypography, BentoLoadingIndicator, BentoListboxLazyLoad, }, directives: { BentoKeyboardNavigationDirective }, props: { /** * Defines a string value that labels an interactive element. */ ariaLabel: { type: String, default: null }, /** * Empty state props that will be used with the inner empty state component to be displayed when no search results are found. */ emptyState: { type: Object as PropType<BentoListboxEmptyStateProps>, default: undefined, }, /** * Indicates whether there are more items to load */ hasMoreItems: { type: Boolean, default: false }, /** * Identifies the listbox whose contents are controlled by the the combobox on which the aria-controls attribute is set. */ id: { type: String, required: true }, /** * Function that allows the options to be disabled * * @type {BentoListboxIsOptionDisabled} * @param {BentoListboxOptionItem} option - Listbox option object * @param {string} option.label - Option's label text * @param {string|number} option.value - Option's value * @param {unknown} option.data - Option's extra data to be passed to the default slot */ isOptionDisabled: { type: Function as PropType<BentoListboxIsOptionDisabled>, default: () => undefined, }, /** * The option elements to populate the listbox with. * It must be an array of {@see BentoListboxOptionItem } * * @property {string} value.label - Text to be displayed in the option * * @property {string,number} value.value - Value of the option */ items: { type: Array as PropType<BentoListboxOptions>, required: true, }, /** * Indicates if new options are lazy loading. */ loading: { type: Boolean, default: false }, /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button */ lazyLoadType: { type: String as PropType<BentoListboxLazyLoadType | `${BentoListboxLazyLoadType}`>, default: BentoListboxLazyLoadType.NONE, validator: (value: BentoListboxLazyLoadType) => Object.values(BentoListboxLazyLoadType).includes(value), }, /** * Indicates the listbox is loading filtered items. */ componentLoading: { type: Boolean, default: false }, /** * If enabled, the user will be able to select multiple items. */ multiple: { type: Boolean, default: false }, /** * If enabled, dropdown will display non-selectable, static categories */ staticCategories: { type: Boolean, default: false }, /** * Messaged displayed when no items are listed in the listbox */ noResultsMessage: { type: String, required: true }, /** * Flag that indicates that an external search it's being made * to filter out the items inside the the multi-select listbox * * If enabled, hides the "Select all" checkbox in "multiple" mode. */ searching: BentoListboxMultiSelect.props.searching, /** * The `input` value. * Providing an empty string or empty array will select no options. * Set to an empty string `''` if you don't want any of the available options to be selected for single select */ selectedValue: { type: Array as PropType<BentoListboxOptions>, default: () => [], }, /** * Enables virtual scrolling if set to true or by providing an object with itemHeight function. * The itemHeight function is used to calculate the height of rendered item given it's index. */ virtualScroll: { type: [Boolean, Object] as PropType<BentoListboxVirtulisationOptions>, default: false, }, }, emits: [ BentoListboxEvent.SELECT, BentoListboxEvent.SPACE, BentoListboxEvent.SHOW_MORE, BentoListboxEvent.ENTER, BentoListboxEvent.ESCAPE, BentoListboxEvent.TAB, ], setup(props, { emit, slots, expose }) { const listboxItemRef = ref([]); const items = toRef(props, 'items'); const isLazyLoadListbox = computed(() => props.lazyLoadType !== BentoListboxLazyLoadType.AUTOMATIC); const hasAutomaticLazyLoader = computed(() => props.lazyLoadType === BentoListboxLazyLoadType.AUTOMATIC); // Virtual scrolling const virtualScrollItemHeight = () => { if (typeof props?.virtualScroll === 'object' && props?.virtualScroll?.itemHeight) { return props.virtualScroll.itemHeight; } return DEFAULT_DROPDOWN_ITEM_HEIGHT; }; const isVirtualScrollEnabled = !props?.multiple && props?.virtualScroll !== false && props?.lazyLoadType === 'none'; const { containerRef: listboxRef, containerProps, onScroll: onContainerScroll, list: virtualisedItems, wrapperProps, scrollToIndex, startIndex: virtualScrollStartIndex, } = useVirtualList<BentoListboxOptionItem>(items, { itemHeight: virtualScrollItemHeight(), // Disable virtual scrolling if not enabled or if lazy loading is enabled or if multi-select disabled: !isVirtualScrollEnabled, }); const totalItemCount = computed(() => (isVirtualScrollEnabled ? items.value.length : 0)); // Aria a11y const ariaAtomic = computed<HTMLAttributes['aria-atomic']>(() => props.componentLoading !== undefined || isLazyLoadListbox.value ? 'true' : undefined ); const ariaBusy = computed<HTMLAttributes['aria-busy']>(() => (isLazyLoadListbox.value && props.loading) || props.componentLoading ? 'true' : 'false' ); const ariaLive = computed<HTMLAttributes['aria-live']>(() => isLazyLoadListbox.value || props.componentLoading !== undefined ? 'polite' : undefined ); const ariaAttributes = computed( () => ({ 'aria-label': props.ariaLabel, 'aria-busy': ariaBusy.value, 'aria-atomic': ariaAtomic.value, 'aria-live': ariaLive.value, }) as HTMLAttributes ); const listboxContainerAttributes = computed( () => (props.componentLoading ? { ...ariaAttributes.value, role: 'alert', } : {}) as HTMLAttributes ); const onOptionSelected = eventName => (selectedItem: BentoListboxOptions) => { emit(eventName, selectedItem); }; const emitShowMore = () => { emit(BentoListboxEvent.SHOW_MORE); }; const emitShowMoreOnScrollEnd = debounce(() => { const container = listboxRef.value; if (!container) { return; } const isAtBottom = Math.round(container.scrollTop) + container.clientHeight >= container.scrollHeight; if (props.hasMoreItems && isAtBottom && !props.loading) { emitShowMore(); } }); const onListboxContainerScroll = () => { if (hasAutomaticLazyLoader.value) { emitShowMoreOnScrollEnd(); } else { onContainerScroll(); } }; const isSearchResultEmpty = computed(() => props.items?.length === 0); const shouldDisplayShowMoreButton = computed( () => props.lazyLoadType === BentoListboxLazyLoadType.BUTTON && props.hasMoreItems ); const loadingButtonState = computed(() => props.loading ? BentoLoadingButtonState.LOADING : BentoLoadingButtonState.START ); const limitedEmptyStateProps = computed<InstanceType<typeof BentoEmptyState>['$props']>(() => ({ ...props.emptyState, variant: 'condensed', })); const listboxListeners = { [BentoListboxEvent.SELECT]: onOptionSelected(BentoListboxEvent.SELECT), [BentoListboxEvent.SPACE]: onOptionSelected(BentoListboxEvent.SPACE), [BentoListboxEvent.TAB]: onOptionSelected(BentoListboxEvent.TAB), [BentoListboxEvent.ENTER]: onOptionSelected(BentoListboxEvent.ENTER), }; expose({ listboxRef, listboxItemRef }); return { // Refs listboxRef, listboxItemRef, // a11y ariaAttributes, listboxContainerAttributes, // Values containerProps, isSearchResultEmpty, limitedEmptyStateProps, loadingButtonState, slots, shouldDisplayShowMoreButton, virtualisedItems, wrapperProps, // Virtual scroll totalItemCount, scrollToIndex, virtualScrollStartIndex, // Enums BentoKeyboardNavigationKeyDownEvent, BentoListboxEvent, // Methods onListboxContainerScroll, onOptionSelected, emitShowMore, listboxListeners, }; }, }); </script> <style lang="scss" scoped src="./listbox.scss" />
1
+ <template> <div :id="id" ref="listboxRef" v-bento-keyboard-navigation-directive class="b-listbox" tabindex="-1" v-bind="listboxContainerAttributes" :style="containerProps?.style" data-testid="listbox-container" @scroll="onListboxContainerScroll" > <div v-bind="wrapperProps"> <!-- Loading filtered items loading --> <div v-if="componentLoading" class="b-listbox__loading-filtered-items"> <bento-loading-indicator /> </div> <!-- Empty search message --> <div v-else-if="isSearchResultEmpty" class="b-listbox__empty-search-message"> <bento-empty-state v-if="emptyState" v-bind="limitedEmptyStateProps" /> <bento-typography v-else el="span">{{ noResultsMessage }}</bento-typography> </div> <template v-else> <!-- Multi select --> <template v-if="multiple"> <bento-listbox-multi-select ref="listboxItemRef" :has-more-items="hasMoreItems" v-bind="ariaAttributes" :items="items" :lazy-load-type="lazyLoadType" :static-categories="staticCategories" :selected-values="selectedValue" :searching="searching" :is-option-disabled="isOptionDisabled" v-on="listboxListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-multi-select> </template> <!-- Single select --> <template v-else> <bento-listbox-single-select ref="listboxItemRef" :active-descendant-id="activeDescendantId" :items="virtualisedItems" :option-id-base="id" :total-item-count="totalItemCount" :scroll-to-index="scrollToIndex" :virtual-scroll-start-index="virtualScrollStartIndex" v-bind="ariaAttributes" :selected-value="selectedValue" :static-categories="staticCategories" :is-option-disabled="isOptionDisabled" v-on="listboxListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-single-select> </template> <!-- Lazy Loading --> <bento-listbox-lazy-load v-if="lazyLoadType !== 'none'" :has-more-items="hasMoreItems" :loading="loading" :type="lazyLoadType" @show-more="emitShowMore" /> </template> </div> </div> </template> <script lang="ts"> import { computed, defineComponent, type PropType, ref, toRef } from 'vue'; import { type BentoListboxEmptyStateProps, BentoListboxEvent, type BentoListboxIsOptionDisabled, BentoListboxLazyLoadType, type BentoListboxOptionItem, type BentoListboxOptions, type BentoListboxVirtulisationOptions, } from '@/types/listbox'; import { BentoKeyboardNavigationKeyDownEvent } from '@/types/keyboard-navigation'; import { BentoKeyboardNavigationDirective } from '@/directives'; import { BentoListboxSingleSelect } from './components/listbox-single-select'; import { BentoListboxMultiSelect } from './components/listbox-multi-select'; import { BentoTypography } from '@/components/typography'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoListboxLazyLoad } from './components/listbox-lazy-load'; import { BentoLoadingButtonState } from '@/components/button'; import { BentoEmptyState } from '@/components/empty-state'; import { debounce } from '@/utils/ts/debounce'; import { useVirtualList } from '@/composables/use-virtual-list'; import type { HTMLAttributes } from 'vue/types/jsx.d.ts'; const DEFAULT_DROPDOWN_ITEM_HEIGHT = 36; /** * Listbox container. * It lists all the available options. * * @example * <bento-listbox * v-slot="{label, value}" * :id="listboxOptionsContainerId" * :aria-label="ariaLabel" * :disabled="disabled" * :multiple="multiple" * :selected="value" * :isOptionDisabled="option => option.value === 2" * :items="[{ * { label: 'Option 1', value: 1 }, * { label: 'Option 2', value: 2 }, * }]" * @select="onOptionSelected" * > * </bento-listbox> */ export default defineComponent({ name: 'bento-listbox', components: { BentoEmptyState, BentoListboxSingleSelect, BentoListboxMultiSelect, BentoTypography, BentoLoadingIndicator, BentoListboxLazyLoad, }, directives: { BentoKeyboardNavigationDirective }, props: { /** * Defines a string value that labels an interactive element. */ ariaLabel: { type: String, default: null }, /** * Identifies the currently active option in the listbox. */ activeDescendantId: { type: String, default: null }, /** * Empty state props that will be used with the inner empty state component to be displayed when no search results are found. */ emptyState: { type: Object as PropType<BentoListboxEmptyStateProps>, default: undefined, }, /** * Indicates whether there are more items to load */ hasMoreItems: { type: Boolean, default: false }, /** * Identifies the listbox whose contents are controlled by the the combobox on which the aria-controls attribute is set. */ id: { type: String, required: true }, /** * Function that allows the options to be disabled * * @type {BentoListboxIsOptionDisabled} * @param {BentoListboxOptionItem} option - Listbox option object * @param {string} option.label - Option's label text * @param {string|number} option.value - Option's value * @param {unknown} option.data - Option's extra data to be passed to the default slot */ isOptionDisabled: { type: Function as PropType<BentoListboxIsOptionDisabled>, default: () => undefined, }, /** * The option elements to populate the listbox with. * It must be an array of {@see BentoListboxOptionItem } * * @property {string} value.label - Text to be displayed in the option * * @property {string,number} value.value - Value of the option */ items: { type: Array as PropType<BentoListboxOptions>, required: true, }, /** * Indicates if new options are lazy loading. */ loading: { type: Boolean, default: false }, /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button */ lazyLoadType: { type: String as PropType<BentoListboxLazyLoadType | `${BentoListboxLazyLoadType}`>, default: BentoListboxLazyLoadType.NONE, validator: (value: BentoListboxLazyLoadType) => Object.values(BentoListboxLazyLoadType).includes(value), }, /** * Indicates the listbox is loading filtered items. */ componentLoading: { type: Boolean, default: false }, /** * If enabled, the user will be able to select multiple items. */ multiple: { type: Boolean, default: false }, /** * If enabled, dropdown will display non-selectable, static categories */ staticCategories: { type: Boolean, default: false }, /** * Messaged displayed when no items are listed in the listbox */ noResultsMessage: { type: String, required: true }, /** * Flag that indicates that an external search it's being made * to filter out the items inside the the multi-select listbox * * If enabled, hides the "Select all" checkbox in "multiple" mode. */ searching: BentoListboxMultiSelect.props.searching, /** * The `input` value. * Providing an empty string or empty array will select no options. * Set to an empty string `''` if you don't want any of the available options to be selected for single select */ selectedValue: { type: Array as PropType<BentoListboxOptions>, default: () => [], }, /** * Enables virtual scrolling if set to true or by providing an object with itemHeight function. * The itemHeight function is used to calculate the height of rendered item given it's index. */ virtualScroll: { type: [Boolean, Object] as PropType<BentoListboxVirtulisationOptions>, default: false, }, }, emits: [ BentoListboxEvent.SELECT, BentoListboxEvent.SPACE, BentoListboxEvent.SHOW_MORE, BentoListboxEvent.ENTER, BentoListboxEvent.ESCAPE, BentoListboxEvent.TAB, ], setup(props, { emit, slots, expose }) { const listboxItemRef = ref([]); const items = toRef(props, 'items'); const isLazyLoadListbox = computed(() => props.lazyLoadType !== BentoListboxLazyLoadType.AUTOMATIC); const hasAutomaticLazyLoader = computed(() => props.lazyLoadType === BentoListboxLazyLoadType.AUTOMATIC); // Virtual scrolling const virtualScrollItemHeight = () => { if (typeof props?.virtualScroll === 'object' && props?.virtualScroll?.itemHeight) { return props.virtualScroll.itemHeight; } return DEFAULT_DROPDOWN_ITEM_HEIGHT; }; const isVirtualScrollEnabled = !props?.multiple && props?.virtualScroll !== false && props?.lazyLoadType === 'none'; const { containerRef: listboxRef, containerProps, onScroll: onContainerScroll, list: virtualisedItems, wrapperProps, scrollToIndex, startIndex: virtualScrollStartIndex, } = useVirtualList<BentoListboxOptionItem>(items, { itemHeight: virtualScrollItemHeight(), // Disable virtual scrolling if not enabled or if lazy loading is enabled or if multi-select disabled: !isVirtualScrollEnabled, }); const totalItemCount = computed(() => (isVirtualScrollEnabled ? items.value.length : 0)); // Aria a11y const ariaAtomic = computed<HTMLAttributes['aria-atomic']>(() => props.componentLoading !== undefined || isLazyLoadListbox.value ? 'true' : undefined ); const ariaBusy = computed<HTMLAttributes['aria-busy']>(() => (isLazyLoadListbox.value && props.loading) || props.componentLoading ? 'true' : 'false' ); const ariaLive = computed<HTMLAttributes['aria-live']>(() => isLazyLoadListbox.value || props.componentLoading !== undefined ? 'polite' : undefined ); const ariaAttributes = computed( () => ({ 'aria-label': props.ariaLabel, 'aria-busy': ariaBusy.value, 'aria-atomic': ariaAtomic.value, 'aria-live': ariaLive.value, }) as HTMLAttributes ); const listboxContainerAttributes = computed( () => (props.componentLoading ? { ...ariaAttributes.value, role: 'alert', } : {}) as HTMLAttributes ); const onOptionSelected = eventName => (selectedItem: BentoListboxOptions) => { emit(eventName, selectedItem); }; const emitShowMore = () => { emit(BentoListboxEvent.SHOW_MORE); }; const emitShowMoreOnScrollEnd = debounce(() => { const container = listboxRef.value; if (!container) { return; } const isAtBottom = Math.round(container.scrollTop) + container.clientHeight >= container.scrollHeight; if (props.hasMoreItems && isAtBottom && !props.loading) { emitShowMore(); } }); const onListboxContainerScroll = () => { if (hasAutomaticLazyLoader.value) { emitShowMoreOnScrollEnd(); } else { onContainerScroll(); } }; const isSearchResultEmpty = computed(() => props.items?.length === 0); const shouldDisplayShowMoreButton = computed( () => props.lazyLoadType === BentoListboxLazyLoadType.BUTTON && props.hasMoreItems ); const loadingButtonState = computed(() => props.loading ? BentoLoadingButtonState.LOADING : BentoLoadingButtonState.START ); const limitedEmptyStateProps = computed<InstanceType<typeof BentoEmptyState>['$props']>(() => ({ ...props.emptyState, variant: 'condensed', })); const listboxListeners = { [BentoListboxEvent.SELECT]: onOptionSelected(BentoListboxEvent.SELECT), [BentoListboxEvent.SPACE]: onOptionSelected(BentoListboxEvent.SPACE), [BentoListboxEvent.TAB]: onOptionSelected(BentoListboxEvent.TAB), [BentoListboxEvent.ENTER]: onOptionSelected(BentoListboxEvent.ENTER), }; expose({ listboxRef, listboxItemRef }); return { // Refs listboxRef, listboxItemRef, // a11y ariaAttributes, listboxContainerAttributes, // Values containerProps, isSearchResultEmpty, limitedEmptyStateProps, loadingButtonState, slots, shouldDisplayShowMoreButton, virtualisedItems, wrapperProps, // Virtual scroll totalItemCount, scrollToIndex, virtualScrollStartIndex, // Enums BentoKeyboardNavigationKeyDownEvent, BentoListboxEvent, // Methods onListboxContainerScroll, onOptionSelected, emitShowMore, listboxListeners, }; }, }); </script> <style lang="scss" scoped src="./listbox.scss" />
@@ -106,13 +106,14 @@ menu supports.
106
106
 
107
107
  ### Roles, states and properties
108
108
 
109
- | ARIA attribute | is automatic? | fallback used | manual override |
110
- | ----------------- | --------------------------------------------------------------------- | ------------- | --------------- |
111
- | `role="menu"` | Yes, placed on the actions list representing the menu. | - | - |
112
- | `role="menuitem"` | Yes, placed on each action item. | - | - |
113
- | `aria-haspopup` | Yes, automatically set to `true` on the menu button | - | - |
114
- | `aria-expanded` | Yes, set to `true` whenever the menu is open and to `false` otherwise | - | - |
115
- | `aria-controls` | Yes, automatically set on the button to point at the menu's popover | - | - |
109
+ | ARIA attribute | is automatic? | fallback used | manual override |
110
+ | ----------------- | --------------------------------------------------------------------- | --------------------------- | ------------------------------------------------------------------- |
111
+ | `role="menu"` | Yes, placed on the actions list representing the menu. | - | - |
112
+ | `role="menuitem"` | Yes, placed on each action item. | - | - |
113
+ | `aria-haspopup` | Yes, automatically set to `true` on the menu button | - | - |
114
+ | `aria-expanded` | Yes, set to `true` whenever the menu is open and to `false` otherwise | - | - |
115
+ | `aria-controls` | Yes, automatically set on the button to point at the menu's popover | - | - |
116
+ | `aria-label` | Set when no default slot is provided | Localized `Open menu` label | `button['aria-label']` overrides the fallback or default-slot label |
116
117
 
117
118
  ## Resources
118
119
 
@@ -1 +1 @@
1
- <template> <div :class="computedClasses" class="b-menu" data-testid="menu" @keydown.down="onKeyDown" @keydown.up="onKeyUp" @keydown.esc="closeMenu()" > <bento-toggle-button ref="menuButtonRef" class="b-menu__toggle-button" aria-haspopup="true" v-bind="computedButtonPropsWithDefaults" :aria-controls="showMenu ? menuId : null" :toggled="showMenu" :inverse="isInverseColorSet" @click="toggleShowMenu" > <template v-if="!hasSlot('default') || hasSlot('iconLeft')" #iconLeft> <slot name="iconLeft"> <options-vertical-icon :svg-title="t('menuButton')" /> </slot> </template> <template v-if="hasSlot('default')" #default> <slot /> </template> <template v-if="hasSlot('default')" #iconRight> <chevron-down-icon /> </template> </bento-toggle-button> <bento-teleport v-if="menuButtonRef" :disabled="!teleport"> <bento-popover v-if="menuButtonRef" :id="menuId" ref="popoverRef" :role="null" :open="showMenu" class="b-menu__popover" :style="computedMenuWidth" :target-element="menuButtonRef" :position="menuPosition" :fixed-positioning="menuFixedPositioning" :fallback-position="[ 'bottom-start', 'bottom-end', 'top-start', 'top-end', 'right-start', 'right-end', 'left-start', 'left-end', ]" :teleport="teleport" without-space > <menu-item-list ref="menuItemListRef" :close-menu-on-item-select="closeMenuOnItemSelect" :data="data" :focus-last-element="isLastElementFocused" :popover-width="menuWidth" @close="closeMenu" @menu-item-click="menuItemClick" /> </bento-popover> </bento-teleport> </div> </template> <script setup lang="ts"> import BentoToggleButton from '@/components/button/components/toggle-button/toggle-button.vue'; import { DIVIDER_INVERSE_INJECTION_KEY } from '@/components/divider/divider.keys'; import BentoPopover from '@/components/popover/popover.vue'; import { useClickOutside, useHasSlot } from '@/composables'; import { generateUid } from '@/core/utils/ts'; import { BentoTeleport } from '@/internal/teleport'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import OptionsVerticalIcon from '@adyen/ui-assets-icons-16/vue/options-vertical'; import { computed, inject, nextTick, onMounted, provide, ref, useSlots } from 'vue'; import MenuItemList from './components/menu-item-list/menu-item-list.vue'; import { MENU_INVERSE_INJECTION_KEY } from './menu.keys'; import { clampMenuWidth } from './composables/use-dynamic-menu-size/use-dynamic-menu-size'; import { type BentoMenuButtonProps, BentoMenuEvent, type BentoMenuProps } from './menu.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoMenuProps>(), { button: () => ({ variant: 'secondary', }) as BentoMenuButtonProps, closeMenuOnItemSelect: true, disableArrowDownOpeningMenu: false, menuPosition: 'bottom-start', menuFixedPositioning: false, menuWidth: null, revertFocusToButton: true, teleport: false, closeOnClickOutsideOptions: undefined, }); const emit = defineEmits<{ (e: BentoMenuEvent.CLOSE): void; (e: BentoMenuEvent.OPEN): void; (e: 'menu-item-click'): void; (e: 'click-outside'): void; }>(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const menuButtonRef = ref(null); const menuItemListRef = ref(null); const showMenu = ref(false); const isLastElementFocused = ref(false); const popoverRef = ref<HTMLElement>(null); const menuId = generateUid('menu'); const isInverseColorSet = inject(MENU_INVERSE_INJECTION_KEY, ref(false)); provide(DIVIDER_INVERSE_INJECTION_KEY, isInverseColorSet); const computedClasses = computed(() => ({ 'b-menu--inverse': isInverseColorSet.value, })); const computedMenuWidth = computed(() => props.menuWidth ? { width: `${clampMenuWidth(props.menuWidth)}px` } : null ); const computedButtonPropsWithDefaults = computed<BentoMenuButtonProps>(() => { if (!props?.button) { return undefined; } return { variant: 'secondary', ...props.button, }; }); const openMenu = () => { showMenu.value = true; emit(BentoMenuEvent.OPEN); }; const closeMenu = async (revertFocus = props.revertFocusToButton) => { if (!showMenu.value) { return; } showMenu.value = false; if (revertFocus) { // ensure that menu is closed and re-rendering is completed before focusing await nextTick(); menuButtonRef.value?.$el?.focus(); } emit(BentoMenuEvent.CLOSE); }; const toggleShowMenu = (e: MouseEvent) => { e.stopPropagation(); if (!showMenu.value) { openMenu(); } else { closeMenu(); } }; const onKeyDown = (event: KeyboardEvent) => { event.preventDefault(); isLastElementFocused.value = false; if (!props.disableArrowDownOpeningMenu) { openMenu(); } }; const onKeyUp = (event: KeyboardEvent) => { event.preventDefault(); isLastElementFocused.value = true; openMenu(); }; const menuItemClick = () => { emit('menu-item-click'); }; // Ignore the events inside the Button and the Popover useClickOutside( popoverRef, () => { closeMenu(); emit('click-outside'); }, { ...props.closeOnClickOutsideOptions, ignore: [menuButtonRef, ...(props.closeOnClickOutsideOptions?.ignore ?? [])], } ); defineExpose({ /** * Control to enable programmatic closing of the menu */ closeMenu, }); onMounted(() => { if (hasSlot('iconRight')) { printDevelopmentWarning( '"bento-menu" no longer supports the "iconRight" slot. The right-aligned icon is now fixed by design.' ); } }); </script> <script lang="ts"> /** * Menu component to render a series of buttons. Csan render nested submenus. * * @example * import { BentoMenu } from '@adyen/bento-vue2'; * * export default { * components: { BentoMenu }, * template: ` * <bento-menu * :data=[ * { text: 'Menu item text', handler: () => { some code here } }, * { text: 'Another menu item text', handler: () => { some other code here }, critical: true } * ] * /> * } */ export default { i18n: { messages }, name: 'bento-menu', }; </script> <style lang="scss" scoped src="./menu.scss" />
1
+ <template> <div :class="computedClasses" class="b-menu" data-testid="menu" @keydown.down="onKeyDown" @keydown.up="onKeyUp" @keydown.esc="closeMenu()" > <bento-toggle-button ref="menuButtonRef" class="b-menu__toggle-button" aria-haspopup="true" v-bind="computedButtonPropsWithDefaults" :aria-label="computedMenuButtonAriaLabel(hasSlot('default'))" :aria-controls="showMenu ? menuId : null" :toggled="showMenu" :inverse="isInverseColorSet" @click="toggleShowMenu" > <template v-if="!hasSlot('default') || hasSlot('iconLeft')" #iconLeft> <slot name="iconLeft"> <options-vertical-icon aria-hidden="true" /> </slot> </template> <template v-if="hasSlot('default')" #default> <slot /> </template> <template v-if="hasSlot('default')" #iconRight> <chevron-down-icon /> </template> </bento-toggle-button> <bento-teleport v-if="menuButtonRef" :disabled="!teleport"> <bento-popover v-if="menuButtonRef" :id="menuId" ref="popoverRef" :role="null" :open="showMenu" class="b-menu__popover" :style="computedMenuWidth" :target-element="menuButtonRef" :position="menuPosition" :fixed-positioning="menuFixedPositioning" :fallback-position="[ 'bottom-start', 'bottom-end', 'top-start', 'top-end', 'right-start', 'right-end', 'left-start', 'left-end', ]" :teleport="teleport" without-space > <menu-item-list ref="menuItemListRef" :close-menu-on-item-select="closeMenuOnItemSelect" :data="data" :focus-last-element="isLastElementFocused" :popover-width="menuWidth" @close="closeMenu" @menu-item-click="menuItemClick" /> </bento-popover> </bento-teleport> </div> </template> <script setup lang="ts"> import BentoToggleButton from '@/components/button/components/toggle-button/toggle-button.vue'; import { DIVIDER_INVERSE_INJECTION_KEY } from '@/components/divider/divider.keys'; import BentoPopover from '@/components/popover/popover.vue'; import { useClickOutside, useHasSlot } from '@/composables'; import { generateUid } from '@/core/utils/ts'; import { BentoTeleport } from '@/internal/teleport'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import OptionsVerticalIcon from '@adyen/ui-assets-icons-16/vue/options-vertical'; import { computed, inject, nextTick, onMounted, provide, ref, useSlots } from 'vue'; import MenuItemList from './components/menu-item-list/menu-item-list.vue'; import { MENU_INVERSE_INJECTION_KEY } from './menu.keys'; import { clampMenuWidth } from './composables/use-dynamic-menu-size/use-dynamic-menu-size'; import { type BentoMenuButtonProps, BentoMenuEvent, type BentoMenuProps } from './menu.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoMenuProps>(), { button: () => ({ variant: 'secondary', }) as BentoMenuButtonProps, closeMenuOnItemSelect: true, disableArrowDownOpeningMenu: false, menuPosition: 'bottom-start', menuFixedPositioning: false, menuWidth: null, revertFocusToButton: true, teleport: false, closeOnClickOutsideOptions: undefined, }); const emit = defineEmits<{ (e: BentoMenuEvent.CLOSE): void; (e: BentoMenuEvent.OPEN): void; (e: 'menu-item-click'): void; (e: 'click-outside'): void; }>(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const menuButtonRef = ref(null); const menuItemListRef = ref(null); const showMenu = ref(false); const isLastElementFocused = ref(false); const popoverRef = ref<HTMLElement>(null); const menuId = generateUid('menu'); const isInverseColorSet = inject(MENU_INVERSE_INJECTION_KEY, ref(false)); provide(DIVIDER_INVERSE_INJECTION_KEY, isInverseColorSet); const computedClasses = computed(() => ({ 'b-menu--inverse': isInverseColorSet.value, })); const computedMenuWidth = computed(() => props.menuWidth ? { width: `${clampMenuWidth(props.menuWidth)}px` } : null ); const computedMenuButtonAriaLabel = (hasDefaultSlot: boolean) => { if (props.button?.['aria-label']) { return props.button?.['aria-label']; } if (!hasDefaultSlot) { return t('menuButton'); } return undefined; }; const computedButtonPropsWithDefaults = computed<BentoMenuButtonProps>(() => { if (!props?.button) { return undefined; } return { variant: 'secondary', ...props.button, }; }); const openMenu = () => { showMenu.value = true; emit(BentoMenuEvent.OPEN); }; const closeMenu = async (revertFocus = props.revertFocusToButton) => { if (!showMenu.value) { return; } showMenu.value = false; if (revertFocus) { // ensure that menu is closed and re-rendering is completed before focusing await nextTick(); menuButtonRef.value?.$el?.focus(); } emit(BentoMenuEvent.CLOSE); }; const toggleShowMenu = (e: MouseEvent) => { e.stopPropagation(); if (!showMenu.value) { openMenu(); } else { closeMenu(); } }; const onKeyDown = (event: KeyboardEvent) => { event.preventDefault(); isLastElementFocused.value = false; if (!props.disableArrowDownOpeningMenu) { openMenu(); } }; const onKeyUp = (event: KeyboardEvent) => { event.preventDefault(); isLastElementFocused.value = true; openMenu(); }; const menuItemClick = () => { emit('menu-item-click'); }; // Ignore the events inside the Button and the Popover useClickOutside( popoverRef, () => { closeMenu(); emit('click-outside'); }, { ...props.closeOnClickOutsideOptions, ignore: [menuButtonRef, ...(props.closeOnClickOutsideOptions?.ignore ?? [])], } ); defineExpose({ /** * Control to enable programmatic closing of the menu */ closeMenu, }); onMounted(() => { if (hasSlot('iconRight')) { printDevelopmentWarning( '"bento-menu" no longer supports the "iconRight" slot. The right-aligned icon is now fixed by design.' ); } }); </script> <script lang="ts"> /** * Menu component to render a series of buttons. Csan render nested submenus. * * @example * import { BentoMenu } from '@adyen/bento-vue2'; * * export default { * components: { BentoMenu }, * template: ` * <bento-menu * :data=[ * { text: 'Menu item text', handler: () => { some code here } }, * { text: 'Another menu item text', handler: () => { some other code here }, critical: true } * ] * /> * } */ export default { i18n: { messages }, name: 'bento-menu', }; </script> <style lang="scss" scoped src="./menu.scss" />
@@ -1 +1 @@
1
- import { PopperContainerPosition } from '../popper-container/popper-container.types'; import type { Placement } from '@floating-ui/vue'; import type { BentoButtonActionsLayout, BentoButtonActionsList, } from '../button/components/button-actions/button-actions.types'; import { type VueNodeElement } from '@/types/elements'; import type { Options as FocusTrapOptions } from 'focus-trap/index'; /** * @deprecated Since v2.0.0. Use string literal instead. */ export const BentoPopoverPositions = { ...PopperContainerPosition }; export type BentoPopoverPositions = PopperContainerPosition; export interface BentoPopoverProps { /** * List of actions that will be used to render the buttons. * First element in the list will be rendered as "primary". * All elements after the first will be rendered as "secondary". * * Each object in the list should have a `title`, an `event` and * (optional) a Vue `icon` from the library `@adyen/ui-assets-icons-16`. * * `title`: Button's text. * * `event`: Event triggered when clicking the Button. * * `icon`: Vue Icon from the library `@adyen/ui-assets-icons-16`. * * ``` * [{ * title: 'Primary', * event: () => { ... }, * icon: FourPeopleIcon * }, { * title: 'Secondary', * event: () => { ... } * }] * ``` */ actions?: BentoButtonActionsList; /** * Defines the layout in which the button actions is presented. * It can be one of: * * @values BUTTONS_END * @values FILL_CONTAINER * @values SPACE_BETWEEN * @values VERTICAL_STACK */ actionsLayout?: BentoButtonActionsLayout | `${BentoButtonActionsLayout}`; /** * Defines a string value that provides an accessible name for the popover. * @deprecated Since v2.0.0. Use `aria-label` instead. */ ariaLabel?: string; /** * Adds a divider line to the actions section, reduces the top/bottom paddings and removes all padding in the "default" slot. */ divider?: boolean; /** * Supports disabling focus trap functionality to avoid scroll into view behavior when we don't need it. * */ disableFocusTrap?: boolean; /** * Adds a "dismiss" icon that triggers the "dismiss" event. Requires the `title` prop to be set. */ dismissible?: boolean; /** * Allows to configure to fallback position to the one specified, it could be ['right', 'top', ...]. * Useful when not using auto as starting placement. * https://floating-ui.com/docs/flip#fallbackplacements */ fallbackPosition?: Array<Placement>; /** * Sets the size of the container to fit its content */ fitContent?: boolean; /** * Instructs popover to be visible outside of the containing container. * Useful when component would be used in limited containers like Modal, Side panel, etc. * @see {@link https://floating-ui.com/docs/usefloating#strategy} */ fixedPositioning?: boolean; /** * Toggles the popover open and closed */ open?: boolean; /** * Displays the popover in a large size. */ large?: boolean; /** * @deprecated Since v2.0.0. Do not use. This property no longer functions since moving to the floating-ui framework. */ modifiers?: Array<unknown>; /** * Popover's position relative to the target element. * Possible values are: * 'auto', 'auto-start', 'auto-end', * 'top', 'top-start', 'top-end' * 'right', 'right-start', 'right-end' * 'bottom', 'bottom-start', 'bottom-end' * 'left', 'left-start', 'left-end' */ position?: BentoPopoverPositions | `${BentoPopoverPositions}`; /** * Displays the popover in a small size. */ small?: boolean; /** * Indicates the title you want to give the popover. */ title?: string; /** * Popover's target element. * The popover will position itself relatively to target element, in specified position. */ targetElement: VueNodeElement; /** * Activates focus trap for both arrow and tab keys. */ trapAll?: boolean; /** * Configurations for the focus-trap when trap-all is active. */ trapAllOptions?: FocusTrapOptions; /** * Displays the popover without padding. */ withoutSpace?: boolean; /** * Allows content to appear outside of the popover. Used when there are nested popovers. */ overflowVisible?: boolean; }
1
+ import { PopperContainerPosition } from '../popper-container/popper-container.types'; import type { Placement } from '@floating-ui/vue'; import type { BentoButtonActionsLayout, BentoButtonActionsList, } from '../button/components/button-actions/button-actions.types'; import { type VueNodeElement } from '@/types/elements'; import type { FallbackStrategy } from '../popper-container/popper-container.types'; import type { Options as FocusTrapOptions } from 'focus-trap/index'; /** * @deprecated Since v2.0.0. Use string literal instead. */ export const BentoPopoverPositions = { ...PopperContainerPosition }; export type BentoPopoverPositions = PopperContainerPosition; export type BentoPopoverFallbackStrategy = FallbackStrategy; export interface BentoPopoverProps { /** * List of actions that will be used to render the buttons. * First element in the list will be rendered as "primary". * All elements after the first will be rendered as "secondary". * * Each object in the list should have a `title`, an `event` and * (optional) a Vue `icon` from the library `@adyen/ui-assets-icons-16`. * * `title`: Button's text. * * `event`: Event triggered when clicking the Button. * * `icon`: Vue Icon from the library `@adyen/ui-assets-icons-16`. * * ``` * [{ * title: 'Primary', * event: () => { ... }, * icon: FourPeopleIcon * }, { * title: 'Secondary', * event: () => { ... } * }] * ``` */ actions?: BentoButtonActionsList; /** * Defines the layout in which the button actions is presented. * It can be one of: * * @values BUTTONS_END * @values FILL_CONTAINER * @values SPACE_BETWEEN * @values VERTICAL_STACK */ actionsLayout?: BentoButtonActionsLayout | `${BentoButtonActionsLayout}`; /** * Defines a string value that provides an accessible name for the popover. * @deprecated Since v2.0.0. Use `aria-label` instead. */ ariaLabel?: string; /** * Adds a divider line to the actions section, reduces the top/bottom paddings and removes all padding in the "default" slot. */ divider?: boolean; /** * Supports disabling focus trap functionality to avoid scroll into view behavior when we don't need it. * */ disableFocusTrap?: boolean; /** * Adds a "dismiss" icon that triggers the "dismiss" event. Requires the `title` prop to be set. */ dismissible?: boolean; /** * Allows to configure to fallback position to the one specified, it could be ['right', 'top', ...]. * Useful when not using auto as starting placement. * https://floating-ui.com/docs/flip#fallbackplacements */ fallbackPosition?: Array<Placement>; /** * Determines whether to keep the initial position or use the best fitting position when no fallback position fits. * @see {@link https://floating-ui.com/docs/flip#fallbackstrategy} */ fallbackStrategy?: BentoPopoverFallbackStrategy; /** * Sets the size of the container to fit its content */ fitContent?: boolean; /** * Instructs popover to be visible outside of the containing container. * Useful when component would be used in limited containers like Modal, Side panel, etc. * @see {@link https://floating-ui.com/docs/usefloating#strategy} */ fixedPositioning?: boolean; /** * Toggles the popover open and closed */ open?: boolean; /** * Displays the popover in a large size. */ large?: boolean; /** * @deprecated Since v2.0.0. Do not use. This property no longer functions since moving to the floating-ui framework. */ modifiers?: Array<unknown>; /** * Popover's position relative to the target element. * Possible values are: * 'auto', 'auto-start', 'auto-end', * 'top', 'top-start', 'top-end' * 'right', 'right-start', 'right-end' * 'bottom', 'bottom-start', 'bottom-end' * 'left', 'left-start', 'left-end' */ position?: BentoPopoverPositions | `${BentoPopoverPositions}`; /** * Displays the popover in a small size. */ small?: boolean; /** * Indicates the title you want to give the popover. */ title?: string; /** * Popover's target element. * The popover will position itself relatively to target element, in specified position. */ targetElement: VueNodeElement; /** * Activates focus trap for both arrow and tab keys. */ trapAll?: boolean; /** * Configurations for the focus-trap when trap-all is active. */ trapAllOptions?: FocusTrapOptions; /** * Displays the popover without padding. */ withoutSpace?: boolean; /** * Allows content to appear outside of the popover. Used when there are nested popovers. */ overflowVisible?: boolean; }
@@ -1 +1 @@
1
- <template> <Transition name="b-popover__animation"> <popper-container v-if="open" :id="computedAttributes['id']" :aria-role="computedAttributes.role" :aria-label="computedAriaLabel" :arrow="{ classes: ['b-popover__arrow'] }" :disabled-focus-trap="disableFocusTrap" :trap-all="trapAll" :trap-all-options="trapAllOptions" :class="conditionalClasses" :fallback-position="fallbackPosition" :fixed-positioning="fixedPositioning" :offset="[0, offset]" :position="position" :target-element="targetElement" class="b-popover" @placement="$emit('placement', $event)" > <div v-if="hasSlot('image')" class="b-popover__image"> <div v-if="dismissible" class="b-popover__image-dismiss-button"> <bento-popover-dismiss-button on-image @click="dismissPopover" /> </div> <slot name="image"></slot> </div> <div v-if="title" class="b-popover__header"> <div class="b-popover__header-title"> <slot v-if="hasSlot('headerIcon')" name="headerIcon"></slot> <bento-popover-title :title="title" :is-image-title="!!hasSlot('image')" /> </div> <div v-if="dismissible && !hasSlot('image')"> <bento-popover-dismiss-button @click="dismissPopover" /> </div> </div> <div v-if="hasSlot('default')" class="b-popover__content" :class="contentConditionalClasses"> <slot></slot> </div> <div v-if="actions" class="b-popover__footer"> <bento-button-actions :actions="actions" :layout="actionsLayout"></bento-button-actions> </div> </popper-container> </Transition> </template> <script setup lang="ts"> import { computed, inject, ref, toRefs, useAttrs, useSlots } from 'vue'; import BentoPopoverDismissButton from './components/popover-dismiss-button/popover-dismiss-button.vue'; import BentoPopoverTitle from './components/popover-title/popover-title.vue'; import BentoButtonActions from '@/components/button/components/button-actions/button-actions.vue'; import { PopperContainer, PopperContainerAriaRole } from '@/components/popper-container'; import { useHasSlot } from '@/composables/use-has-slot/use-has-slot'; import { INPUT_FILTER_POPOVER_INJECTION_KEY } from '@/components/filter-bar/components/input-filter/input-filter.keys'; import { INPUT_FILTER_WITH_DROPDOWN_POPOVER_INJECTION_KEY } from '@/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.keys'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { POPOVER_OFFSET_INJECTION_KEY } from './popover.keys'; import type { Placement } from '@floating-ui/vue'; import type { BentoPopoverProps } from './popover.types'; const DEFAULT_OFFSET = 2; const props = withDefaults(defineProps<BentoPopoverProps>(), { actions: undefined, actionsLayout: 'buttons-end', ariaLabel: undefined, divider: false, disableFocusTrap: false, dismissible: false, fallbackPosition: () => ['bottom-start', 'right-start', 'top-start', 'left-start'], fitContent: false, fixedPositioning: false, open: false, large: false, modifiers: undefined, position: 'bottom-start', small: false, title: undefined, trapAll: false, trapAllOptions: undefined, withoutSpace: false, overflowVisible: false, }); const emit = defineEmits<{ /** * Triggered when the `dismiss` button is clicked */ (e: 'dismiss'): void; /** * Triggered when the popover `placement` changes */ (e: 'placement', placement: Placement): void; }>(); const slots = useSlots(); const attrs = useAttrs(); const computedAttributes = computed(() => ({ id: attrs.id?.toString() as string, role: attrs?.['role'] === null ? undefined : PopperContainerAriaRole.POPOVER, })); const offset = inject<number>(POPOVER_OFFSET_INJECTION_KEY, DEFAULT_OFFSET); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel, title } = toRefs(props); const computedAriaLabel = useAriaLabel({ ariaLabel: ariaLabelAttribute.value as string, // TODO: take this line away when the aria-label props is removed label: ariaLabel, defaultFallback: title, }); const isOverflowVisible = inject(INPUT_FILTER_WITH_DROPDOWN_POPOVER_INJECTION_KEY, false) || inject(INPUT_FILTER_POPOVER_INJECTION_KEY, false); const conditionalClasses = computed(() => ({ 'b-popover--small': props.small && !props.large, 'b-popover--with-divider': !!props.divider, 'b-popover--large': props.large && !props.small, 'b-popover--fit-content': props.fitContent, 'b-popover--without-space': props.withoutSpace, 'b-popover--overflow-visible': props.overflowVisible || isOverflowVisible, })); const contentConditionalClasses = computed(() => ({ 'b-popover__content--with-title': !!props.title, })); const dismissPopover = () => emit('dismiss'); const hasSlot = useHasSlot(slots); </script> <script lang="ts"> /** * Example description of the component... * * @example * import { BentoPopover } from '@adyen/adl-vue2'; * * export default { * components: { BentoPopover }, * template: ` * <bento-popover id="your-popover-id"> * ...Example code of the component * </bento-popover> * ` * } */ export default {}; </script> <style lang="scss" scoped src="./popover.scss" />
1
+ <template> <Transition name="b-popover__animation"> <popper-container v-if="open" :id="computedAttributes['id']" :aria-role="computedAttributes.role" :aria-label="computedAriaLabel" :arrow="{ classes: ['b-popover__arrow'] }" :disabled-focus-trap="disableFocusTrap" :trap-all="trapAll" :trap-all-options="trapAllOptions" :class="conditionalClasses" :fallback-position="fallbackPosition" :fallback-strategy="fallbackStrategy" :fixed-positioning="fixedPositioning" :offset="[0, offset]" :position="position" :target-element="targetElement" class="b-popover" @placement="$emit('placement', $event)" > <div v-if="hasSlot('image')" class="b-popover__image"> <div v-if="dismissible" class="b-popover__image-dismiss-button"> <bento-popover-dismiss-button on-image @click="dismissPopover" /> </div> <slot name="image"></slot> </div> <div v-if="title" class="b-popover__header"> <div class="b-popover__header-title"> <slot v-if="hasSlot('headerIcon')" name="headerIcon"></slot> <bento-popover-title :title="title" :is-image-title="!!hasSlot('image')" /> </div> <div v-if="dismissible && !hasSlot('image')"> <bento-popover-dismiss-button @click="dismissPopover" /> </div> </div> <div v-if="hasSlot('default')" class="b-popover__content" :class="contentConditionalClasses"> <slot></slot> </div> <div v-if="actions" class="b-popover__footer"> <bento-button-actions :actions="actions" :layout="actionsLayout"></bento-button-actions> </div> </popper-container> </Transition> </template> <script setup lang="ts"> import { computed, inject, ref, toRefs, useAttrs, useSlots } from 'vue'; import BentoPopoverDismissButton from './components/popover-dismiss-button/popover-dismiss-button.vue'; import BentoPopoverTitle from './components/popover-title/popover-title.vue'; import BentoButtonActions from '@/components/button/components/button-actions/button-actions.vue'; import { PopperContainer, PopperContainerAriaRole } from '@/components/popper-container'; import { useHasSlot } from '@/composables/use-has-slot/use-has-slot'; import { INPUT_FILTER_POPOVER_INJECTION_KEY } from '@/components/filter-bar/components/input-filter/input-filter.keys'; import { INPUT_FILTER_WITH_DROPDOWN_POPOVER_INJECTION_KEY } from '@/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.keys'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { POPOVER_OFFSET_INJECTION_KEY } from './popover.keys'; import type { Placement } from '@floating-ui/vue'; import type { BentoPopoverProps } from './popover.types'; const DEFAULT_OFFSET = 2; const props = withDefaults(defineProps<BentoPopoverProps>(), { actions: undefined, actionsLayout: 'buttons-end', ariaLabel: undefined, divider: false, disableFocusTrap: false, dismissible: false, fallbackPosition: () => ['bottom-start', 'right-start', 'top-start', 'left-start'], fallbackStrategy: undefined, fitContent: false, fixedPositioning: false, open: false, large: false, modifiers: undefined, position: 'bottom-start', small: false, title: undefined, trapAll: false, trapAllOptions: undefined, withoutSpace: false, overflowVisible: false, }); const emit = defineEmits<{ /** * Triggered when the `dismiss` button is clicked */ (e: 'dismiss'): void; /** * Triggered when the popover `placement` changes */ (e: 'placement', placement: Placement): void; }>(); const slots = useSlots(); const attrs = useAttrs(); const computedAttributes = computed(() => ({ id: attrs.id?.toString() as string, role: attrs?.['role'] === null ? undefined : PopperContainerAriaRole.POPOVER, })); const offset = inject<number>(POPOVER_OFFSET_INJECTION_KEY, DEFAULT_OFFSET); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel, title } = toRefs(props); const computedAriaLabel = useAriaLabel({ ariaLabel: ariaLabelAttribute.value as string, // TODO: take this line away when the aria-label props is removed label: ariaLabel, defaultFallback: title, }); const isOverflowVisible = inject(INPUT_FILTER_WITH_DROPDOWN_POPOVER_INJECTION_KEY, false) || inject(INPUT_FILTER_POPOVER_INJECTION_KEY, false); const conditionalClasses = computed(() => ({ 'b-popover--small': props.small && !props.large, 'b-popover--with-divider': !!props.divider, 'b-popover--large': props.large && !props.small, 'b-popover--fit-content': props.fitContent, 'b-popover--without-space': props.withoutSpace, 'b-popover--overflow-visible': props.overflowVisible || isOverflowVisible, })); const contentConditionalClasses = computed(() => ({ 'b-popover__content--with-title': !!props.title, })); const dismissPopover = () => emit('dismiss'); const hasSlot = useHasSlot(slots); </script> <script lang="ts"> /** * Example description of the component... * * @example * import { BentoPopover } from '@adyen/adl-vue2'; * * export default { * components: { BentoPopover }, * template: ` * <bento-popover id="your-popover-id"> * ...Example code of the component * </bento-popover> * ` * } */ export default {}; </script> <style lang="scss" scoped src="./popover.scss" />
@@ -1 +1 @@
1
- export interface PopperContainerArrowProp { classes?: Array<string>; padding?: string; } export enum PopperContainerPositionBasic { TOP = 'top', RIGHT = 'right', BOTTOM = 'bottom', LEFT = 'left', } export enum PopperContainerPositionExtended { AUTO = 'auto', AUTO_START = 'auto-start', AUTO_END = 'auto-end', TOP_START = 'top-start', TOP_END = 'top-end', RIGHT_START = 'right-start', RIGHT_END = 'right-end', BOTTOM_START = 'bottom-start', BOTTOM_END = 'bottom-end', LEFT_START = 'left-start', LEFT_END = 'left-end', } // TODO: add more roles as we figure them out export enum PopperContainerAriaRole { TOOLTIP = 'tooltip', POPOVER = 'dialog', } /** * @deprecated Since v2.0.0. Use string literal instead. */ export const PopperContainerPosition = { ...PopperContainerPositionBasic, ...PopperContainerPositionExtended }; export type PopperContainerPosition = PopperContainerPositionBasic | PopperContainerPositionExtended; export type FallbackPositionProp = Array<`${PopperContainerPosition}`> | `${PopperContainerPosition}`;
1
+ import type { FlipOptions } from '@floating-ui/core'; export interface PopperContainerArrowProp { classes?: Array<string>; padding?: string; } export enum PopperContainerPositionBasic { TOP = 'top', RIGHT = 'right', BOTTOM = 'bottom', LEFT = 'left', } export enum PopperContainerPositionExtended { AUTO = 'auto', AUTO_START = 'auto-start', AUTO_END = 'auto-end', TOP_START = 'top-start', TOP_END = 'top-end', RIGHT_START = 'right-start', RIGHT_END = 'right-end', BOTTOM_START = 'bottom-start', BOTTOM_END = 'bottom-end', LEFT_START = 'left-start', LEFT_END = 'left-end', } // TODO: add more roles as we figure them out export enum PopperContainerAriaRole { TOOLTIP = 'tooltip', POPOVER = 'dialog', } /** * @deprecated Since v2.0.0. Use string literal instead. */ export const PopperContainerPosition = { ...PopperContainerPositionBasic, ...PopperContainerPositionExtended }; export type PopperContainerPosition = PopperContainerPositionBasic | PopperContainerPositionExtended; export type FallbackPositionProp = Array<`${PopperContainerPosition}`> | `${PopperContainerPosition}`; export type FallbackStrategy = FlipOptions['fallbackStrategy'];
@@ -1 +1 @@
1
- <template> <bento-focus-trap v-bind="$attrs" :id="id" ref="popperContainerRef" class="b-popper-container" :data-popper-placement="placement" :disabled="disabledFocusTrap" :style="floatingStyles" :role="ariaRole" :trap-all="trapAll" :trap-all-options="trapAllOptions" > <slot></slot> <span v-if="props.arrow" v-bind="arrowAttributes" ref="floatingArrow" class="b-popper-container__arrow" data-testid="arrow" ></span> </bento-focus-trap> </template> <script setup lang="ts"> import type {} from '@floating-ui/core'; import { computed, type PropType, type Ref, ref, toRefs, watch } from 'vue'; import { arrow as arrowMiddleware, autoPlacement, autoUpdate, flip, // eslint-disable-next-line vue/no-dupe-keys offset, shift, useFloating, } from '@floating-ui/vue'; import { BentoFocusTrap } from '@/components/focus-trap'; import { type FallbackPositionProp, PopperContainerAriaRole, type PopperContainerArrowProp, PopperContainerPosition, type PopperContainerPositionBasic, } from './popper-container.types'; import type { VueNodeElement } from '@/types/elements'; import type { AutoPlacementOptions, Middleware, Placement, Strategy } from '@floating-ui/vue'; import { getFlipModifier, getOffsetModifier, sameWidthMiddleware } from './popper-container.utils'; import type { CSSProperties } from 'vue/types/jsx.d.ts'; import { type Options as FocusTrapOptions } from 'focus-trap'; const props = defineProps({ /** * The aria role assigned to the container. Defaults to null, which means it's not set if nothing is passed. */ ariaRole: { type: String as PropType<PopperContainerAriaRole>, default: null, validator: value => Object.values(PopperContainerAriaRole).includes(value as PopperContainerAriaRole), }, /** * Indicates if container should have additional arrow, pointing at target element * Can be just a boolean flag, or an object with additional configuration: * @property {Array<string>} arrow.classes – apply any additional classes to the arrow element. * @property {string} arrow.padding – useful when content has rounded corners, @see {@link https://floating-ui.com/docs/arrow#padding} */ arrow: { type: Object as PropType<PopperContainerArrowProp | boolean>, default: undefined }, /** * Disables wrapping focus-trap component. * Mostly useful for the storybook demo. */ disabledFocusTrap: { type: Boolean, default: false }, /** * Allows to configure to fallback position to the one specified, it could be 'auto' or ['right', 'top', ...]. * Useful when not using auto as starting placement. * @see {@link https://floating-ui.com/docs/flip#fallbackplacements} */ fallbackPosition: { type: [Array, String] as PropType<FallbackPositionProp>, default: null, }, /** * Instructs popover to be visible outside of the containing container. * Useful when component would be used in limited containers like Modal, Side panel, etc. * @see {@link https://floating-ui.com/docs/usefloating#strategy} */ fixedPositioning: { type: Boolean, default: false }, /** * The id to be set on the component. Use it for accessibility purposes. */ id: { type: String, default: null }, /** * If hiding / showing popper element then use this property to tell the popper when to update it's position. * This will drastically help improve performance, as we will not need to track hidden elements. */ isHidden: { type: Boolean, default: null }, /** * Instructs popper container to have the same width as target element. */ matchWidth: { type: Boolean, default: false }, /** * Custom set of middleware. Useful for low-level control over Popper behavior in some really complex use cases. * @see {@link https://floating-ui.com/docs/middleware} */ middleware: { type: Array as PropType<Array<Middleware>>, default: null }, /** * Allows to displace a popper container from its target element. * @see {@link https://floating-ui.com/docs/offset#options} */ offset: { type: Array as PropType<Array<number>>, default: () => [0, 0], validator: (value: Array<number>) => value?.length === 2, }, /** * Position relative to the target element * @see {@link https://floating-ui.com/docs/computeposition#placement} */ position: { type: String as PropType<PopperContainerPosition | `${PopperContainerPosition}`>, default: PopperContainerPosition.AUTO, validator: value => Object.values(PopperContainerPosition).includes(value as PopperContainerPositionBasic), }, /** * Reference to the DOM element or Vue component for positioning */ targetElement: { type: [Element, Object] as PropType<VueNodeElement>, required: true }, /** * Activates focus trap for both arrow and tab keys. */ trapAll: { type: Boolean, default: false }, /** * Configurations for the focus-trap when trap-all is active. */ trapAllOptions: { type: Object as PropType<FocusTrapOptions>, default: undefined, }, }); const emit = defineEmits<{ /** * Emits new placement of popper container when it's placement is updated. */ (e: 'placement', value: Placement): void; }>(); // Popper / floating ui logic const { position } = toRefs(props); const popperContainerRef = ref(); const floatingArrow = ref(null); const emittedPosition = ref(props.position); const targetElement = computed(() => (props.targetElement as any).$el ? ((props.targetElement as any).$el as Element) : (props.targetElement as Element) ); const popperContainerElement = computed(() => (popperContainerRef.value as any).$el ? ((popperContainerRef.value as any).$el as HTMLElement) : (popperContainerRef.value as HTMLElement) ); const isFlipMiddleware = computed(() => props?.position !== 'auto' && props?.fallbackPosition !== 'auto'); const getAutoPlacementProps = computed<AutoPlacementOptions>(() => { switch (props.position) { case 'auto-start': return { alignment: 'start', crossAxis: true }; case 'auto-end': return { alignment: 'end', crossAxis: true }; default: return undefined; } }); const strategy = computed<Strategy>(() => (props.fixedPositioning ? 'fixed' : 'absolute')); const { floatingStyles, middlewareData, placement, update } = useFloating(targetElement, popperContainerRef, { placement: position as Ref<Placement>, middleware: [ // Adds flip modifier if fallbackPosition is not set to auto ...(isFlipMiddleware.value ? [ flip({ fallbackPlacements: getFlipModifier(props.fallbackPosition).fallbackPlacements, }), ] : []), // Adds autoposition middlelare if position is set to any auto variants ...(!isFlipMiddleware.value ? [autoPlacement(getAutoPlacementProps.value)] : []), // Add shifting middle in case popper is still not fully visible in view port shift(), // Adds offset middlelare if offset property is set ...(props.offset ? [offset(getOffsetModifier(props.arrow, props.offset))] : []), // Adds arrow middlelare if arrow property is set, can set additional padding property ...(props.arrow ? [arrowMiddleware({ element: floatingArrow, padding: props.arrow.padding || 0 })] : []), // Adds same width middleware to keep popper width same as referenced element's width ...(props.matchWidth ? [sameWidthMiddleware] : []), // If there is additional custom middleware to add via the property then add it ...(props.middleware?.length > 0 ? props.middleware : []), ], strategy, // If we're mounting the element with a `v-if` then we need to auto update it while it's mounted ...(props.isHidden === null ? { whileElementsMounted: autoUpdate } : []), }); // If we're hiding the floating element, then we need to manually update once it's visible let cleanupAutoUpdate: undefined | (() => void); watch( () => props.isHidden, () => { if (props.isHidden !== null && !props.isHidden) { cleanupAutoUpdate = autoUpdate(targetElement?.value, popperContainerElement?.value, update); } else if (props.isHidden !== null && props.isHidden && typeof cleanupAutoUpdate === 'function') { cleanupAutoUpdate(); } } ); // Watch placement for changes, if changed then emit new placemenet watch( () => placement.value, () => { if (emittedPosition.value !== placement.value) { emittedPosition.value = placement.value; emit('placement', emittedPosition.value); } } ); // Arrow attributes and styles const arrowStyles = computed<CSSProperties>(() => ({ position: 'absolute', left: middlewareData.value.arrow?.x != null ? `${middlewareData.value.arrow.x}px` : '', top: middlewareData.value.arrow?.y != null ? `${middlewareData.value.arrow.y}px` : '', })); const arrowAttributes = computed(() => ({ class: props.arrow.classes, style: arrowStyles.value, // Applies a data-hide attribute to arrow element depending on center offset. ...(middlewareData.value.arrow?.centerOffset !== 0 ? { 'data-hide': '' } : {}), })); </script> <script lang="ts"> import type {} from '@floating-ui/vue'; /** * A wrapper for the 'popper/core' package that overlays a small piece of content near a target element. * * NOTE: This component should not be used by itself but more implemented by other exported components * e.g. Tooltip * More info here on the package: https://floating-ui.com/docs/vue * * * @usage * import { PopperContainer } from '@adyen/bento-vue2'; * * export default { * components: { PopperContainer }, * computed: { * targetElement() { * return this.$refs.tooltipTarget; * }, * }, * template: ` * <popper-container * :target-element="targetElement" * :position="position" * > * {{ content }} * </popper-container>`, * } */ export default {}; </script> <style lang="scss" scoped src="./popper-container.scss" />
1
+ <template> <bento-focus-trap v-bind="$attrs" :id="id" ref="popperContainerRef" class="b-popper-container" :data-popper-placement="placement" :disabled="disabledFocusTrap" :style="floatingStyles" :role="ariaRole" :trap-all="trapAll" :trap-all-options="trapAllOptions" > <slot></slot> <span v-if="props.arrow" v-bind="arrowAttributes" ref="floatingArrow" class="b-popper-container__arrow" data-testid="arrow" ></span> </bento-focus-trap> </template> <script setup lang="ts"> import type {} from '@floating-ui/core'; import { computed, type PropType, type Ref, ref, toRefs, watch } from 'vue'; import { arrow as arrowMiddleware, autoPlacement, autoUpdate, flip, // eslint-disable-next-line vue/no-dupe-keys offset, shift, useFloating, } from '@floating-ui/vue'; import { BentoFocusTrap } from '@/components/focus-trap'; import { type FallbackPositionProp, type FallbackStrategy, PopperContainerAriaRole, type PopperContainerArrowProp, PopperContainerPosition, type PopperContainerPositionBasic, } from './popper-container.types'; import type { VueNodeElement } from '@/types/elements'; import type { AutoPlacementOptions, Middleware, Placement, Strategy } from '@floating-ui/vue'; import { getFlipModifier, getOffsetModifier, sameWidthMiddleware } from './popper-container.utils'; import type { CSSProperties } from 'vue/types/jsx.d.ts'; import { type Options as FocusTrapOptions } from 'focus-trap'; const props = defineProps({ /** * The aria role assigned to the container. Defaults to null, which means it's not set if nothing is passed. */ ariaRole: { type: String as PropType<PopperContainerAriaRole>, default: null, validator: value => Object.values(PopperContainerAriaRole).includes(value as PopperContainerAriaRole), }, /** * Indicates if container should have additional arrow, pointing at target element * Can be just a boolean flag, or an object with additional configuration: * @property {Array<string>} arrow.classes – apply any additional classes to the arrow element. * @property {string} arrow.padding – useful when content has rounded corners, @see {@link https://floating-ui.com/docs/arrow#padding} */ arrow: { type: Object as PropType<PopperContainerArrowProp | boolean>, default: undefined }, /** * Disables wrapping focus-trap component. * Mostly useful for the storybook demo. */ disabledFocusTrap: { type: Boolean, default: false }, /** * Allows to configure to fallback position to the one specified, it could be 'auto' or ['right', 'top', ...]. * Useful when not using auto as starting placement. * @see {@link https://floating-ui.com/docs/flip#fallbackplacements} */ fallbackPosition: { type: [Array, String] as PropType<FallbackPositionProp>, default: null, }, /** * Determines whether to keep the initial position or use the best fitting position when no fallback position fits. * @see {@link https://floating-ui.com/docs/flip#fallbackstrategy} */ fallbackStrategy: { type: String as PropType<FallbackStrategy>, default: undefined, }, /** * Instructs popover to be visible outside of the containing container. * Useful when component would be used in limited containers like Modal, Side panel, etc. * @see {@link https://floating-ui.com/docs/usefloating#strategy} */ fixedPositioning: { type: Boolean, default: false }, /** * The id to be set on the component. Use it for accessibility purposes. */ id: { type: String, default: null }, /** * If hiding / showing popper element then use this property to tell the popper when to update it's position. * This will drastically help improve performance, as we will not need to track hidden elements. */ isHidden: { type: Boolean, default: null }, /** * Instructs popper container to have the same width as target element. */ matchWidth: { type: Boolean, default: false }, /** * Custom set of middleware. Useful for low-level control over Popper behavior in some really complex use cases. * @see {@link https://floating-ui.com/docs/middleware} */ middleware: { type: Array as PropType<Array<Middleware>>, default: null }, /** * Allows to displace a popper container from its target element. * @see {@link https://floating-ui.com/docs/offset#options} */ offset: { type: Array as PropType<Array<number>>, default: () => [0, 0], validator: (value: Array<number>) => value?.length === 2, }, /** * Position relative to the target element * @see {@link https://floating-ui.com/docs/computeposition#placement} */ position: { type: String as PropType<PopperContainerPosition | `${PopperContainerPosition}`>, default: PopperContainerPosition.AUTO, validator: value => Object.values(PopperContainerPosition).includes(value as PopperContainerPositionBasic), }, /** * Reference to the DOM element or Vue component for positioning */ targetElement: { type: [Element, Object] as PropType<VueNodeElement>, required: true }, /** * Activates focus trap for both arrow and tab keys. */ trapAll: { type: Boolean, default: false }, /** * Configurations for the focus-trap when trap-all is active. */ trapAllOptions: { type: Object as PropType<FocusTrapOptions>, default: undefined, }, }); const emit = defineEmits<{ /** * Emits new placement of popper container when it's placement is updated. */ (e: 'placement', value: Placement): void; }>(); // Popper / floating ui logic const { position } = toRefs(props); const popperContainerRef = ref(); const floatingArrow = ref(null); const emittedPosition = ref(props.position); const targetElement = computed(() => (props.targetElement as any).$el ? ((props.targetElement as any).$el as Element) : (props.targetElement as Element) ); const popperContainerElement = computed(() => (popperContainerRef.value as any).$el ? ((popperContainerRef.value as any).$el as HTMLElement) : (popperContainerRef.value as HTMLElement) ); const isFlipMiddleware = computed(() => props?.position !== 'auto' && props?.fallbackPosition !== 'auto'); const getAutoPlacementProps = computed<AutoPlacementOptions>(() => { switch (props.position) { case 'auto-start': return { alignment: 'start', crossAxis: true }; case 'auto-end': return { alignment: 'end', crossAxis: true }; default: return undefined; } }); const strategy = computed<Strategy>(() => (props.fixedPositioning ? 'fixed' : 'absolute')); const { floatingStyles, middlewareData, placement, update } = useFloating(targetElement, popperContainerRef, { placement: position as Ref<Placement>, middleware: [ // Adds flip modifier if fallbackPosition is not set to auto ...(isFlipMiddleware.value ? [ flip({ fallbackPlacements: getFlipModifier(props.fallbackPosition).fallbackPlacements, fallbackStrategy: props.fallbackStrategy, }), ] : []), // Adds autoposition middlelare if position is set to any auto variants ...(!isFlipMiddleware.value ? [autoPlacement(getAutoPlacementProps.value)] : []), // Add shifting middle in case popper is still not fully visible in view port shift(), // Adds offset middlelare if offset property is set ...(props.offset ? [offset(getOffsetModifier(props.arrow, props.offset))] : []), // Adds arrow middlelare if arrow property is set, can set additional padding property ...(props.arrow ? [arrowMiddleware({ element: floatingArrow, padding: props.arrow.padding || 0 })] : []), // Adds same width middleware to keep popper width same as referenced element's width ...(props.matchWidth ? [sameWidthMiddleware] : []), // If there is additional custom middleware to add via the property then add it ...(props.middleware?.length > 0 ? props.middleware : []), ], strategy, // If we're mounting the element with a `v-if` then we need to auto update it while it's mounted ...(props.isHidden === null ? { whileElementsMounted: autoUpdate } : []), }); // If we're hiding the floating element, then we need to manually update once it's visible let cleanupAutoUpdate: undefined | (() => void); watch( () => props.isHidden, () => { if (props.isHidden !== null && !props.isHidden) { cleanupAutoUpdate = autoUpdate(targetElement?.value, popperContainerElement?.value, update); } else if (props.isHidden !== null && props.isHidden && typeof cleanupAutoUpdate === 'function') { cleanupAutoUpdate(); } } ); // Watch placement for changes, if changed then emit new placemenet watch( () => placement.value, () => { if (emittedPosition.value !== placement.value) { emittedPosition.value = placement.value; emit('placement', emittedPosition.value); } } ); // Arrow attributes and styles const arrowStyles = computed<CSSProperties>(() => ({ position: 'absolute', left: middlewareData.value.arrow?.x != null ? `${middlewareData.value.arrow.x}px` : '', top: middlewareData.value.arrow?.y != null ? `${middlewareData.value.arrow.y}px` : '', })); const arrowAttributes = computed(() => ({ class: props.arrow.classes, style: arrowStyles.value, // Applies a data-hide attribute to arrow element depending on center offset. ...(middlewareData.value.arrow?.centerOffset !== 0 ? { 'data-hide': '' } : {}), })); </script> <script lang="ts"> import type {} from '@floating-ui/vue'; /** * A wrapper for the 'popper/core' package that overlays a small piece of content near a target element. * * NOTE: This component should not be used by itself but more implemented by other exported components * e.g. Tooltip * More info here on the package: https://floating-ui.com/docs/vue * * * @usage * import { PopperContainer } from '@adyen/bento-vue2'; * * export default { * components: { PopperContainer }, * computed: { * targetElement() { * return this.$refs.tooltipTarget; * }, * }, * template: ` * <popper-container * :target-element="targetElement" * :position="position" * > * {{ content }} * </popper-container>`, * } */ export default {}; </script> <style lang="scss" scoped src="./popper-container.scss" />
@@ -0,0 +1,102 @@
1
+ import { Canvas, Meta, Story } from '@storybook/blocks';
2
+ import * as ProgressBarStories from './progress-bar.stories';
3
+
4
+ <Meta of={ProgressBarStories} />
5
+
6
+ # Progress Bar
7
+
8
+ A progress bar communicates the completion status of a task or process, such as a file upload or a multi-step flow.
9
+
10
+ <Canvas withSource="closed">
11
+ <Story of={ProgressBarStories.Default} />
12
+ </Canvas>
13
+
14
+ ## Use Cases
15
+
16
+ Use a progress bar to give feedback about the duration and progression of a process.
17
+
18
+ - Use a determinate progress bar when the completion percentage can be calculated.
19
+ - Use an indeterminate progress bar when the remaining wait time is unknown.
20
+
21
+ ## Value
22
+
23
+ The `value` prop sets the current progress as a percentage. Values are clamped between `0` and `100`. The current
24
+ percentage is shown to the right of the label; set `valueVisible` to `false` to hide it.
25
+
26
+ Use the `value` slot to display custom text (for example `3 of 5 files`) in place of the percentage. A `value` is still
27
+ required to fill the bar.
28
+
29
+ The value shown next to the label follows this priority: `valueVisible` (`false` hides it entirely), then `variant`
30
+ (`indeterminate` shows nothing), then the `value` slot, then the percentage. When `valueVisible` is `true` and neither a
31
+ `value` nor a `value` slot is provided, the bar falls back to `indeterminate`.
32
+
33
+ <Canvas withSource="closed">
34
+ <Story of={ProgressBarStories.CustomValue} />
35
+ </Canvas>
36
+
37
+ ## Variant
38
+
39
+ The `variant` prop controls how progress is represented:
40
+
41
+ - `determinate` (default) animates the bar to the current `value` and shows a gradient that moves across the fill.
42
+ - `indeterminate` shows a continuously animated bar for when progress cannot be measured.
43
+ - `static` animates from 0% to the current `value` when mounted. Set `staticAnimation` to `false` to render the value
44
+ immediately. It does not show the moving gradient, stop indicator, or success flash.
45
+
46
+ Set `shimmer`, `stopIndicator`, or `successFlash` to `false` to disable the corresponding determinate effect.
47
+
48
+ The moving gradient, indeterminate animation and static animation on load are paused for users who prefer reduced
49
+ motion.
50
+
51
+ ## Type
52
+
53
+ The `type` prop sets the color of the indicator to convey status: `default`, `highlight`, `success`, or `critical`.
54
+
55
+ ## Error
56
+
57
+ Set the `errorMessage` prop to render an error message below the bar and apply the error styling.
58
+
59
+ ## Height
60
+
61
+ The `height` prop adjusts the thickness of the bar: `default`, `small`, or `large`.
62
+
63
+ ## Direction
64
+
65
+ The `direction` prop sets the fill origin. Use `reverse` to fill the bar from the opposite end.
66
+
67
+ ## Label, description and tooltip
68
+
69
+ Set the `label` prop to render a visible label above the bar. Add supporting `description` text below the bar, and pass
70
+ `tooltipText` to show an info icon next to the label that reveals the message on hover or focus.
71
+
72
+ ## Accessibility
73
+
74
+ The progress bar exposes the `progressbar` role together with `aria-valuemin`, `aria-valuemax` and `aria-valuenow` so
75
+ assistive technologies can announce the current progress. The `aria-valuenow` attribute is omitted while the progress
76
+ bar is `indeterminate`. When a `label` is set it names the progress bar through `aria-labelledby`, while the
77
+ `description` and `errorMessage` are linked through `aria-describedby`.
78
+
79
+ ### Live announcements
80
+
81
+ Screen readers do not reliably announce `progressbar` value changes unless the bar is focused, so completion is surfaced
82
+ through a visually hidden polite live region (`role="status"`) when a `determinate` bar's value crosses into 100. It
83
+ announces a localised _"Complete"_ by default; use `completionAnnouncement` to provide a custom message (e.g. _"Upload
84
+ complete"_).
85
+
86
+ The `errorMessage` is rendered in its own polite live region, so failures are announced when they appear even when focus
87
+ is elsewhere.
88
+
89
+ ### Roles, states and properties
90
+
91
+ | ARIA attribute | is automatic? | fallback used | manual override |
92
+ | ------------------ | --------------------------------------------------------------------------------------------------- | -------------------- | ------------------------ |
93
+ | `role` | Yes, the `progressbar` role is set on the bar element. | - | - |
94
+ | `aria-valuenow` | Yes, it reflects the current value and is removed when the component is indeterminate. | - | - |
95
+ | `aria-labelledby` | Yes, it links the bar to the `label` when one is provided. | - | - |
96
+ | `aria-describedby` | Yes, it links the bar to the `description` and `errorMessage` when provided. | - | - |
97
+ | `role="status"` | Yes, a polite live region announces completion. Customise the message via `completionAnnouncement`. | Localised "Complete" | `completionAnnouncement` |
98
+ | `aria-live` | Yes, the `errorMessage` is announced through its own polite live region when it appears. | - | - |
99
+
100
+ ## Resources
101
+
102
+ - [WAI-ARIA progressbar role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/progressbar_role)
@@ -0,0 +1 @@
1
+ import BentoProgressBar from './progress-bar.vue'; import { BentoTypography } from '@/components/typography'; import type { Meta, StoryObj } from '@storybook/vue'; import { isVue2 } from 'vue-demi'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import ProgressBarDefaultExample from './__tests__/progress-bar-default-example.vue?raw'; import ProgressBarCustomValueExample from './__tests__/progress-bar-custom-value-example.vue?raw'; import { BentoProgressBarDirectionOptions, BentoProgressBarHeightOptions, BentoProgressBarTypeOptions, BentoProgressBarVariantOptions, } from './progress-bar.types'; const meta: Meta = { title: 'Progress Bar', component: BentoProgressBar, parameters: { layout: 'padded', }, argTypes: { value: { control: { type: 'range', min: 0, max: 100, step: 1 }, }, variant: { control: { type: 'select' }, options: BentoProgressBarVariantOptions, }, type: { control: { type: 'select' }, options: BentoProgressBarTypeOptions, }, height: { control: { type: 'select' }, options: BentoProgressBarHeightOptions, }, direction: { control: { type: 'select' }, options: BentoProgressBarDirectionOptions, }, }, }; export default meta; type Story = StoryObj<typeof BentoProgressBar>; const render: Story['render'] = (_args, { argTypes }) => ({ components: { BentoProgressBar }, props: Object.keys(argTypes), setup(props) { return { args: isVue2 ? props : _args, }; }, template: `<bento-progress-bar v-bind="args" />`, }); export const Default: Story = { render, args: { value: 40, staticAnimation: true, shimmer: true, stopIndicator: true, successFlash: true, valueVisible: true, label: 'Uploading files', description: 'This may take a few moments', tooltipText: 'Only PDF files are supported', }, parameters: storybookDocsParameter(ProgressBarDefaultExample), }; export const CustomValue: Story = { render: (_args, { argTypes }) => ({ components: { BentoProgressBar, BentoTypography }, props: Object.keys(argTypes), setup(props) { return { args: isVue2 ? props : _args, }; }, template: ` <bento-progress-bar v-bind="args"> <template #value> <bento-typography el="span" strongest>2.4 MB</bento-typography> <bento-typography el="span"> of 10 MB uploaded</bento-typography> </template> </bento-progress-bar> `, }), args: { value: 60, label: 'Uploading supporting documents', }, parameters: storybookDocsParameter(ProgressBarCustomValueExample), };
@@ -0,0 +1 @@
1
+ export const BentoProgressBarDirectionOptions = ['default', 'reverse'] as const; export const BentoProgressBarHeightOptions = ['default', 'small', 'large'] as const; export const BentoProgressBarTypeOptions = ['default', 'highlight', 'success', 'critical'] as const; export const BentoProgressBarVariantOptions = ['determinate', 'indeterminate', 'static'] as const; export type BentoProgressBarDirection = (typeof BentoProgressBarDirectionOptions)[number]; export type BentoProgressBarHeight = (typeof BentoProgressBarHeightOptions)[number]; export type BentoProgressBarType = (typeof BentoProgressBarTypeOptions)[number]; export type BentoProgressBarVariant = (typeof BentoProgressBarVariantOptions)[number]; export interface BentoProgressBarProps { /** * Whether the static variant animates from 0% to its value when mounted. */ staticAnimation?: boolean; /** * The message announced to screen readers when a determinate bar reaches 100%. * Defaults to a localized "Complete". */ completionAnnouncement?: string; /** * If set, displays a description text below the progress bar. */ description?: string; /** * Sets the direction in which the bar fills. Use `reverse` to fill from the opposite end. */ direction?: BentoProgressBarDirection; /** * If set, displays an error message below the bar and applies error styling. */ errorMessage?: string; /** * Sets the thickness of the progress bar. */ height?: BentoProgressBarHeight; /** * Whether an animated shimmer overlay plays across the indicator. */ shimmer?: boolean; /** * Whether a stop indicator marks the end of the track (determinate only). */ stopIndicator?: boolean; /** * Whether a green success flash plays when the value reaches 100. */ successFlash?: boolean; /** * Whether the percentage value is shown next to the label. */ valueVisible?: boolean; /** * The text label displayed above the progress bar. */ label?: string; /** * If set, displays an info icon with this tooltip message next to the label. */ tooltipText?: string; /** * Sets the visual style of the progress bar to convey its status. */ type?: BentoProgressBarType; /** * Sets the current value of the progress bar, clamped to the range 0 to 100. */ value?: number; /** * Determines how progress is represented: `determinate` for a known value, * `indeterminate` for an animated bar when progress cannot be determined, and * `static` for a fixed bar. */ variant?: BentoProgressBarVariant; }
@@ -0,0 +1 @@
1
+ <template> <div class="b-progress-bar" :class="conditionalClasses"> <div class="b-progress-bar__header"> <field-label v-if="label" :id="labelId" class="b-progress-bar__label" :label="label" :tooltip-text="tooltipText" /> <div v-if="shouldShowValue" class="b-progress-bar__value"> <slot name="value"> <bento-typography el="span" stronger>{{ valueText }}</bento-typography> </slot> </div> </div> <div class="b-progress-bar__track" :class="conditionalTrackClasses" role="progressbar" v-bind="trackA11yAttrs"> <div class="b-progress-bar__track-indicator" :class="conditionalIndicatorClasses" :style="indicatorStyle" @animationend="onIndicatorAnimationEnd" ></div> </div> <error-message v-if="!!errorMessage" :id="errorId" :error-message="errorMessage" class="b-progress-bar__error-message" /> <bento-typography v-if="description" :id="descriptionId" el="span" class="b-progress-bar__description" :class="conditionalDescriptionClasses" > {{ description }} </bento-typography> <div class="b-progress-bar__announcement" role="status" aria-live="polite"> {{ completionMessage }} </div> </div> </template> <script setup lang="ts"> import { computed, ref, useSlots, watch } from 'vue'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; import { useHasSlot } from '@/composables'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { type BentoProgressBarProps } from './progress-bar.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoProgressBarProps>(), { staticAnimation: true, completionAnnouncement: undefined, description: undefined, direction: 'default', errorMessage: undefined, height: 'default', shimmer: true, stopIndicator: true, successFlash: true, valueVisible: true, label: undefined, tooltipText: undefined, type: 'default', value: undefined, variant: 'determinate', }); const slots = useSlots(); const hasSlot = useHasSlot(slots); const labelId = generateUid('progress-bar-label'); const descriptionId = generateUid('progress-bar-description'); const errorId = generateUid('progress-bar-error'); // Value const clampedValue = computed(() => { if (props.value === undefined) { return 0; } return Math.min(Math.max(props.value, 0), 100); }); const valueText = computed(() => `${clampedValue.value}%`); // Success flash, triggered once when the value crosses into 100 const isSuccessFlashing = ref(false); // Screen readers do not reliably announce progressbar value changes, so completion is announced // through a polite live region when the value crosses into 100. const completionMessage = ref(''); watch(clampedValue, (value, previousValue) => { if (props.variant !== 'determinate' || previousValue >= 100 || value < 100) { return; } if (props.successFlash) { isSuccessFlashing.value = true; } completionMessage.value = props.completionAnnouncement ?? t('progressComplete'); }); // Variant state const effectiveVariant = computed(() => { if (props.valueVisible && props.value === undefined && !hasSlot('value')) { return 'indeterminate'; } return props.variant; }); const isIndeterminate = computed(() => effectiveVariant.value === 'indeterminate'); // Display flags const shouldShowValue = computed( () => props.valueVisible && !isIndeterminate.value && (hasSlot('value') || props.value !== undefined) ); const shouldShowError = computed(() => !!props.errorMessage); // Indicator styling const indicatorStyle = computed(() => { if (isIndeterminate.value) { return undefined; } const width = props.direction === 'default' ? `${clampedValue.value}%` : `${100 - clampedValue.value}%`; return { width }; }); // Accessibility const trackA11yAttrs = computed(() => { const describedBy = [props.description ? descriptionId : null, props.errorMessage ? errorId : null].filter(Boolean).join(' ') || undefined; return { 'aria-labelledby': props.label ? labelId : undefined, 'aria-describedby': describedBy, 'aria-busy': isIndeterminate.value || undefined, 'aria-valuemin': isIndeterminate.value ? undefined : 0, 'aria-valuemax': isIndeterminate.value ? undefined : 100, 'aria-valuenow': isIndeterminate.value ? undefined : clampedValue.value, }; }); // Conditional classes const conditionalClasses = computed(() => ({ [`b-progress-bar--${props.height}`]: props.height !== 'default', [`b-progress-bar--${props.type}`]: props.type !== 'default', 'b-progress-bar--error': shouldShowError.value, })); const conditionalTrackClasses = computed(() => ({ 'b-progress-bar__track--reverse': props.direction === 'reverse', 'b-progress-bar__track--with-stop-indicator': props.stopIndicator && effectiveVariant.value === 'determinate', 'b-progress-bar__track--with-success-flash': isSuccessFlashing.value && effectiveVariant.value === 'determinate', })); const conditionalIndicatorClasses = computed(() => ({ 'b-progress-bar__track-indicator--indeterminate': isIndeterminate.value, 'b-progress-bar__track-indicator--static': effectiveVariant.value === 'static', 'b-progress-bar__track-indicator--static-animated': props.staticAnimation && effectiveVariant.value === 'static', 'b-progress-bar__track-indicator--with-shimmer': props.shimmer && !props.errorMessage && effectiveVariant.value === 'determinate', 'b-progress-bar__track-indicator--with-success-flash': isSuccessFlashing.value && effectiveVariant.value === 'determinate', [`b-progress-bar__track-indicator--${props.type}`]: props.type !== 'default', 'b-progress-bar__track-indicator--error': shouldShowError.value, })); const conditionalDescriptionClasses = computed(() => ({ 'b-progress-bar__description--error': !!props.errorMessage, })); const onIndicatorAnimationEnd = (event: AnimationEvent) => { if (event.animationName?.includes('success-flash')) { isSuccessFlashing.value = false; } }; </script> <script lang="ts"> /** * A progress bar communicates the completion status of a task or process. * * @usage * import { BentoProgressBar } from '@adyen/bento-vue2'; * * export default { * components: { BentoProgressBar }, * template: ` * <bento-progress-bar :value="40" label="Uploading files" /> * ` * }; */ export default { name: 'bento-progress-bar', i18n: { messages }, }; </script> <style lang="scss" scoped src="./progress-bar.scss" />
@@ -74,6 +74,7 @@
74
74
  "pagination",
75
75
  "payment-method",
76
76
  "popover",
77
+ "progress-bar",
77
78
  "promo-banner",
78
79
  "radio-button",
79
80
  "radio-group",