@adyen/bento-mcp 0.10.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.
- package/CHANGELOG.md +5 -0
- package/dist/assets/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.vue +1 -1
- package/dist/assets/components/date-picker/date-picker.vue +1 -1
- package/dist/assets/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.vue +1 -1
- package/dist/assets/components/date-range-picker/date-range-picker.stories.ts +1 -1
- package/dist/assets/components/date-range-picker/date-range-picker.vue +1 -1
- package/dist/assets/components/header-with-views/components/header-with-views-all-views/header-with-views-all-views.vue +1 -1
- package/dist/assets/components/header-with-views/header-with-views.vue +1 -1
- package/dist/assets/components/internal/calendar/composables/use-granularity-adjustments.types.ts +1 -1
- package/dist/assets/components/menu/menu.docs.mdx +8 -7
- package/dist/assets/components/menu/menu.vue +1 -1
- package/dist/assets/components/popover/popover.types.ts +1 -1
- package/dist/assets/components/popover/popover.vue +1 -1
- package/dist/assets/components/popper-container/popper-container.types.ts +1 -1
- package/dist/assets/components/popper-container/popper-container.vue +1 -1
- package/dist/assets/components/progress-bar/progress-bar.docs.mdx +102 -0
- package/dist/assets/components/progress-bar/progress-bar.stories.ts +1 -0
- package/dist/assets/components/progress-bar/progress-bar.types.ts +1 -0
- package/dist/assets/components/progress-bar/progress-bar.vue +1 -0
- package/dist/assets/components.json +1 -0
- package/dist/assets/deprecations.md +160 -160
- package/dist/assets/index.ts +1 -1
- package/dist/assets/usage.json +78 -77
- package/dist/assets/variables.css +39 -39
- package/dist/main.js +1 -1
- package/package.json +1 -1
- package/dist/assets/components/internal/calendar/composables/use-range-pane.types.ts +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div ref="hostRef" class="b-header-with-views"> <div class="b-header-with-views__container" :class="conditionalContainerClass"> <div class="b-header-with-views__title"> <bento-typography el="h2" variant="title">{{ title }}</bento-typography> <info-icon-with-popover v-if="infoIconTooltipText || hasSlot('infoIconTooltipContent')" :popover-text="infoIconTooltipText" class="b-header-with-views__title-icon" > <template v-if="hasSlot('infoIconTooltipContent')" #default> <slot name="infoIconTooltipContent" /> </template> </info-icon-with-popover> </div> <fieldset ref="viewsRef" class="b-header-with-views__bar" :aria-label="t('viewListLabel')"> <div v-for="(view, index) in computedViews" :key="index" ref="viewRefs" class="b-header-with-views__bar-tab" :class="isViewActive(index) ? 'b-header-with-views__bar-tab--active' : null" > <bento-button class="b-header-with-views__bar-tab-button" variant="tertiary-with-background" :aria-current="currentActiveView === index ? 'true' : 'false'" @click="handleViewSelect(index)" @keydown.enter.native="handleViewSelect(index)" @keydown.space.native="handleViewSelect(index)" > <template #iconLeft> <span class="b-header-with-views__bar-tab-button-icon" :class="`b-header-with-views__bar-tab-button-icon--${view.iconColor}`" aria-hidden="true" > <component :is="BentoHeaderWithViewsItemIconElement[view.icon]" /> </span> </template> <bento-typography el="span" variant="body" stronger class="b-header-with-views__bar-tab-button-text" > {{ view.label }} </bento-typography> <template v-if="isViewUnsaved(index)" #iconRight> <span class="b-header-with-views__bar-tab-button--unsaved"> <dot-icon :svg-title="t('withUnsavedChanges')" /> </span> </template> </bento-button> <bento-menu v-if="currentActiveView === index" ref="editViewMenuRefs" class="b-header-with-views__bar-tab-menu" :button="{ variant: 'tertiary' }" :data="getViewMenuData(index)" :close-on-click-outside-options="{ ignore: [`#${editViewPopoverId}`], }" :menu-fixed-positioning="true" @open="showViewMenuPopover(index)" @close="hideViewMenuPopover()" @keydown.tab.native.capture="handleTabEditViewPopover" > <template #iconLeft> <options-horizontal-icon :svg-title="t('openViewOptions', { label: view.label })" /> </template> </bento-menu> </div> <div ref="viewControlRef" class="b-header-with-views__bar-control"> <!-- All views --> <header-with-views-all-views v-if="computedViews.length" ref="viewControlAllViewsRef" :menu-items="computedViews" :menu-items-actions="getViewMenuData(activeViewMenuPopover)" :menu-close-on-click-outside-options="{ ignore: [`#${editViewPopoverId}`] }" @close:view-menu="hideViewMenuPopover()" @close:all-views="showAllViewsPopover = false" @open:all-views="showAllViewsPopover = true" @open:view-menu="index => showViewMenuPopover(index)" @reorder:view="views => handleReorder(views)" @select:view="index => handleViewSelect(index)" /> <bento-toggle-button ref="createViewButtonRef" :aria-expanded="showCreateViewPopover" :aria-controls="showCreateViewPopover ? createViewPopoverId : null" :toggled="showCreateViewPopover" variant="tertiary" @click="showCreateViewPopover = !showCreateViewPopover" > <template #iconLeft> <plus-icon :svg-title="t('createView')" :aria-hidden="computedViews.length === 0" /> </template> <template v-if="computedViews.length === 0" #default>{{ t('createView') }}</template> </bento-toggle-button> </div> </fieldset> <header-meta v-if="hasMetaSection" class="b-header-with-views__meta-container" :description="description" :last-updated="lastUpdated" :reload-button="reloadButton" > <template v-if="hasSlot('description')" #description><slot name="description" /></template> </header-meta> <button-actions-with-menu v-if="actions" :actions="actions" :actions-layout="isCompact ? 'buttons-start' : 'buttons-end'" :displayed-actions="2" :disable-responsive-behavior="true" :hide-displayed-actions-breakpoint="STACKED_LAYOUT_BREAKPOINT" class="b-header-with-views__actions" /> </div> <!-- Create view --> <bento-popover v-if="activeViewMenuPopover === null && createViewButtonRef" :id="createViewPopoverId" ref="createViewPopoverRef" :dismissible="true" :fixed-positioning="true" :target-element="createViewButtonRef" :title="t('createView')" :open="showCreateViewPopover" :overflow-visible="true" position="bottom-start" @dismiss="hideCreateViewPopover" @keydown.esc.native.capture.stop.prevent="hideCreateViewPopover" > <header-with-views-edit-form context="create" :errors="errors" :additional-form-item="additionalViewSetting" :disable-customization="disableCustomization" @blur:label="value => validateLabel(value, activeViewMenuPopover)" @input:label="value => validateLabel(value, activeViewMenuPopover)" @keydown:enter="handleKeydownEnterViewItem" @update="handleCreateViewItem" @cancel="hideCreateViewPopover" > </header-with-views-edit-form> </bento-popover> <!-- Edit view --> <bento-popover v-if="activeViewMenuPopover !== null && editViewTargetElementRef" :id="editViewPopoverId" ref="editViewPopoverRef" :disable-focus-trap="false" :dismissible="true" :fixed-positioning="true" :target-element="editViewTargetElementRef" :title="t('editDetails')" :open="activeEditViewPopover !== null" :overflow-visible="true" position="right-start" @dismiss="setActiveEditViewPopover()" @keydown.esc.native.capture.stop.prevent="setActiveEditViewPopover()" > <header-with-views-edit-form v-if="computedViews[activeEditViewPopover]" context="edit" :additional-form-item="computedViews[activeEditViewPopover].additionalViewSetting" :errors="errors" :icon="computedViews[activeEditViewPopover].icon" :icon-color="computedViews[activeEditViewPopover].iconColor" :label="computedViews[activeEditViewPopover].label" :disable-customization="disableCustomization" @blur:label="value => validateLabel(value, activeViewMenuPopover)" @input:label="value => validateLabel(value, activeViewMenuPopover)" @keydown:enter="handleKeydownEnterViewItem" @update="handleUpdateViewItem" @cancel="hideEditViewPopover" > </header-with-views-edit-form> </bento-popover> <!-- Unsaved views prompt --> <bento-modal v-if="showUnsavedModalWarning" :actions="computedUnsavedViewModalActions" :is-dismissible="false" :is-open="showUnsavedModalWarning" @close-modal="showUnsavedModalWarning = false" > <template #default>{{ t('unsavedChangesTitle', { label: computedViews[currentActiveView].label }) }}</template> <template #content> <div class="b-header-with-views__modal-description"> <bento-typography>{{ t('unsavedChangesDescription') }}</bento-typography> <bento-checkbox v-if="skipUnsavedChangesText" :model-value="skipUnsavedChangesReminder" @update:model-value="value => (skipUnsavedChangesReminder = value)" > {{ skipUnsavedChangesText }} </bento-checkbox> </div> </template> </bento-modal> <!-- Delete views prompt --> <bento-modal v-if="showDeleteModalWarning" :actions="computedDeleteViewModalActions" :is-open="showDeleteModalWarning" :is-dismissible="false" destructive-actions @click.native.capture="handleClickModal" @close-modal="showDeleteModalWarning = false" > <template #default> {{ t('deleteViewTitle', { label: computedViewToBeDeletedLabel }) }} </template> <template #content> <div class="b-header-with-views__modal-description"> <bento-typography>{{ t('deleteViewDescription', { label: computedViewToBeDeletedLabel }) }}</bento-typography> </div> </template> </bento-modal> </div> </template> <script setup lang="ts"> import { BentoButton, BentoToggleButton } from '@/components/button'; import { BentoCheckbox } from '@/components/checkbox'; import { BentoMenu, type BentoMenuItem } from '@/components/menu'; import BentoModal from '@/components/modal/modal.vue'; import { BentoPopover } from '@/components/popover'; import { BentoTypography } from '@/components/typography'; import { HeaderMeta } from '@/internal'; import { useBentoToastController, useClickOutside, useHasSlot } from '@/composables'; import { generateUid } from '@/core/utils/ts'; import ButtonActionsWithMenu from '@/internal/button-actions-with-menu/button-actions-with-menu.vue'; import { InfoIconWithPopover } from '@/internal/info-icon-with-popover'; import { useI18n } from '@/utils/ts/i18n'; import BinIcon from '@adyen/ui-assets-icons-16/vue/bin'; import DotIcon from '@adyen/ui-assets-icons-16/vue/dot'; import DownloadIcon from '@adyen/ui-assets-icons-16/vue/download'; import EditIcon from '@adyen/ui-assets-icons-16/vue/edit-2'; import HeartIcon from '@adyen/ui-assets-icons-16/vue/heart'; import OptionsHorizontalIcon from '@adyen/ui-assets-icons-16/vue/options-horizontal'; import PlusIcon from '@adyen/ui-assets-icons-16/vue/plus'; import PlusMultipleIcon from '@adyen/ui-assets-icons-16/vue/plus-multiple'; import RefreshIcon from '@adyen/ui-assets-icons-16/vue/refresh'; import { useElementSize } from '@vueuse/core'; import { computed, nextTick, onMounted, ref, useSlots, watch } from 'vue'; import { HeaderWithViewsAllViews, HeaderWithViewsEditForm } from './components'; import { useEditViews, useResponsiveViews } from './composables'; import { type BentoHeaderWithViewsEmits, type BentoHeaderWithViewsItem, BentoHeaderWithViewsItemIconElement, type BentoHeaderWithViewsProps, } from './header-with-views.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoHeaderWithViewsProps>(), { actions: () => [], additionalViewSetting: null, title: null, description: null, disableCustomization: false, lastUpdated: null, reloadButton: null, infoIconTooltipText: null, views: () => [], disableAutoSelect: false, activeViewIndex: undefined, }); const emit = defineEmits<BentoHeaderWithViewsEmits>(); const STACKED_LAYOUT_BREAKPOINT = 573; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const slots = useSlots(); const hasSlot = useHasSlot(slots); // refs const createViewButtonRef = ref(null); const createViewPopoverRef = ref(null); const editViewPopoverRef = ref(null); const editViewMenuRefs = ref([]); const editViewTargetElementRef = ref(null); // used as a mount for the edit form popover const hostRef = ref(null); const viewsRef = ref<HTMLElement>(null); // container for all views const viewRefs = ref<Array<HTMLDivElement>>([]); // individual view const viewControlRef = ref(null); // all views menu and create view button container const viewControlAllViewsRef = ref(null); // all views menu // controls const showCreateViewPopover = ref<boolean>(false); // control the create view popover const showAllViewsPopover = ref<boolean>(false); // control when menu button is shown const showUnsavedModalWarning = ref<boolean>(false); const showDeleteModalWarning = ref<boolean>(false); const activeEditViewPopover = ref<number>(null); // control the edit view popover const activeViewMenuPopover = ref<number>(null); // control when menu items are shown const internalActiveView = ref<number>(null); // control which view the user is currently on const nextActiveView = ref<number>(null); // control which view is does the user want to navigate to, used for caching unsaved view navigation const viewToBeDeleted = ref<number>(null); // cache which view is marked for deletion // Computed property to handle two-way binding for activeViewIndex const currentActiveView = computed({ get: () => internalActiveView.value, set: (value: number | null) => { internalActiveView.value = value; emit('update:active-view-index', value); }, }); // models const skipUnsavedChangesReminder = ref<boolean>(false); const createViewPopoverId = generateUid('createViewPopover'); const editViewPopoverId = generateUid('editViewPopover'); useResponsiveViews({ container: viewsRef, children: viewRefs, offset: viewControlRef, activeViewIndex: currentActiveView, }); const { computedViews, clearError, duplicateView, errors, setViews, updateView, validateLabel } = useEditViews({ additionalViewSetting: props.additionalViewSetting, views: props.views, }); const hideCreateViewPopover = async (updateFocus = true) => { showCreateViewPopover.value = false; if (updateFocus) { await nextTick(); createViewButtonRef.value.$el.focus(); } clearError(); }; const hideEditViewPopover = () => { const currentMenuRef = showAllViewsPopover.value ? viewControlAllViewsRef.value.$refs.editViewMenuRefs[activeEditViewPopover.value] : editViewMenuRefs.value[0]; currentMenuRef?.closeMenu(); }; useClickOutside(createViewPopoverRef, () => hideCreateViewPopover(false), { ignore: [createViewButtonRef], }); useClickOutside(editViewPopoverRef, () => setActiveEditViewPopover(), { ignore: [editViewTargetElementRef], }); const { width: hostWidth } = useElementSize(hostRef); const { addToast } = useBentoToastController(); const computedUnsavedViewModalActions = computed(() => [ { title: t('saveChanges'), event: handleSaveChanges, }, { title: t('leave'), event: handleRestoreChanges, }, ]); const computedDeleteViewModalActions = computed(() => [ { title: t('deleteViewAction'), event: async () => { showDeleteModalWarning.value = false; const updatedViewObj = updateView(null, viewToBeDeleted.value); emit('delete:view', updatedViewObj); hideViewMenuPopover(); // if a view that is placed higher in the order is deleted, ignore if (currentActiveView.value < viewToBeDeleted.value) { return; } // if a view that is placed lower in the order is deleted, // we need to shift the index to maintain the current view if (currentActiveView.value > viewToBeDeleted.value) { currentActiveView.value -= 1; } else if (currentActiveView.value === viewToBeDeleted.value) { if (updatedViewObj.newViews.length === 0) { // there are no more views currentActiveView.value = null; } else if (currentActiveView.value >= updatedViewObj.newViews.length) { // shift it to the left currentActiveView.value -= 1; } // else, keep it at the same index } viewToBeDeleted.value = null; }, }, { title: t('cancel'), event: () => { showDeleteModalWarning.value = false; }, }, ]); const computedViewToBeDeletedLabel = computed(() => computedViews.value[viewToBeDeleted.value]?.label); const hasMetaSection = computed(() => props.description || hasSlot('description') || props.lastUpdated); const isCompact = computed(() => hostWidth.value <= STACKED_LAYOUT_BREAKPOINT); const conditionalContainerClass = computed(() => ({ 'b-header-with-views__container--is-compact': isCompact.value, 'b-header-with-views__container--has-meta': hasMetaSection.value, })); watch([currentActiveView, computedViews], ([newIndex, newViews], [oldIndex, oldViews]) => { // skip if the labels are the same or no active view const isViewUpdated = newIndex === oldIndex && newViews.length === oldViews.length; const isViewDeletedWithChangedIndex = newViews[newIndex]?.label === oldViews[oldIndex]?.label; if (isViewUpdated || isViewDeletedWithChangedIndex) { return; } // allow empty label // occurs when the user removes all views emit('select:view', newViews[newIndex]?.label ?? null); }); // whenever the view menu popover changes, we reset the edit popover because it is never opened by default watch(activeViewMenuPopover, () => { setActiveEditViewPopover(); }); watch(showAllViewsPopover, value => { if (value) { return; } // if show all view popover is closed, close children as well hideViewMenuPopover(); }); // set the popover to open next to the edit button watch(activeEditViewPopover, value => { const editButtonIndex = isViewUnsaved(value) ? 2 : 0; // EDGE CASE: // Since edit button can be clicked while in all views popover or while it is in the bar, // we will not support the case where edit popover is opened and the screen is resize until the view menu is hidden. // Edit popover will still be displayed but the position does not have to follow the edit button as it is hidden. if (showAllViewsPopover.value) { const menuListItemRef = viewControlAllViewsRef.value.$refs.editViewMenuRefs[activeViewMenuPopover.value]?.$refs ?.menuItemListRef; editViewTargetElementRef.value = viewControlAllViewsRef.value.$refs.editViewMenuRefs[activeViewMenuPopover.value]?.$refs?.menuItemListRef ?.$refs?.menuItemRef?.[editButtonIndex] ?? null; if (!value) { menuListItemRef?.focusItem(); clearError(); } return; } const menuListItemRef = editViewMenuRefs.value.find(ref => ref.$refs.menuItemListRef)?.$refs?.menuItemListRef; editViewTargetElementRef.value = menuListItemRef?.$refs?.menuItemRef?.[editButtonIndex] ?? null; if (!value) { menuListItemRef?.focusItem(); clearError(); } }); // Watch for external changes to the activeViewIndex prop watch( () => props.activeViewIndex, newValue => { if (newValue !== undefined && newValue >= 0 && newValue < computedViews.value.length) { currentActiveView.value = newValue; } } ); onMounted(() => { if ( props.activeViewIndex !== undefined && props.activeViewIndex >= 0 && props.activeViewIndex < computedViews.value.length ) { currentActiveView.value = props.activeViewIndex; } else if (!props.disableAutoSelect) { const defaultView = computedViews.value.findIndex(view => view.isDefault); currentActiveView.value = defaultView >= 0 ? defaultView : 0; } }); const getViewMenuData = (index: number): Array<BentoMenuItem> => [ ...(isViewUnsaved(index) ? [ { text: t('saveChanges'), handler: handleSaveChanges, icon: DownloadIcon, }, { addDivider: true, text: t('restoreChanges'), handler: handleRestoreChanges, icon: RefreshIcon, }, ] : []), ...(!computedViews.value[index]?.isFixed ? [ { text: t('edit'), handler: () => { setActiveEditViewPopover(activeViewMenuPopover.value); }, icon: EditIcon, skipClose: true, }, ] : []), { text: t('setAsDefault'), handler: () => { const updatedViewObj = updateView({ isDefault: true }, activeViewMenuPopover.value); emit('update:view', updatedViewObj); addToast({ text: t('setAsDefaultToast', { label: updatedViewObj.targetViewLabel }), }); hideViewMenuPopover(); }, icon: HeartIcon, }, { text: t('duplicate'), handler: () => { const updatedViewObj = duplicateView(activeViewMenuPopover.value); emit('duplicate:view', updatedViewObj); hideViewMenuPopover(); // switch to newly duplicated view handleViewSelect(updatedViewObj.newViews.length - 1); }, icon: PlusMultipleIcon, }, ...(!computedViews.value[index]?.isFixed ? [ { text: t('delete'), handler: () => { viewToBeDeleted.value = activeViewMenuPopover.value; showDeleteModalWarning.value = true; }, critical: true, icon: BinIcon, }, ] : []), ]; const showViewMenuPopover = (index: number) => { activeViewMenuPopover.value = index; }; const hideViewMenuPopover = () => { activeViewMenuPopover.value = null; setActiveEditViewPopover(); }; const setActiveEditViewPopover = (index?: number) => { if (index === activeEditViewPopover.value) { activeEditViewPopover.value = null; } else { activeEditViewPopover.value = index ?? null; } }; const handleCreateViewItem = (value: BentoHeaderWithViewsItem) => { const updatedViewObj = updateView(value); emit('add:view', updatedViewObj); hideCreateViewPopover(); // switch to new view on add handleViewSelect(updatedViewObj.newViews.findIndex(view => updatedViewObj.targetViewLabel === view.label)); addToast({ text: t('createViewToast', { label: updatedViewObj.targetViewLabel }), }); }; const handleUpdateViewItem = (value: BentoHeaderWithViewsItem) => { emit('update:view', updateView(value, activeEditViewPopover.value)); hideEditViewPopover(); }; const handleKeydownEnterViewItem = (value: BentoHeaderWithViewsItem) => { validateLabel(value.label, activeViewMenuPopover.value); if (Object.values(errors).some(error => error.length > 0)) { return; } if (activeEditViewPopover.value) { handleUpdateViewItem(value); return; } handleCreateViewItem(value); }; const handleViewSelect = (index: number) => { // If there's no currently active view, or if there are no unsaved changes, // just switch to the new view. if (currentActiveView.value === null || !props.hasChanges || props.skipUnsavedChangesText === null) { // If switching from an existing view, emit restore event. if (currentActiveView.value !== null) { emit('restore:view', computedViews.value[currentActiveView.value].label); } currentActiveView.value = index; return; } nextActiveView.value = index; showUnsavedModalWarning.value = true; }; const handleReorder = (reorderedViews: Array<BentoHeaderWithViewsItem>) => { const currentActiveLabel = currentActiveView.value !== null ? computedViews.value[currentActiveView.value]?.label : null; const result = setViews(reorderedViews); if (currentActiveLabel) { const newIndex = result.newViews.findIndex(view => view.label === currentActiveLabel); if (newIndex !== -1) { currentActiveView.value = newIndex; } } emit('reorder:view', result); }; const handleClickModal = (event: MouseEvent) => { if ((event.target as HTMLElement).closest('button')) { return; } event.stopPropagation(); event.preventDefault(); }; const handleTabEditViewPopover = () => { // skip built-in refocus on the menu button when menu is closed // enables tab to flow as intended by HTML spec editViewMenuRefs.value[0].closeMenu(false); }; const handleSaveChanges = () => { const label = computedViews.value[currentActiveView.value].label; emit('save:view', label); if (showUnsavedModalWarning.value) { emit('update:skip-unsaved-changes', skipUnsavedChangesReminder.value); // fired when user saves the view while navigating to a different tab currentActiveView.value = nextActiveView.value; nextActiveView.value = null; showUnsavedModalWarning.value = false; } else { // fired when user saves the view via the menu hideViewMenuPopover(); } addToast({ text: t('saveChangesToast', { label }), action: { handler: () => emit('restore:view', label), text: t('undo'), }, }); }; const handleRestoreChanges = () => { emit('restore:view', computedViews.value[currentActiveView.value].label); if (showUnsavedModalWarning.value) { emit('update:skip-unsaved-changes', skipUnsavedChangesReminder.value); // fired when user restore the view while navigating to a different tab currentActiveView.value = nextActiveView.value; nextActiveView.value = null; showUnsavedModalWarning.value = false; } else { // fired when user restore the view via the menu hideViewMenuPopover(); } }; const isViewActive = (index: number) => currentActiveView.value === index; const isViewUnsaved = (index: number) => isViewActive(index) && props.hasChanges; </script> <script lang="ts"> /** * Header with views allow users to save and quickly retrieve configurations tailored to specific tasks. * For example, managing daily transactions, auditing, or analyzing specific data sets. * It also allows the user to define the columns, filters, and search queries they need. * * @example * import { BentoHeaderWithViews } from '@adyen/bento-vue2'; * * export default { * components: { BentoHeaderWithViews }, * template: ` * <bento-header-with-views * :actions="[ * { title: 'Primary action', event: () => undefined }, * { title: 'Secondary action', event: () => undefined }, * { title: 'Tertiary action', event: () => undefined }, * { title: 'Another action', event: () => undefined } * ]" * description="Page Description" * title="Page Title" * :views="[ * { icon: 'stack', iconColor: 'blue', label: 'A very long value' }, * { icon: 'stack', iconColor: 'primary', label: 'Title 1' }, * { icon: 'stack', iconColor: 'primary', label: 'Title 2' } * ]" * :additionalViewSetting="{ * label: 'Include date filter (relative)', * value: false * }" * hasChanges * /> * ` * }; */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./header-with-views.scss" />
|
|
1
|
+
<template> <div ref="hostRef" class="b-header-with-views"> <div class="b-header-with-views__container" :class="conditionalContainerClass"> <div class="b-header-with-views__title"> <bento-typography el="h2" variant="title">{{ title }}</bento-typography> <info-icon-with-popover v-if="infoIconTooltipText || hasSlot('infoIconTooltipContent')" :popover-text="infoIconTooltipText" class="b-header-with-views__title-icon" > <template v-if="hasSlot('infoIconTooltipContent')" #default> <slot name="infoIconTooltipContent" /> </template> </info-icon-with-popover> </div> <fieldset ref="viewsRef" class="b-header-with-views__bar" :aria-label="t('viewListLabel')"> <div v-for="(view, index) in computedViews" :key="index" ref="viewRefs" class="b-header-with-views__bar-tab" :class="isViewActive(index) ? 'b-header-with-views__bar-tab--active' : null" > <bento-button class="b-header-with-views__bar-tab-button" variant="tertiary-with-background" :aria-current="currentActiveView === index ? 'true' : 'false'" @click="handleViewSelect(index)" @keydown.enter.native="handleViewSelect(index)" @keydown.space.native="handleViewSelect(index)" > <template #iconLeft> <span class="b-header-with-views__bar-tab-button-icon" :class="`b-header-with-views__bar-tab-button-icon--${view.iconColor}`" aria-hidden="true" > <component :is="BentoHeaderWithViewsItemIconElement[view.icon]" /> </span> </template> <bento-typography el="span" variant="body" stronger class="b-header-with-views__bar-tab-button-text" > {{ view.label }} </bento-typography> <template v-if="isViewUnsaved(index)" #iconRight> <span class="b-header-with-views__bar-tab-button--unsaved"> <dot-icon :svg-title="t('withUnsavedChanges')" /> </span> </template> </bento-button> <bento-menu v-if="currentActiveView === index" ref="editViewMenuRefs" class="b-header-with-views__bar-tab-menu" :button="{ variant: 'tertiary', 'aria-label': t('openViewOptions', { label: view.label }) }" :data="getViewMenuData(index)" :close-on-click-outside-options="{ ignore: [`#${editViewPopoverId}`], }" :menu-fixed-positioning="true" @open="showViewMenuPopover(index)" @close="hideViewMenuPopover()" @keydown.tab.native.capture="handleTabEditViewPopover" > <template #iconLeft> <options-horizontal-icon :aria-hidden="true" /> </template> </bento-menu> </div> <div ref="viewControlRef" class="b-header-with-views__bar-control"> <!-- All views --> <header-with-views-all-views v-if="computedViews.length" ref="viewControlAllViewsRef" :menu-items="computedViews" :menu-items-actions="getViewMenuData(activeViewMenuPopover)" :menu-close-on-click-outside-options="{ ignore: [`#${editViewPopoverId}`] }" @close:view-menu="hideViewMenuPopover()" @close:all-views="showAllViewsPopover = false" @open:all-views="showAllViewsPopover = true" @open:view-menu="index => showViewMenuPopover(index)" @reorder:view="views => handleReorder(views)" @select:view="index => handleViewSelect(index)" /> <bento-toggle-button ref="createViewButtonRef" :aria-expanded="showCreateViewPopover" :aria-controls="showCreateViewPopover ? createViewPopoverId : null" :toggled="showCreateViewPopover" variant="tertiary" @click="showCreateViewPopover = !showCreateViewPopover" > <template #iconLeft> <plus-icon :svg-title="t('createView')" :aria-hidden="computedViews.length === 0" /> </template> <template v-if="computedViews.length === 0" #default>{{ t('createView') }}</template> </bento-toggle-button> </div> </fieldset> <header-meta v-if="hasMetaSection" class="b-header-with-views__meta-container" :description="description" :last-updated="lastUpdated" :reload-button="reloadButton" > <template v-if="hasSlot('description')" #description><slot name="description" /></template> </header-meta> <button-actions-with-menu v-if="actions" :actions="actions" :actions-layout="isCompact ? 'buttons-start' : 'buttons-end'" :displayed-actions="2" :disable-responsive-behavior="true" :hide-displayed-actions-breakpoint="STACKED_LAYOUT_BREAKPOINT" class="b-header-with-views__actions" /> </div> <!-- Create view --> <bento-popover v-if="activeViewMenuPopover === null && createViewButtonRef" :id="createViewPopoverId" ref="createViewPopoverRef" :dismissible="true" :fixed-positioning="true" :target-element="createViewButtonRef" :title="t('createView')" :open="showCreateViewPopover" :overflow-visible="true" position="bottom-start" @dismiss="hideCreateViewPopover" @keydown.esc.native.capture.stop.prevent="hideCreateViewPopover" > <header-with-views-edit-form context="create" :errors="errors" :additional-form-item="additionalViewSetting" :disable-customization="disableCustomization" @blur:label="value => validateLabel(value, activeViewMenuPopover)" @input:label="value => validateLabel(value, activeViewMenuPopover)" @keydown:enter="handleKeydownEnterViewItem" @update="handleCreateViewItem" @cancel="hideCreateViewPopover" > </header-with-views-edit-form> </bento-popover> <!-- Edit view --> <bento-popover v-if="activeViewMenuPopover !== null && editViewTargetElementRef" :id="editViewPopoverId" ref="editViewPopoverRef" :disable-focus-trap="false" :dismissible="true" :fixed-positioning="true" :target-element="editViewTargetElementRef" :title="t('editDetails')" :open="activeEditViewPopover !== null" :overflow-visible="true" position="right-start" @dismiss="setActiveEditViewPopover()" @keydown.esc.native.capture.stop.prevent="setActiveEditViewPopover()" > <header-with-views-edit-form v-if="computedViews[activeEditViewPopover]" context="edit" :additional-form-item="computedViews[activeEditViewPopover].additionalViewSetting" :errors="errors" :icon="computedViews[activeEditViewPopover].icon" :icon-color="computedViews[activeEditViewPopover].iconColor" :label="computedViews[activeEditViewPopover].label" :disable-customization="disableCustomization" @blur:label="value => validateLabel(value, activeViewMenuPopover)" @input:label="value => validateLabel(value, activeViewMenuPopover)" @keydown:enter="handleKeydownEnterViewItem" @update="handleUpdateViewItem" @cancel="hideEditViewPopover" > </header-with-views-edit-form> </bento-popover> <!-- Unsaved views prompt --> <bento-modal v-if="showUnsavedModalWarning" :actions="computedUnsavedViewModalActions" :is-dismissible="false" :is-open="showUnsavedModalWarning" @close-modal="showUnsavedModalWarning = false" > <template #default>{{ t('unsavedChangesTitle', { label: computedViews[currentActiveView].label }) }}</template> <template #content> <div class="b-header-with-views__modal-description"> <bento-typography>{{ t('unsavedChangesDescription') }}</bento-typography> <bento-checkbox v-if="skipUnsavedChangesText" :model-value="skipUnsavedChangesReminder" @update:model-value="value => (skipUnsavedChangesReminder = value)" > {{ skipUnsavedChangesText }} </bento-checkbox> </div> </template> </bento-modal> <!-- Delete views prompt --> <bento-modal v-if="showDeleteModalWarning" :actions="computedDeleteViewModalActions" :is-open="showDeleteModalWarning" :is-dismissible="false" destructive-actions @click.native.capture="handleClickModal" @close-modal="showDeleteModalWarning = false" > <template #default> {{ t('deleteViewTitle', { label: computedViewToBeDeletedLabel }) }} </template> <template #content> <div class="b-header-with-views__modal-description"> <bento-typography>{{ t('deleteViewDescription', { label: computedViewToBeDeletedLabel }) }}</bento-typography> </div> </template> </bento-modal> </div> </template> <script setup lang="ts"> import { BentoButton, BentoToggleButton } from '@/components/button'; import { BentoCheckbox } from '@/components/checkbox'; import { BentoMenu, type BentoMenuItem } from '@/components/menu'; import BentoModal from '@/components/modal/modal.vue'; import { BentoPopover } from '@/components/popover'; import { BentoTypography } from '@/components/typography'; import { HeaderMeta } from '@/internal'; import { useBentoToastController, useClickOutside, useHasSlot } from '@/composables'; import { generateUid } from '@/core/utils/ts'; import ButtonActionsWithMenu from '@/internal/button-actions-with-menu/button-actions-with-menu.vue'; import { InfoIconWithPopover } from '@/internal/info-icon-with-popover'; import { useI18n } from '@/utils/ts/i18n'; import BinIcon from '@adyen/ui-assets-icons-16/vue/bin'; import DotIcon from '@adyen/ui-assets-icons-16/vue/dot'; import DownloadIcon from '@adyen/ui-assets-icons-16/vue/download'; import EditIcon from '@adyen/ui-assets-icons-16/vue/edit-2'; import HeartIcon from '@adyen/ui-assets-icons-16/vue/heart'; import OptionsHorizontalIcon from '@adyen/ui-assets-icons-16/vue/options-horizontal'; import PlusIcon from '@adyen/ui-assets-icons-16/vue/plus'; import PlusMultipleIcon from '@adyen/ui-assets-icons-16/vue/plus-multiple'; import RefreshIcon from '@adyen/ui-assets-icons-16/vue/refresh'; import { useElementSize } from '@vueuse/core'; import { computed, nextTick, onMounted, ref, useSlots, watch } from 'vue'; import { HeaderWithViewsAllViews, HeaderWithViewsEditForm } from './components'; import { useEditViews, useResponsiveViews } from './composables'; import { type BentoHeaderWithViewsEmits, type BentoHeaderWithViewsItem, BentoHeaderWithViewsItemIconElement, type BentoHeaderWithViewsProps, } from './header-with-views.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoHeaderWithViewsProps>(), { actions: () => [], additionalViewSetting: null, title: null, description: null, disableCustomization: false, lastUpdated: null, reloadButton: null, infoIconTooltipText: null, views: () => [], disableAutoSelect: false, activeViewIndex: undefined, }); const emit = defineEmits<BentoHeaderWithViewsEmits>(); const STACKED_LAYOUT_BREAKPOINT = 573; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const slots = useSlots(); const hasSlot = useHasSlot(slots); // refs const createViewButtonRef = ref(null); const createViewPopoverRef = ref(null); const editViewPopoverRef = ref(null); const editViewMenuRefs = ref([]); const editViewTargetElementRef = ref(null); // used as a mount for the edit form popover const hostRef = ref(null); const viewsRef = ref<HTMLElement>(null); // container for all views const viewRefs = ref<Array<HTMLDivElement>>([]); // individual view const viewControlRef = ref(null); // all views menu and create view button container const viewControlAllViewsRef = ref(null); // all views menu // controls const showCreateViewPopover = ref<boolean>(false); // control the create view popover const showAllViewsPopover = ref<boolean>(false); // control when menu button is shown const showUnsavedModalWarning = ref<boolean>(false); const showDeleteModalWarning = ref<boolean>(false); const activeEditViewPopover = ref<number>(null); // control the edit view popover const activeViewMenuPopover = ref<number>(null); // control when menu items are shown const internalActiveView = ref<number>(null); // control which view the user is currently on const nextActiveView = ref<number>(null); // control which view is does the user want to navigate to, used for caching unsaved view navigation const viewToBeDeleted = ref<number>(null); // cache which view is marked for deletion // Computed property to handle two-way binding for activeViewIndex const currentActiveView = computed({ get: () => internalActiveView.value, set: (value: number | null) => { internalActiveView.value = value; emit('update:active-view-index', value); }, }); // models const skipUnsavedChangesReminder = ref<boolean>(false); const createViewPopoverId = generateUid('createViewPopover'); const editViewPopoverId = generateUid('editViewPopover'); useResponsiveViews({ container: viewsRef, children: viewRefs, offset: viewControlRef, activeViewIndex: currentActiveView, }); const { computedViews, clearError, duplicateView, errors, setViews, updateView, validateLabel } = useEditViews({ additionalViewSetting: props.additionalViewSetting, views: props.views, }); const hideCreateViewPopover = async (updateFocus = true) => { showCreateViewPopover.value = false; if (updateFocus) { await nextTick(); createViewButtonRef.value.$el.focus(); } clearError(); }; const hideEditViewPopover = () => { const currentMenuRef = showAllViewsPopover.value ? viewControlAllViewsRef.value.$refs.editViewMenuRefs[activeEditViewPopover.value] : editViewMenuRefs.value[0]; currentMenuRef?.closeMenu(); }; useClickOutside(createViewPopoverRef, () => hideCreateViewPopover(false), { ignore: [createViewButtonRef], }); useClickOutside(editViewPopoverRef, () => setActiveEditViewPopover(), { ignore: [editViewTargetElementRef], }); const { width: hostWidth } = useElementSize(hostRef); const { addToast } = useBentoToastController(); const computedUnsavedViewModalActions = computed(() => [ { title: t('saveChanges'), event: handleSaveChanges, }, { title: t('leave'), event: handleRestoreChanges, }, ]); const computedDeleteViewModalActions = computed(() => [ { title: t('deleteViewAction'), event: async () => { showDeleteModalWarning.value = false; const updatedViewObj = updateView(null, viewToBeDeleted.value); emit('delete:view', updatedViewObj); hideViewMenuPopover(); // if a view that is placed higher in the order is deleted, ignore if (currentActiveView.value < viewToBeDeleted.value) { return; } // if a view that is placed lower in the order is deleted, // we need to shift the index to maintain the current view if (currentActiveView.value > viewToBeDeleted.value) { currentActiveView.value -= 1; } else if (currentActiveView.value === viewToBeDeleted.value) { if (updatedViewObj.newViews.length === 0) { // there are no more views currentActiveView.value = null; } else if (currentActiveView.value >= updatedViewObj.newViews.length) { // shift it to the left currentActiveView.value -= 1; } // else, keep it at the same index } viewToBeDeleted.value = null; }, }, { title: t('cancel'), event: () => { showDeleteModalWarning.value = false; }, }, ]); const computedViewToBeDeletedLabel = computed(() => computedViews.value[viewToBeDeleted.value]?.label); const hasMetaSection = computed(() => props.description || hasSlot('description') || props.lastUpdated); const isCompact = computed(() => hostWidth.value <= STACKED_LAYOUT_BREAKPOINT); const conditionalContainerClass = computed(() => ({ 'b-header-with-views__container--is-compact': isCompact.value, 'b-header-with-views__container--has-meta': hasMetaSection.value, })); watch([currentActiveView, computedViews], ([newIndex, newViews], [oldIndex, oldViews]) => { // skip if the labels are the same or no active view const isViewUpdated = newIndex === oldIndex && newViews.length === oldViews.length; const isViewDeletedWithChangedIndex = newViews[newIndex]?.label === oldViews[oldIndex]?.label; if (isViewUpdated || isViewDeletedWithChangedIndex) { return; } // allow empty label // occurs when the user removes all views emit('select:view', newViews[newIndex]?.label ?? null); }); // whenever the view menu popover changes, we reset the edit popover because it is never opened by default watch(activeViewMenuPopover, () => { setActiveEditViewPopover(); }); watch(showAllViewsPopover, value => { if (value) { return; } // if show all view popover is closed, close children as well hideViewMenuPopover(); }); // set the popover to open next to the edit button watch(activeEditViewPopover, value => { const editButtonIndex = isViewUnsaved(value) ? 2 : 0; // EDGE CASE: // Since edit button can be clicked while in all views popover or while it is in the bar, // we will not support the case where edit popover is opened and the screen is resize until the view menu is hidden. // Edit popover will still be displayed but the position does not have to follow the edit button as it is hidden. if (showAllViewsPopover.value) { const menuListItemRef = viewControlAllViewsRef.value.$refs.editViewMenuRefs[activeViewMenuPopover.value]?.$refs ?.menuItemListRef; editViewTargetElementRef.value = viewControlAllViewsRef.value.$refs.editViewMenuRefs[activeViewMenuPopover.value]?.$refs?.menuItemListRef ?.$refs?.menuItemRef?.[editButtonIndex] ?? null; if (!value) { menuListItemRef?.focusItem(); clearError(); } return; } const menuListItemRef = editViewMenuRefs.value.find(ref => ref.$refs.menuItemListRef)?.$refs?.menuItemListRef; editViewTargetElementRef.value = menuListItemRef?.$refs?.menuItemRef?.[editButtonIndex] ?? null; if (!value) { menuListItemRef?.focusItem(); clearError(); } }); // Watch for external changes to the activeViewIndex prop watch( () => props.activeViewIndex, newValue => { if (newValue !== undefined && newValue >= 0 && newValue < computedViews.value.length) { currentActiveView.value = newValue; } } ); onMounted(() => { if ( props.activeViewIndex !== undefined && props.activeViewIndex >= 0 && props.activeViewIndex < computedViews.value.length ) { currentActiveView.value = props.activeViewIndex; } else if (!props.disableAutoSelect) { const defaultView = computedViews.value.findIndex(view => view.isDefault); currentActiveView.value = defaultView >= 0 ? defaultView : 0; } }); const getViewMenuData = (index: number): Array<BentoMenuItem> => [ ...(isViewUnsaved(index) ? [ { text: t('saveChanges'), handler: handleSaveChanges, icon: DownloadIcon, }, { addDivider: true, text: t('restoreChanges'), handler: handleRestoreChanges, icon: RefreshIcon, }, ] : []), ...(!computedViews.value[index]?.isFixed ? [ { text: t('edit'), handler: () => { setActiveEditViewPopover(activeViewMenuPopover.value); }, icon: EditIcon, skipClose: true, }, ] : []), { text: t('setAsDefault'), handler: () => { const updatedViewObj = updateView({ isDefault: true }, activeViewMenuPopover.value); emit('update:view', updatedViewObj); addToast({ text: t('setAsDefaultToast', { label: updatedViewObj.targetViewLabel }), }); hideViewMenuPopover(); }, icon: HeartIcon, }, { text: t('duplicate'), handler: () => { const updatedViewObj = duplicateView(activeViewMenuPopover.value); emit('duplicate:view', updatedViewObj); hideViewMenuPopover(); // switch to newly duplicated view handleViewSelect(updatedViewObj.newViews.length - 1); }, icon: PlusMultipleIcon, }, ...(!computedViews.value[index]?.isFixed ? [ { text: t('delete'), handler: () => { viewToBeDeleted.value = activeViewMenuPopover.value; showDeleteModalWarning.value = true; }, critical: true, icon: BinIcon, }, ] : []), ]; const showViewMenuPopover = (index: number) => { activeViewMenuPopover.value = index; }; const hideViewMenuPopover = () => { activeViewMenuPopover.value = null; setActiveEditViewPopover(); }; const setActiveEditViewPopover = (index?: number) => { if (index === activeEditViewPopover.value) { activeEditViewPopover.value = null; } else { activeEditViewPopover.value = index ?? null; } }; const handleCreateViewItem = (value: BentoHeaderWithViewsItem) => { const updatedViewObj = updateView(value); emit('add:view', updatedViewObj); hideCreateViewPopover(); // switch to new view on add handleViewSelect(updatedViewObj.newViews.findIndex(view => updatedViewObj.targetViewLabel === view.label)); addToast({ text: t('createViewToast', { label: updatedViewObj.targetViewLabel }), }); }; const handleUpdateViewItem = (value: BentoHeaderWithViewsItem) => { emit('update:view', updateView(value, activeEditViewPopover.value)); hideEditViewPopover(); }; const handleKeydownEnterViewItem = (value: BentoHeaderWithViewsItem) => { validateLabel(value.label, activeViewMenuPopover.value); if (Object.values(errors).some(error => error.length > 0)) { return; } if (activeEditViewPopover.value) { handleUpdateViewItem(value); return; } handleCreateViewItem(value); }; const handleViewSelect = (index: number) => { // If there's no currently active view, or if there are no unsaved changes, // just switch to the new view. if (currentActiveView.value === null || !props.hasChanges || props.skipUnsavedChangesText === null) { // If switching from an existing view, emit restore event. if (currentActiveView.value !== null) { emit('restore:view', computedViews.value[currentActiveView.value].label); } currentActiveView.value = index; return; } nextActiveView.value = index; showUnsavedModalWarning.value = true; }; const handleReorder = (reorderedViews: Array<BentoHeaderWithViewsItem>) => { const currentActiveLabel = currentActiveView.value !== null ? computedViews.value[currentActiveView.value]?.label : null; const result = setViews(reorderedViews); if (currentActiveLabel) { const newIndex = result.newViews.findIndex(view => view.label === currentActiveLabel); if (newIndex !== -1) { currentActiveView.value = newIndex; } } emit('reorder:view', result); }; const handleClickModal = (event: MouseEvent) => { if ((event.target as HTMLElement).closest('button')) { return; } event.stopPropagation(); event.preventDefault(); }; const handleTabEditViewPopover = () => { // skip built-in refocus on the menu button when menu is closed // enables tab to flow as intended by HTML spec editViewMenuRefs.value[0].closeMenu(false); }; const handleSaveChanges = () => { const label = computedViews.value[currentActiveView.value].label; emit('save:view', label); if (showUnsavedModalWarning.value) { emit('update:skip-unsaved-changes', skipUnsavedChangesReminder.value); // fired when user saves the view while navigating to a different tab currentActiveView.value = nextActiveView.value; nextActiveView.value = null; showUnsavedModalWarning.value = false; } else { // fired when user saves the view via the menu hideViewMenuPopover(); } addToast({ text: t('saveChangesToast', { label }), action: { handler: () => emit('restore:view', label), text: t('undo'), }, }); }; const handleRestoreChanges = () => { emit('restore:view', computedViews.value[currentActiveView.value].label); if (showUnsavedModalWarning.value) { emit('update:skip-unsaved-changes', skipUnsavedChangesReminder.value); // fired when user restore the view while navigating to a different tab currentActiveView.value = nextActiveView.value; nextActiveView.value = null; showUnsavedModalWarning.value = false; } else { // fired when user restore the view via the menu hideViewMenuPopover(); } }; const isViewActive = (index: number) => currentActiveView.value === index; const isViewUnsaved = (index: number) => isViewActive(index) && props.hasChanges; </script> <script lang="ts"> /** * Header with views allow users to save and quickly retrieve configurations tailored to specific tasks. * For example, managing daily transactions, auditing, or analyzing specific data sets. * It also allows the user to define the columns, filters, and search queries they need. * * @example * import { BentoHeaderWithViews } from '@adyen/bento-vue2'; * * export default { * components: { BentoHeaderWithViews }, * template: ` * <bento-header-with-views * :actions="[ * { title: 'Primary action', event: () => undefined }, * { title: 'Secondary action', event: () => undefined }, * { title: 'Tertiary action', event: () => undefined }, * { title: 'Another action', event: () => undefined } * ]" * description="Page Description" * title="Page Title" * :views="[ * { icon: 'stack', iconColor: 'blue', label: 'A very long value' }, * { icon: 'stack', iconColor: 'primary', label: 'Title 1' }, * { icon: 'stack', iconColor: 'primary', label: 'Title 2' } * ]" * :additionalViewSetting="{ * label: 'Include date filter (relative)', * value: false * }" * hasChanges * /> * ` * }; */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./header-with-views.scss" />
|
package/dist/assets/components/internal/calendar/composables/use-granularity-adjustments.types.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import { type Day } from 'date-fns'; export type
|
|
1
|
+
import { type Day } from 'date-fns'; import type { CalendarGranularityConfig } from '../calendar.types'; /** Identifies one endpoint of a date range. */ export type BentoRangeEndpoint = 'start' | 'end'; /** Options for applying every date-range constraint in a consistent order. */ export interface BentoNormalizeRangeOptions { /** Requested start of the range. */ startDate: Date; /** Requested end of the range. */ endDate: Date; /** Endpoint that must stay unchanged if the range needs to be shortened. */ fixedEndpoint: BentoRangeEndpoint; /** Active granularity, including an optional granularity-specific maximum range. */ granularity?: CalendarGranularityConfig; /** Whether to expand the range to complete granularity units. */ alignToGranularity?: boolean; /** Maximum range used when the active granularity does not define one. */ fallbackMaxRange?: number; /** Earliest permitted date. */ min?: Date; /** Latest permitted date. */ max?: Date; } /** Date operations needed to measure and shorten a range in one granularity unit. */ export interface BentoGranularityClampConfig { /** Counts how many units the range spans. */ getCount: (end: Date, start: Date) => number; /** Moves a date backwards by the specified number of units. */ sub: (date: Date, amount: number) => Date; /** Moves a date forwards by the specified number of units. */ add: (date: Date, amount: number) => Date; } /** Functions that normalize complete ranges. */ export type UseGranularityAdjustments = (firstDayOfWeek: Day) => { normalizeRange: (options: BentoNormalizeRangeOptions) => { startDate: Date; endDate: Date }; };
|
|
@@ -106,13 +106,14 @@ menu supports.
|
|
|
106
106
|
|
|
107
107
|
### Roles, states and properties
|
|
108
108
|
|
|
109
|
-
| ARIA attribute | is automatic? | fallback used
|
|
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
|
|
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" />
|