@adyen/bento-mcp 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +11 -66
- package/dist/assets/components/action-bar/action-bar.stories.ts +1 -1
- package/dist/assets/components/action-bar/action-bar.vue +1 -1
- package/dist/assets/components/ai-tag/ai-tag.docs.mdx +75 -0
- package/dist/assets/components/ai-tag/ai-tag.stories.ts +1 -0
- package/dist/assets/components/ai-tag/ai-tag.vue +1 -0
- package/dist/assets/components/anchor-scroller/anchor-scroller.docs.mdx +55 -0
- package/dist/assets/components/anchor-scroller/anchor-scroller.stories.ts +1 -0
- package/dist/assets/components/anchor-scroller/anchor-scroller.vue +1 -0
- package/dist/assets/components/anchor-scroller/components/anchor-scroller-list/anchor-scroller-list.vue +1 -0
- package/dist/assets/components/card/card.docs.mdx +3 -3
- package/dist/assets/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.vue +1 -1
- package/dist/assets/components/data-grid/components/data-grid-tag-with-text-cell/data-grid-tag-with-text-cell.docs.mdx +35 -0
- package/dist/assets/components/data-grid/components/data-grid-tag-with-text-cell/data-grid-tag-with-text-cell.stories.ts +1 -0
- package/dist/assets/components/data-grid/components/data-grid-tag-with-text-cell/data-grid-tag-with-text-cell.vue +1 -0
- package/dist/assets/components/data-grid/data-grid.docs.mdx +29 -9
- package/dist/assets/components/data-grid/data-grid.stories.ts +1 -1
- package/dist/assets/components/data-grid/data-grid.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.vue +1 -1
- package/dist/assets/components/draggable-handle/draggable-handle.docs.mdx +61 -0
- package/dist/assets/components/draggable-handle/draggable-handle.stories.ts +1 -0
- package/dist/assets/components/draggable-handle/draggable-handle.vue +1 -0
- package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-default-textbox/dropdown-default-textbox.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-small-textbox/dropdown-small-textbox.vue +1 -1
- package/dist/assets/components/dropdown/dropdown.docs.mdx +3 -3
- package/dist/assets/components/dropdown/dropdown.vue +1 -1
- package/dist/assets/components/file-uploader/components/file-uploader-file-card/file-uploader-file-card.vue +1 -1
- package/dist/assets/components/file-uploader/file-uploader.vue +1 -1
- package/dist/assets/components/filter-bar/components/date-range-filter/date-range-filter.vue +1 -1
- package/dist/assets/components/filter-bar/filter-bar.docs.mdx +1 -1
- package/dist/assets/components/form-layout/components/form-layout-title/form-layout-title.vue +1 -1
- package/dist/assets/components/form-layout/form-layout.docs.mdx +2 -2
- package/dist/assets/components/inspector/inspector.docs.mdx +2 -2
- package/dist/assets/components/internal/copy/copy.docs.mdx +2 -2
- package/dist/assets/components/internal/dialog-page/dialog-page.vue +1 -1
- package/dist/assets/components/internal/fixed-scroller/fixed-scroller.vue +1 -1
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/listbox-multi-select-options.vue +1 -1
- package/dist/assets/components/layout/layout.docs.mdx +8 -8
- package/dist/assets/components/layout/layout.vue +1 -1
- package/dist/assets/components/modal/components/base-modal/base-modal.vue +1 -1
- package/dist/assets/components/modal/modal.docs.mdx +4 -4
- package/dist/assets/components/modal-fullscreen/modal-fullscreen.docs.mdx +5 -5
- package/dist/assets/components/modal-fullscreen/modal-fullscreen.vue +1 -1
- package/dist/assets/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue +1 -1
- package/dist/assets/components/sidepanel/sidepanel.docs.mdx +27 -27
- package/dist/assets/components/sidepanel/sidepanel.vue +1 -1
- package/dist/assets/components/stepper/stepper.docs.mdx +8 -1
- package/dist/assets/components/stepper/stepper.stories.ts +1 -1
- package/dist/assets/components/summary-grid/summary-grid.docs.mdx +1 -1
- package/dist/assets/components/tabs/tabs.docs.mdx +1 -1
- package/dist/assets/components/toast/toast.docs.mdx +10 -10
- package/dist/assets/index.ts +1 -1
- package/dist/main.js +501 -427
- package/package.json +5 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
<!-- TODO: Remove aria props as these are part of JSX elements /vue/types/jsx.d.ts --> <!-- eslint-disable vue/attribute-hyphenation --> <template> <div v-bento-click-outside-directive="clickOutsideDropdown" class="b-dropdown" :class="conditionalClasses"> <field-label v-if="label" :id="labelId" :condensed="condensed" :label="label" :tooltip-text="tooltipText" :optional="optional" :required="required" /> <div ref="inputContainerRefWrapper" v-bento-keyboard-navigation-directive v-on="textboxKeyboardNavigationListeners" > <component :is="computedSize" ref="inputContainerRef" :condensed="condensed" :value="searchTerm" :additional-items-selected="additionalItemsSelected" :aria-controls="dropdownOptionsContainerId" :ariaExpanded="isDropdownOpen" :aria-labelledby="label ? labelId : null" :aria-describedby="ariaDescribedBy" :aria-label="computedAriaLabel" :aria-readonly="isReadOnly" :readonly="isReadOnly" :required="required" :disabled="disabled" :display-value="displayValue" :dynamic-filtering="dynamicFiltering" :is-invalid="!!errorMessage || error" :multiple="multiple" :open="isDropdownOpen" :selected-value-item="!isInputComponent ? selectedValueItem : undefined" :show-slot-content-in-multiple="showSlotContentInMultiple" @input="searchTerm = $event" @open="toggleDropdown" @close="toggleDropdown" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </component> </div> <bento-dropdown-options-container v-if="inputContainerRef" :id="dropdownOptionsContainerId" ref="optionsContainerRef" :aria-label="computedAriaLabel" :component-loading="componentLoading" :empty-state="emptyState" :debounce-time="search?.debounceTime" :disabled="disabled" :is-option-disabled="isOptionDisabled" :items="filteredItems" :multiple="multiple" :static-categories="staticCategories" :open="isDropdownOpen" :selected-value="cachedSelectedListboxOptions" :searching="isSearching" :target-element="inputContainerRefWrapper" :loading="loading" :has-more-items="hasMoreItems" :lazy-load-type="lazyLoadType" :virtual-scroll="virtualScroll" v-on="listboxListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-dropdown-options-container> <error-message v-if="!!errorMessage" :id="errorId" :error-message="errorMessage" class="b-dropdown__error-message" /> <bento-typography v-if="description || hasSlot('description')" :id="descriptionId" class="b-dropdown__description" :class="descriptionConditionalClasses" el="span" > <slot name="description"> {{ description }} </slot> </bento-typography> </div> </template> <script setup lang="ts"> import { computed, defineComponent, inject, nextTick, type Ref, ref, toRef, toRefs, useAttrs, useSlots, watch, } from 'vue'; // Components import { BentoDropdownOptionsContainer } from './components/dropdown-options-container'; import { BentoTypography } from '@/components/typography'; import { DropdownInputDefault, DropdownInputSmall } from './components'; import { BentoListbox, ErrorMessage, FieldLabel, useCachedSelectedValues, useMultiLevelItems } from '@/internal'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { generateUid } from '@/core/utils/ts'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; // Composables import { useDisplayValue, useListboxKeyboardNavigation, useTextboxKeyboardNavigation } from './composables'; import { useVisibleDomOptions } from '@/internal/listbox/composables/use-visible-dom-options/use-visible-dom-options'; import { useFormLayoutFieldLoading } from '@/composables/use-form-layout-loading/use-form-layout-loading'; import { useSearchBarFilter } from '@/components/search-bar/useSearchBarDataFilter'; // Directives import { BentoClickOutsideDirective as vBentoClickOutsideDirective } from '@/directives/click-outside'; import { BentoKeyboardNavigationDirective as vBentoKeyboardNavigationDirective } from '@/directives'; // Types import { BentoDropdownEvent, type BentoDropdownProps, BentoDropdownSize } from './dropdown.types'; import { BentoListboxEvent, type BentoListboxItemRole, type BentoListboxMultiSelectValue, type BentoListboxOptions, type BentoListboxSelectedValue, type BentoListboxSelectedValueLabelPair, } from '@/types/listbox'; import messages from './messages.json'; import { useHasSlot } from '@/composables'; import { INPUT_FIELD_COMPONENT_INJECTION_KEY } from '@/components/input-field/input-field.keys'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = withDefaults(defineProps<BentoDropdownProps>(), { ariaLabel: null, condensed: false, emptyState: undefined, disabled: false, dynamicFiltering: false, error: false, errorMessage: null, description: null, hasMoreItems: false, isOptionDisabled: undefined, items: () => [], label: null, loading: false, componentLoading: false, lazyLoadType: BentoListbox.props.lazyLoadType.default, multiple: false, optional: false, placeholder: null, readonly: false, required: false, search: undefined, showSlotContentInMultiple: false, size: null, staticCategories: false, tooltipText: null, value: null, modelValue: null, virtualScroll: false, enableValueLabelPair: false, }); const emit = defineEmits<{ /** * Emitted when an option is clicked. Updates the selected option from the "value" property linked to the v-model. * @deprecated since version 2.0. Use `v-model` or `update:model-value instead. */ (e: 'input', selectedValue: BentoListboxSelectedValue | any); // eslint-disable-line @typescript-eslint/no-explicit-any /** * Emitted when an option is clicked. Updates the selected option from the "modelValue" property linked to the v-model. */ (e: 'update:model-value', selectedValue: BentoListboxSelectedValue | any); // eslint-disable-line @typescript-eslint/no-explicit-any /** * Emitted when: * - `lazyLoad` is `automatic` and the scroll meets the end of the list * - `lazyload` is `button` and the button is clicked */ (e: 'show-more'); /** * Emitted when the dropdown is opened. */ (e: 'open'); }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const { emitValue } = useFormFieldEmits<BentoListboxSelectedValue>(emit); const inputContainerRef = ref(null); const inputContainerRefWrapper = ref(null); const optionsContainerRef: Ref<InstanceType<typeof BentoDropdownOptionsContainer>> = ref(null); const isDropdownOpen = ref(false); const dropdownOptionsContainerId = generateUid('bento-dropdown-options-container'); const labelId = generateUid('dropdown-label'); const descriptionId = generateUid('dropdown-description'); const errorId = generateUid('dropdown-error'); const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const dropdownValue = computed(() => props.modelValue ?? props.value); const { cachedSelectedListboxOptions, cachedListboxSelectedValues, setCachedValues } = useCachedSelectedValues( toRef(props, 'items'), dropdownValue, toRef(props, 'multiple'), toRef(props, 'enableValueLabelPair') ); const ariaLabelAttribute = ref(attrs['aria-label']); const ariaDescribedByAttribute = ref(attrs['aria-describedby']); const { ariaLabel, label } = toRefs(props); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => t('ariaLabelFallback')); const computedAriaLabel = useAriaLabel({ // TODO: take ariaLabel ref away when aria-label prop is removed ariaLabel: ariaLabel.value || (ariaLabelAttribute as Ref<string>), label, defaultFallback: computedAriaLabelFallbackMessage, }); // Inject input field component key to change the dropdown size const isInputComponent = props.size === BentoDropdownSize.SMALL || inject(INPUT_FIELD_COMPONENT_INJECTION_KEY, false); const computedSize = computed(() => (isInputComponent ? BentoDropdownSize.SMALL : BentoDropdownSize.DEFAULT)); const conditionalClasses = computed(() => ({ 'b-dropdown--condensed': props.condensed, })); // Description styles const descriptionConditionalClasses = computed(() => ({ 'b-dropdown__description--error': !!props.errorMessage, })); // Filtering const searchTerm = ref(''); const items = toRef(props, 'items'); const staticCategories = toRef(props, 'staticCategories'); const filteredItems = useSearchBarFilter(searchTerm, items, props?.search?.searchEvent, staticCategories); const isSearching = computed(() => props.dynamicFiltering && !!searchTerm.value); const displayValue = useDisplayValue({ items, placeholder: toRef(props, 'placeholder'), isMultiSelect: toRef(props, 'multiple'), selectedValues: cachedSelectedListboxOptions, }); const selectedValueItem = computed(() => { if (props?.multiple && Array.isArray(dropdownValue.value)) { return props.items.filter( ({ value }) => props?.value && (props?.value as BentoListboxMultiSelectValue).includes(value) ); } if (!props.multiple && props.staticCategories) { // Find the category that holds the selected value const category = props.items.find(categoryItem => categoryItem.items.some(item => item.value === dropdownValue.value) ); // Find the specific item within the category const selectedCategoryItem = category ? category.items.find(item => item.value === dropdownValue.value) : null; return selectedCategoryItem; } return props.items.find(({ value }) => value === dropdownValue.value); }); const selectedValueIndex = computed(() => { const index = filteredItems.value.findIndex(({ value }) => { return props.enableValueLabelPair ? (dropdownValue.value as BentoListboxSelectedValueLabelPair).value === value : dropdownValue.value === value; }); return index > -1 ? index : 0; }); const { isMultiLevelSelect, flattenedItemsList } = useMultiLevelItems(items); const isTreePattern = computed(() => isMultiLevelSelect.value && !props.staticCategories); const computedOptionRole = computed<BentoListboxItemRole>(() => (isTreePattern.value ? 'treeitem' : 'option')); const additionalItemsSelected = computed(() => { if (props.multiple) { const selectedItemsCount = Array.isArray(dropdownValue.value) ? dropdownValue.value.length : 0; const total = isMultiLevelSelect.value ? flattenedItemsList.value.length : items.value.length; if (selectedItemsCount === 0) { return null; } // Display count always - show "all" only when is not external filtering (hasMoreItems = true) if (!props.hasMoreItems && total > 0 && total === selectedItemsCount) { return t('all'); } return selectedItemsCount; } return null; }); // ARIA const ariaDescribedBy = computed( () => [ props.description ? descriptionId : null, ariaDescribedByAttribute.value, props.errorMessage ? errorId : null, ] .filter(Boolean) .join(' ') || null ); const listboxRef = computed(() => optionsContainerRef.value?.$el as HTMLDivElement); const { visibleDomOptions, updateVisibleDomOptions } = useVisibleDomOptions( listboxRef, filteredItems, computedOptionRole ); const openDropdown = () => { if (isReadOnly.value) { return; } isDropdownOpen.value = true; nextTick(() => { updateVisibleDomOptions(); }); }; const closeDropdown = () => { isDropdownOpen.value = false; }; const toggleDropdown = () => { if (isReadOnly.value) { return; } isDropdownOpen.value = !isDropdownOpen.value; nextTick(() => { updateVisibleDomOptions(); }); // Clear search if (isSearching.value) { searchTerm.value = ''; } }; const clickOutsideDropdown = () => { if (isSearching.value) { searchTerm.value = ''; } if (isDropdownOpen.value) { isDropdownOpen.value = false; // Reset the internal dropdown options state optionsContainerRef.value.onOutsideDropdownClick(); } }; const onOptionSelected = (selectedValue: BentoListboxOptions) => { // Clear and close the dropdown on single selection if (!props.multiple) { searchTerm.value = ''; isDropdownOpen.value = false; focus(); } setCachedValues(selectedValue); /** * Emit the value/label pair objects if enableValueLabelPair is enabled, * otherwise just emit the list of selected values (number or string) */ emitValue(cachedListboxSelectedValues.value); }; const emitShowMoreEvent = () => { emit(BentoDropdownEvent.SHOW_MORE); }; const textboxKeyboardNavigationListeners = useTextboxKeyboardNavigation( { inputContainerRef, optionsContainerRef, selectedValueIndex, isDropdownOpen, isDynamicFltering: toRef(props, 'dynamicFiltering'), items: visibleDomOptions, isMultiple: props.multiple, }, toggleDropdown, openDropdown, closeDropdown ); const listboxKeyboardNavigationListeners = useListboxKeyboardNavigation( { inputContainerRef, isDropdownOpen, }, closeDropdown ); const listboxListeners = { [BentoListboxEvent.CLOSE_DROPDOWN]: closeDropdown, [BentoListboxEvent.SELECT]: onOptionSelected, [BentoListboxEvent.SHOW_MORE]: emitShowMoreEvent, ...listboxKeyboardNavigationListeners, }; if (props.error) { deprecate( 'BentoDropdown "error" property', `Use the BentoDropdown "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-dropdown errorMessage="Error message text" />`, '2.0.0' ); } if (props.size) { deprecate('BentoDropdown "size" property', `Do not use. Only 'default' size should be used.`, '2.0.0'); } if (props.value) { deprecate( 'BentoDropdown "value" property', `The use of "value" prop in "BentoDropdown" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } const focus = () => { inputContainerRef.value.focus(); }; watch( () => isDropdownOpen.value, (isOpen: boolean) => { if (isOpen) { emit('open'); } } ); defineExpose({ focus, }); </script> <script lang="ts"> /** * A Dropdown shows a selected option's. * Use a dropdown when you want users to select options * from a list of pre-defined options. * * @example * import { BentoDropdown } from '@adyen/bento-vue2'; * import type { BentoDropdownOptions } from '@adyen/bento-vue2' * * export default { * components: { BentoDropdown }, * template: ` * <bento-dropdown * v-model="selectedValue" * :disabled="false" * :multiple="true" * :isOptionsDisabled="option => option.value === 2" * :items="options" * /> * `, * setup() { * const selectedValue = ref(2) // Default value * const options: BentoDropdownOptions = [ * { label: 'Option 1', value: 1 }, * { label: 'Option 2', value: 2 }, * ]; * return { options, selectedValue }; * } * } */ export default defineComponent({ i18n: { messages }, name: 'b-dropdown', components: { DropdownInputDefault, DropdownInputSmall, }, model: { prop: 'modelValue' }, }); </script> <style lang="scss" scoped src="./dropdown.scss" />
|
|
1
|
+
<!-- TODO: Remove aria props as these are part of JSX elements /vue/types/jsx.d.ts --> <!-- eslint-disable vue/attribute-hyphenation --> <template> <div v-bento-click-outside-directive="clickOutsideDropdown" class="b-dropdown" :class="conditionalClasses"> <field-label v-if="label" :id="labelId" :condensed="condensed" :label="label" :tooltip-text="tooltipText" :optional="optional" :required="required" /> <div ref="inputContainerRefWrapper" v-bento-keyboard-navigation-directive v-on="textboxKeyboardNavigationListeners" > <component :is="computedSize" ref="inputContainerRef" :condensed="condensed" :value="searchTerm" :additional-items-selected="additionalItemsSelected" :aria-controls="dropdownOptionsContainerId" :ariaExpanded="isDropdownOpen" :aria-labelledby="label ? labelId : null" :aria-describedby="ariaDescribedBy" :aria-label="computedAriaLabel" :aria-readonly="isReadOnly" :readonly="isReadOnly" :required="required" :disabled="disabled" :display-value="displayValue" :dynamic-filtering="dynamicFiltering" :is-invalid="!!errorMessage || error" :multiple="multiple" :open="isDropdownOpen" :selected-value-item="!isInputComponent ? selectedValueItem : undefined" :show-slot-content-in-multiple="showSlotContentInMultiple" @input="searchTerm = $event" @open="toggleDropdown" @close="toggleDropdown" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </component> </div> <bento-dropdown-options-container v-if="inputContainerRef" :id="dropdownOptionsContainerId" ref="optionsContainerRef" :aria-label="computedAriaLabel" :component-loading="componentLoading" :empty-state="emptyState" :disabled="disabled" :is-option-disabled="isOptionDisabled" :items="filteredItems" :multiple="multiple" :static-categories="staticCategories" :open="isDropdownOpen" :selected-value="cachedSelectedListboxOptions" :searching="isSearching" :target-element="inputContainerRefWrapper" :loading="loading" :has-more-items="hasMoreItems" :lazy-load-type="lazyLoadType" :virtual-scroll="virtualScroll" v-on="listboxListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-dropdown-options-container> <error-message v-if="!!errorMessage" :id="errorId" :error-message="errorMessage" class="b-dropdown__error-message" /> <bento-typography v-if="description || hasSlot('description')" :id="descriptionId" class="b-dropdown__description" :class="descriptionConditionalClasses" el="span" > <slot name="description"> {{ description }} </slot> </bento-typography> </div> </template> <script setup lang="ts"> import { computed, defineComponent, inject, nextTick, type Ref, ref, toRef, toRefs, useAttrs, useSlots, watch, } from 'vue'; // Components import { BentoDropdownOptionsContainer } from './components/dropdown-options-container'; import { BentoTypography } from '@/components/typography'; import { DropdownInputDefault, DropdownInputSmall } from './components'; import { BentoListbox, ErrorMessage, FieldLabel, useCachedSelectedValues, useMultiLevelItems } from '@/internal'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { generateUid } from '@/core/utils/ts'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; // Composables import { useDisplayValue, useListboxKeyboardNavigation, useTextboxKeyboardNavigation } from './composables'; import { useVisibleDomOptions } from '@/internal/listbox/composables/use-visible-dom-options/use-visible-dom-options'; import { useFormLayoutFieldLoading } from '@/composables/use-form-layout-loading/use-form-layout-loading'; import { useSearchBarFilter } from '@/components/search-bar/useSearchBarDataFilter'; // Directives import { BentoClickOutsideDirective as vBentoClickOutsideDirective } from '@/directives/click-outside'; import { BentoKeyboardNavigationDirective as vBentoKeyboardNavigationDirective } from '@/directives'; // Types import { BentoDropdownEvent, type BentoDropdownProps, BentoDropdownSize } from './dropdown.types'; import { BentoListboxEvent, type BentoListboxItemRole, type BentoListboxMultiSelectValue, type BentoListboxOptions, type BentoListboxSelectedValue, type BentoListboxSelectedValueLabelPair, } from '@/types/listbox'; import messages from './messages.json'; import { useHasSlot } from '@/composables'; import { INPUT_FIELD_COMPONENT_INJECTION_KEY } from '@/components/input-field/input-field.keys'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = withDefaults(defineProps<BentoDropdownProps>(), { ariaLabel: null, condensed: false, emptyState: undefined, disabled: false, dynamicFiltering: false, error: false, errorMessage: null, description: null, hasMoreItems: false, isOptionDisabled: undefined, items: () => [], label: null, loading: false, componentLoading: false, lazyLoadType: BentoListbox.props.lazyLoadType.default, multiple: false, optional: false, placeholder: null, readonly: false, required: false, search: undefined, showSlotContentInMultiple: false, size: null, staticCategories: false, tooltipText: null, value: null, modelValue: null, virtualScroll: false, enableValueLabelPair: false, }); const emit = defineEmits<{ /** * Emitted when an option is clicked. Updates the selected option from the "value" property linked to the v-model. * @deprecated since version 2.0. Use `v-model` or `update:model-value instead. */ (e: 'input', selectedValue: BentoListboxSelectedValue | any); // eslint-disable-line @typescript-eslint/no-explicit-any /** * Emitted when an option is clicked. Updates the selected option from the "modelValue" property linked to the v-model. */ (e: 'update:model-value', selectedValue: BentoListboxSelectedValue | any); // eslint-disable-line @typescript-eslint/no-explicit-any /** * Emitted when: * - `lazyLoad` is `automatic` and the scroll meets the end of the list * - `lazyload` is `button` and the button is clicked */ (e: 'show-more'); /** * Emitted when the dropdown is opened. */ (e: 'open'); }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const { emitValue } = useFormFieldEmits<BentoListboxSelectedValue>(emit); const inputContainerRef = ref(null); const inputContainerRefWrapper = ref(null); const optionsContainerRef: Ref<InstanceType<typeof BentoDropdownOptionsContainer>> = ref(null); const isDropdownOpen = ref(false); const dropdownOptionsContainerId = generateUid('bento-dropdown-options-container'); const labelId = generateUid('dropdown-label'); const descriptionId = generateUid('dropdown-description'); const errorId = generateUid('dropdown-error'); const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const dropdownValue = computed(() => props.modelValue ?? props.value); const { cachedSelectedListboxOptions, cachedListboxSelectedValues, setCachedValues } = useCachedSelectedValues( toRef(props, 'items'), dropdownValue, toRef(props, 'multiple'), toRef(props, 'enableValueLabelPair') ); const ariaLabelAttribute = ref(attrs['aria-label']); const ariaDescribedByAttribute = ref(attrs['aria-describedby']); const { ariaLabel, label } = toRefs(props); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => t('ariaLabelFallback')); const computedAriaLabel = useAriaLabel({ // TODO: take ariaLabel ref away when aria-label prop is removed ariaLabel: ariaLabel.value || (ariaLabelAttribute as Ref<string>), label, defaultFallback: computedAriaLabelFallbackMessage, }); // Inject input field component key to change the dropdown size const isInputComponent = props.size === BentoDropdownSize.SMALL || inject(INPUT_FIELD_COMPONENT_INJECTION_KEY, false); const computedSize = computed(() => (isInputComponent ? BentoDropdownSize.SMALL : BentoDropdownSize.DEFAULT)); const conditionalClasses = computed(() => ({ 'b-dropdown--condensed': props.condensed, })); // Description styles const descriptionConditionalClasses = computed(() => ({ 'b-dropdown__description--error': !!props.errorMessage, })); // Filtering const searchTerm = ref(''); const items = toRef(props, 'items'); const staticCategories = toRef(props, 'staticCategories'); const filteredItems = useSearchBarFilter( searchTerm, items, props?.search?.searchEvent, staticCategories, props?.search?.debounceTime ); const isSearching = computed(() => props.dynamicFiltering && !!searchTerm.value); const displayValue = useDisplayValue({ items, placeholder: toRef(props, 'placeholder'), isMultiSelect: toRef(props, 'multiple'), selectedValues: cachedSelectedListboxOptions, }); const selectedValueItem = computed(() => { if (props?.multiple && Array.isArray(dropdownValue.value)) { return props.items.filter( ({ value }) => props?.value && (props?.value as BentoListboxMultiSelectValue).includes(value) ); } if (!props.multiple && props.staticCategories) { // Find the category that holds the selected value const category = props.items.find(categoryItem => categoryItem.items.some(item => item.value === dropdownValue.value) ); // Find the specific item within the category const selectedCategoryItem = category ? category.items.find(item => item.value === dropdownValue.value) : null; return selectedCategoryItem; } return props.items.find(({ value }) => value === dropdownValue.value); }); const selectedValueIndex = computed(() => { const index = filteredItems.value.findIndex(({ value }) => { return props.enableValueLabelPair ? (dropdownValue.value as BentoListboxSelectedValueLabelPair).value === value : dropdownValue.value === value; }); return index > -1 ? index : 0; }); const { isMultiLevelSelect, flattenedItemsList } = useMultiLevelItems(items); const isTreePattern = computed(() => isMultiLevelSelect.value && !props.staticCategories); const computedOptionRole = computed<BentoListboxItemRole>(() => (isTreePattern.value ? 'treeitem' : 'option')); const additionalItemsSelected = computed(() => { if (props.multiple) { const selectedItemsCount = Array.isArray(dropdownValue.value) ? dropdownValue.value.length : 0; const total = isMultiLevelSelect.value ? flattenedItemsList.value.length : items.value.length; if (selectedItemsCount === 0) { return null; } // Display count always - show "all" only when is not external filtering (hasMoreItems = true) if (!props.hasMoreItems && total > 0 && total === selectedItemsCount) { return t('all'); } return selectedItemsCount; } return null; }); // ARIA const ariaDescribedBy = computed( () => [ props.description ? descriptionId : null, ariaDescribedByAttribute.value, props.errorMessage ? errorId : null, ] .filter(Boolean) .join(' ') || null ); const listboxRef = computed(() => optionsContainerRef.value?.$el as HTMLDivElement); const { visibleDomOptions, updateVisibleDomOptions } = useVisibleDomOptions( listboxRef, filteredItems, computedOptionRole ); const openDropdown = () => { if (isReadOnly.value) { return; } isDropdownOpen.value = true; nextTick(() => { updateVisibleDomOptions(); }); }; const closeDropdown = () => { isDropdownOpen.value = false; }; const toggleDropdown = () => { if (isReadOnly.value) { return; } isDropdownOpen.value = !isDropdownOpen.value; nextTick(() => { updateVisibleDomOptions(); }); // Clear search if (isSearching.value) { searchTerm.value = ''; } }; const clickOutsideDropdown = () => { if (isSearching.value) { searchTerm.value = ''; } if (isDropdownOpen.value) { isDropdownOpen.value = false; // Reset the internal dropdown options state optionsContainerRef.value.onOutsideDropdownClick(); } }; const onOptionSelected = (selectedValue: BentoListboxOptions) => { // Clear and close the dropdown on single selection if (!props.multiple) { searchTerm.value = ''; isDropdownOpen.value = false; focus(); } setCachedValues(selectedValue); /** * Emit the value/label pair objects if enableValueLabelPair is enabled, * otherwise just emit the list of selected values (number or string) */ emitValue(cachedListboxSelectedValues.value); }; const emitShowMoreEvent = () => { emit(BentoDropdownEvent.SHOW_MORE); }; const textboxKeyboardNavigationListeners = useTextboxKeyboardNavigation( { inputContainerRef, optionsContainerRef, selectedValueIndex, isDropdownOpen, isDynamicFltering: toRef(props, 'dynamicFiltering'), items: visibleDomOptions, isMultiple: props.multiple, }, toggleDropdown, openDropdown, closeDropdown ); const listboxKeyboardNavigationListeners = useListboxKeyboardNavigation( { inputContainerRef, isDropdownOpen, }, closeDropdown ); const listboxListeners = { [BentoListboxEvent.CLOSE_DROPDOWN]: closeDropdown, [BentoListboxEvent.SELECT]: onOptionSelected, [BentoListboxEvent.SHOW_MORE]: emitShowMoreEvent, ...listboxKeyboardNavigationListeners, }; if (props.error) { deprecate( 'BentoDropdown "error" property', `Use the BentoDropdown "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-dropdown errorMessage="Error message text" />`, '2.0.0' ); } if (props.size) { deprecate('BentoDropdown "size" property', `Do not use. Only 'default' size should be used.`, '2.0.0'); } if (props.value) { deprecate( 'BentoDropdown "value" property', `The use of "value" prop in "BentoDropdown" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } const focus = () => { inputContainerRef.value.focus(); }; watch( () => isDropdownOpen.value, (isOpen: boolean) => { if (isOpen) { emit('open'); } } ); defineExpose({ focus, }); </script> <script lang="ts"> /** * A Dropdown shows a selected option's. * Use a dropdown when you want users to select options * from a list of pre-defined options. * * @example * import { BentoDropdown } from '@adyen/bento-vue2'; * import type { BentoDropdownOptions } from '@adyen/bento-vue2' * * export default { * components: { BentoDropdown }, * template: ` * <bento-dropdown * v-model="selectedValue" * :disabled="false" * :multiple="true" * :isOptionsDisabled="option => option.value === 2" * :items="options" * /> * `, * setup() { * const selectedValue = ref(2) // Default value * const options: BentoDropdownOptions = [ * { label: 'Option 1', value: 1 }, * { label: 'Option 2', value: 2 }, * ]; * return { options, selectedValue }; * } * } */ export default defineComponent({ i18n: { messages }, name: 'b-dropdown', components: { DropdownInputDefault, DropdownInputSmall, }, model: { prop: 'modelValue' }, }); </script> <style lang="scss" scoped src="./dropdown.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div class="b-file-uploader-file-card" :class="conditionalClasses"> <div class="b-file-uploader-file-card__details"> <div class="b-file-uploader-file-card__info"> <file-uploader-file-card-animation :name=" isVue2 ? 'b-file-uploader-file-card__animation' : 'b-file-uploader-file-card__animation--vue3' " :show-file-icon="showFileIcon" > <component :is="fileUploaderIcon" :key="JSON.stringify(fileUploaderIcon)" small aria-hidden="true" class="b-file-uploader-file-card__icon" :class="conditionalIconClasses" data-testid="file-uploader-item-icon" /> </file-uploader-file-card-animation> <div ref="fileNameContainerRef" class="b-file-uploader-file-card__file-name"> <bento-link aria-hidden="true" tabindex="-1" :download="file.data.name" :to="downloadUrl" disable-typography is-not-routing variant="quiet" > <bento-typography ref="fileNameRef" v-bento-tooltip-directive="isFileNameTruncated ? file.data.name : null" stronger el="span" > {{ file.data.name }} </bento-typography> </bento-link> </div> <bento-typography v-if="file.progress" el="span">{{ progressLabel }}</bento-typography> <bento-typography v-else-if="file.data.size" class="b-file-uploader-file-card__file-size" aria-hidden="true" el="span" > {{ sizeLabel }} </bento-typography> </div> <div v-if="hasSizeError || hasDimensionsError || (hasTypeError && supportedFileTypes)" :id="fileErrorsSectionId" role="alert" > <bento-typography v-if="hasSizeError" class="b-file-uploader-file-card__error-message"> {{ t('fileExceedsSizeLimit') }} </bento-typography> <bento-typography v-if="hasDimensionsError" class="b-file-uploader-file-card__error-message"> {{ t('imageDimensionsExceedLimits') }} </bento-typography> <bento-typography v-if="hasTypeError && supportedFileTypes" class="b-file-uploader-file-card__error-message" > {{ t('fileTypeMustBe', { list: supportedFileTypes }) }} </bento-typography> </div> </div> <div v-if="!readonly" class="b-file-uploader-file-card__actions"> <bento-button v-if="hasLoadError" ref="retryFileButtonRef" :aria-labelledby="`${fileErrorsSectionId} ${retryFileButtonId}`" variant="tertiary" @click="emit('retry')" > <template #iconLeft> <refresh-icon :id="retryFileButtonId" :svg-title="`${t('retry')} - ${file.data.name} `" /> </template> {{ t('retry') }} </bento-button> <bento-button v-if="isLoading || hasError" ref="cancelFileButtonRef" :aria-labelledby="`${fileErrorsSectionId} ${cancelFileButtonId}`" variant="tertiary" @click="emit('cancel')" > <template #iconLeft> <cross-icon :id="cancelFileButtonId" :svg-title="`${t('cancel')} - ${file.data.name}`" /> </template> </bento-button> <bento-button v-else ref="removeFileButtonRef" :aria-labelledby="`${fileErrorsSectionId} ${removeFileButtonId}`" variant="tertiary" @click="emit('remove')" > <template #iconLeft> <bin-icon :id="removeFileButtonId" :svg-title="`${t('remove')} - ${file.data.name}`" /> </template> </bento-button> </div> </div> </template> <script setup lang="ts"> import { computed, ref, watch } from 'vue'; import { isVue2 } from 'vue-demi'; import { FileUploaderFileCardAnimation } from './components'; import BinIcon from '@adyen/ui-assets-icons-16/vue/bin'; import CheckmarkCircleFilledIcon from '@adyen/ui-assets-icons-16/vue/checkmark-circle-fill'; import CrossIcon from '@adyen/ui-assets-icons-16/vue/cross'; import RefreshIcon from '@adyen/ui-assets-icons-16/vue/refresh'; import WarningFilledIcon from '@adyen/ui-assets-icons-16/vue/warning-filled'; import { BentoTooltipDirective as vBentoTooltipDirective } from '@/directives'; import { BentoButton } from '@/components/button'; import { BentoLink } from '@/components/link'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoTypography } from '@/components/typography'; import { useTruncatedFileName } from './composables/use-truncated-file-name'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { formatFileSize } from '../../utils'; import { getFileIcon, getFileIconColor } from './utils'; import { FileErrorType, FileState } from '@/components/file-uploader/file-uploader.types'; import type { FileUploaderFileCardProps } from './file-uploader-file-card.types'; import type { VueInstance } from '@/types/elements'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const ICON_TIMER = 1200; const props = withDefaults(defineProps<FileUploaderFileCardProps>(), { supportedFileTypes: null, }); const emit = defineEmits<{ /** * Triggered when the "cancel" button is clicked */ (e: 'cancel'): void; /** * Triggered when the "remove" button is clicked */ (e: 'remove'): void; /** * Triggered when the "retry" button is clicked */ (e: 'retry'): void; }>(); const { t, locale } = useI18n<{ message: MessageSchema }>({ messages }); const removeFileButtonRef = ref(null); const cancelFileButtonRef = ref(null); const retryFileButtonRef = ref(null); const fileErrorsSectionId = generateUid('file-errors-section'); const cancelFileButtonId = generateUid('cancel-file-button'); const removeFileButtonId = generateUid('remove-file-button'); const retryFileButtonId = generateUid('retry-file-button'); // If initial state is success, immediately show file icon const showFileIcon = ref(props.file.state === FileState.SUCCESS); const downloadUrl = computed(() => URL.createObjectURL(props.file.data)); const hasError = computed(() => props.file.state === FileState.ERROR); const hasLoadError = computed(() => props.file.errors.includes(FileErrorType.ERROR_LOAD)); const hasSizeError = computed(() => props.file.errors.includes(FileErrorType.ERROR_SIZE)); const hasDimensionsError = computed(() => props.file.errors.includes(FileErrorType.ERROR_DIMENSIONS)); const hasTypeError = computed(() => props.file.errors.includes(FileErrorType.ERROR_TYPE)); const isLoading = computed(() => props.file.state === FileState.LOADING); const progressLabel = computed(() => props.file.progress && `(${props.file.progress.toFixed(0)}%)`); const sizeLabel = computed(() => formatFileSize(locale.value, props.file.data.size)); // Truncation tooltip const fileNameRef = ref<VueInstance>(); const fileNameContainerRef = ref<HTMLDivElement>(); const isFileNameTruncated = useTruncatedFileName(fileNameRef, fileNameContainerRef); const conditionalClasses = computed(() => ({ 'b-file-uploader-file-card--error': hasError.value, })); const conditionalIconClasses = computed(() => ({ [`b-file-uploader-file-card__icon--${getFileIconColor(props.file)}`]: showFileIcon.value, 'b-file-uploader-file-card__icon--error': hasError.value, })); // Compute the icon to be displayed const fileUploaderIcon = computed(() => { if (isLoading.value) { return BentoLoadingIndicator; } if (hasError.value) { return WarningFilledIcon; } return showFileIcon.value ? getFileIcon(props.file) : CheckmarkCircleFilledIcon; }); // Animation when success icon turns to file icon. watch( () => props.file.state, value => { if (value === FileState.SUCCESS && !showFileIcon.value) { const changeTimer = setTimeout(() => { showFileIcon.value = true; clearTimeout(changeTimer); }, ICON_TIMER); } } ); /** * Places the focus on the button element. */ function focus() { if (retryFileButtonRef.value) { retryFileButtonRef.value.$el.focus(); return; } if (removeFileButtonRef.value) { removeFileButtonRef.value.$el.focus(); return; } if (cancelFileButtonRef.value) { cancelFileButtonRef.value.$el.focus(); return; } } defineExpose({ focus, }); </script> <script lang="ts"> /** * * Individual file items that are listed inside "bento-file-uploader" that can * be retried, removed or
|
|
1
|
+
<template> <div class="b-file-uploader-file-card" :class="conditionalClasses"> <div class="b-file-uploader-file-card__details"> <div class="b-file-uploader-file-card__info"> <file-uploader-file-card-animation :name=" isVue2 ? 'b-file-uploader-file-card__animation' : 'b-file-uploader-file-card__animation--vue3' " :show-file-icon="showFileIcon" > <component :is="fileUploaderIcon" :key="JSON.stringify(fileUploaderIcon)" small aria-hidden="true" class="b-file-uploader-file-card__icon" :class="conditionalIconClasses" data-testid="file-uploader-item-icon" /> </file-uploader-file-card-animation> <div ref="fileNameContainerRef" class="b-file-uploader-file-card__file-name"> <bento-link aria-hidden="true" tabindex="-1" :download="file.data.name" :to="downloadUrl" disable-typography is-not-routing variant="quiet" > <bento-typography ref="fileNameRef" v-bento-tooltip-directive="isFileNameTruncated ? file.data.name : null" stronger el="span" > {{ file.data.name }} </bento-typography> </bento-link> </div> <bento-typography v-if="file.progress" el="span">{{ progressLabel }}</bento-typography> <bento-typography v-else-if="file.data.size" class="b-file-uploader-file-card__file-size" aria-hidden="true" el="span" > {{ sizeLabel }} </bento-typography> </div> <div v-if="hasSizeError || hasDimensionsError || (hasTypeError && supportedFileTypes)" :id="fileErrorsSectionId" role="alert" > <bento-typography v-if="hasSizeError" class="b-file-uploader-file-card__error-message"> {{ t('fileExceedsSizeLimit') }} </bento-typography> <bento-typography v-if="hasDimensionsError" class="b-file-uploader-file-card__error-message"> {{ t('imageDimensionsExceedLimits') }} </bento-typography> <bento-typography v-if="hasTypeError && supportedFileTypes" class="b-file-uploader-file-card__error-message" > {{ t('fileTypeMustBe', { list: supportedFileTypes }) }} </bento-typography> </div> </div> <div v-if="!readonly" class="b-file-uploader-file-card__actions"> <bento-button v-if="hasLoadError" ref="retryFileButtonRef" :aria-labelledby="`${fileErrorsSectionId} ${retryFileButtonId}`" variant="tertiary" @click="emit('retry')" > <template #iconLeft> <refresh-icon :id="retryFileButtonId" :svg-title="`${t('retry')} - ${file.data.name} `" /> </template> {{ t('retry') }} </bento-button> <bento-button v-if="isLoading || hasError" ref="cancelFileButtonRef" :aria-labelledby="`${fileErrorsSectionId} ${cancelFileButtonId}`" variant="tertiary" @click="emit('cancel')" > <template #iconLeft> <cross-icon :id="cancelFileButtonId" :svg-title="`${t('cancel')} - ${file.data.name}`" /> </template> </bento-button> <bento-button v-else ref="removeFileButtonRef" :aria-labelledby="`${fileErrorsSectionId} ${removeFileButtonId}`" variant="tertiary" @click="emit('remove')" > <template #iconLeft> <bin-icon :id="removeFileButtonId" :svg-title="`${t('remove')} - ${file.data.name}`" /> </template> </bento-button> </div> </div> </template> <script setup lang="ts"> import { computed, ref, watch } from 'vue'; import { isVue2 } from 'vue-demi'; import { FileUploaderFileCardAnimation } from './components'; import BinIcon from '@adyen/ui-assets-icons-16/vue/bin'; import CheckmarkCircleFilledIcon from '@adyen/ui-assets-icons-16/vue/checkmark-circle-fill'; import CrossIcon from '@adyen/ui-assets-icons-16/vue/cross'; import RefreshIcon from '@adyen/ui-assets-icons-16/vue/refresh'; import WarningFilledIcon from '@adyen/ui-assets-icons-16/vue/warning-filled'; import { BentoTooltipDirective as vBentoTooltipDirective } from '@/directives'; import { BentoButton } from '@/components/button'; import { BentoLink } from '@/components/link'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoTypography } from '@/components/typography'; import { useTruncatedFileName } from './composables/use-truncated-file-name'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { formatFileSize } from '../../utils'; import { getFileIcon, getFileIconColor } from './utils'; import { FileErrorType, FileState } from '@/components/file-uploader/file-uploader.types'; import type { FileUploaderFileCardProps } from './file-uploader-file-card.types'; import type { VueInstance } from '@/types/elements'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const ICON_TIMER = 1200; const props = withDefaults(defineProps<FileUploaderFileCardProps>(), { supportedFileTypes: null, }); const emit = defineEmits<{ /** * Triggered when the "cancel" button is clicked */ (e: 'cancel'): void; /** * Triggered when the "remove" button is clicked */ (e: 'remove'): void; /** * Triggered when the "retry" button is clicked */ (e: 'retry'): void; }>(); const { t, locale } = useI18n<{ message: MessageSchema }>({ messages }); const removeFileButtonRef = ref(null); const cancelFileButtonRef = ref(null); const retryFileButtonRef = ref(null); const fileErrorsSectionId = generateUid('file-errors-section'); const cancelFileButtonId = generateUid('cancel-file-button'); const removeFileButtonId = generateUid('remove-file-button'); const retryFileButtonId = generateUid('retry-file-button'); // If initial state is success, immediately show file icon const showFileIcon = ref(props.file.state === FileState.SUCCESS); const downloadUrl = computed(() => URL.createObjectURL(props.file.data)); const hasError = computed(() => props.file.state === FileState.ERROR); const hasLoadError = computed(() => props.file.errors.includes(FileErrorType.ERROR_LOAD)); const hasSizeError = computed(() => props.file.errors.includes(FileErrorType.ERROR_SIZE)); const hasDimensionsError = computed(() => props.file.errors.includes(FileErrorType.ERROR_DIMENSIONS)); const hasTypeError = computed(() => props.file.errors.includes(FileErrorType.ERROR_TYPE)); const isLoading = computed(() => props.file.state === FileState.LOADING); const progressLabel = computed(() => props.file.progress && `(${props.file.progress.toFixed(0)}%)`); const sizeLabel = computed(() => formatFileSize(locale.value, props.file.data.size)); // Truncation tooltip const fileNameRef = ref<VueInstance>(); const fileNameContainerRef = ref<HTMLDivElement>(); const isFileNameTruncated = useTruncatedFileName(fileNameRef, fileNameContainerRef); const conditionalClasses = computed(() => ({ 'b-file-uploader-file-card--error': hasError.value, })); const conditionalIconClasses = computed(() => ({ [`b-file-uploader-file-card__icon--${getFileIconColor(props.file)}`]: showFileIcon.value, 'b-file-uploader-file-card__icon--error': hasError.value, })); // Compute the icon to be displayed const fileUploaderIcon = computed(() => { if (isLoading.value) { return BentoLoadingIndicator; } if (hasError.value) { return WarningFilledIcon; } return showFileIcon.value ? getFileIcon(props.file) : CheckmarkCircleFilledIcon; }); // Animation when success icon turns to file icon. watch( () => props.file.state, value => { if (value === FileState.SUCCESS && !showFileIcon.value) { const changeTimer = setTimeout(() => { showFileIcon.value = true; clearTimeout(changeTimer); }, ICON_TIMER); } } ); /** * Places the focus on the button element. */ function focus() { if (retryFileButtonRef.value) { retryFileButtonRef.value.$el.focus(); return; } if (removeFileButtonRef.value) { removeFileButtonRef.value.$el.focus(); return; } if (cancelFileButtonRef.value) { cancelFileButtonRef.value.$el.focus(); return; } } defineExpose({ focus, }); </script> <script lang="ts"> /** * * Individual file items that are listed inside "bento-file-uploader" that can * be retried, removed or canceled as they are being uploaded. * * @example * import { FileUploaderFileCard, FileObject } from './'; * * export default { * components: { FileUploaderFileCard }, * template: ` * <file-uploader-file-card * :file="file" * supportedFileTypes="video files, image files" * @retry="retryFile(file)" * @cancel="removeFile(file)" * @remove="removeFile(file)" * /> * `, * setup() { * const file = ref<FileObject>({ * id: 'stub-file', * type: FileType.AUDIO, * data: new File(['blob'], 'File name'), * state: FileState.SUCCESS, * progress: 99, * }); * } * } */ export default { name: 'file-uploader-file-card', i18n: { messages }, }; </script> <style lang="scss" scoped src="./file-uploader-file-card.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div class="b-file-uploader" :class="conditionalClasses"> <field-label class="b-file-uploader__label" :for="inputFieldId" :label="label" :optional="optional" :required="required" :tooltip-text="tooltipText" /> <input :id="inputFieldId" ref="inputRef" aria-hidden="true" type="file" tabindex="-1" data-testid="input" class="b-file-uploader__input" :accept="accept" :disabled="disabled" :multiple="isMultiple" @change="onInputChange" @click="onInputClick" /> <!-- Used to separate the a11y text with breaks for the reader. Hidden to users --> <span :id="commaTextId" aria-hidden="true" class="b-file-uploader__invisible-comma"> , </span> <div v-if="!hideArea" ref="dropAreaRef" :aria-labelledby="fileUploadAriaLabelledBy" class="b-file-uploader__area" role="button" :tabindex="disabled ? -1 : 0" @click="inputRef.click()" @dragenter.stop.prevent="onDragEnter" @dragleave.stop.prevent="onDragLeave" @drop.stop.prevent="onDrop" @dragover="onDragOver" @keydown.enter.prevent.capture="inputRef.click()" @keydown.space.prevent.capture="inputRef.click()" > <div class="b-file-uploader__icon" aria-hidden="true"> <warning-filled-icon v-if="hasError" /> <upload-icon v-else /> </div> <template v-if="!condensed"> <bento-typography :id="dropFilesTextId" el="span" strongest class="b-file-uploader__title"> {{ tc('dropFiles', maxCount) }} </bento-typography> <bento-typography v-if="description" :id="customTextId" el="span" class="b-file-uploader__description"> {{ description }} </bento-typography> <file-uploader-restrictions v-bind="fileUploadRestrictionsProps" :id="fileUploaderRestrictionsId" /> </template> <bento-typography :id="browseFilesButtonId" stronger class="b-file-uploader__button"> {{ t('browseFiles') }} </bento-typography> </div> <error-message v-if="hasError && errorMessage" :id="errorId" :error-message="errorMessage" class="b-file-uploader__error-message" /> <bento-typography v-if="condensed && description" :id="customTextId" aria-hidden="true" el="span" class="b-file-uploader__description b-file-uploader__description--condensed" > {{ description }} </bento-typography> <file-uploader-restrictions v-if="condensed" v-bind="fileUploadRestrictionsProps" :id="fileUploaderRestrictionsId" /> <bento-alert v-if="maxFileCountError" type="critical"> {{ t('tooManyFiles') }} <template #description> {{ t('youHaveExceededTheMaximum', { count: n(props.maxCount) }) }} </template> </bento-alert> <div v-show="files?.length" class="b-file-uploader__files"> <file-uploader-file-card v-for="(file, index) in files" :ref="registerFileCardRef(index)" :key="file.id" :file="file" :supported-file-types="supportedFileTypeList" :readonly="readonly" @cancel="removeFile(file)" @remove="removeFile(file)" /> </div> <bento-typography v-if="!files?.length && readonly" class="b-file-uploader__no-files" wide>{{ t('noFilesUploaded') }}</bento-typography> </div> </template> <script setup lang="ts"> import { computed, nextTick, onMounted, reactive, ref, toRef, watch } from 'vue'; import UploadIcon from '@adyen/ui-assets-icons-16/vue/upload'; import WarningFilledIcon from '@adyen/ui-assets-icons-16/vue/warning-filled'; import { BentoAlert } from '@/components/alert'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; import { FileUploaderFileCard } from './components/file-uploader-file-card'; import { FileUploaderRestrictions } from './components/file-uploader-restrictions'; import { useI18n } from '@/utils/ts/i18n'; import { generateUid } from '@/core/utils/ts'; import { getFileType, readFile } from './utils'; import { useFileTypeList } from './composables'; import { useDragDropState } from '@/components/file-uploader/composables/use-drag-drop-state/use-drag-drop-state'; import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { type BentoFileUploaderProps, type BentoFileUploaderValue, FileErrorType, type FileObject, FileState, FileUploaderState, } from './file-uploader.types'; import type { FileUploaderRestrictionsProps } from '@/components/file-uploader/components/file-uploader-restrictions/file-uploader-restrictions.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t, tc, n } = useI18n<{ message: MessageSchema }>({ messages }); const emit = defineEmits<{ /** * Emitted when files are added or removed. * Provides the entire list of files available. * Can be undefined if the list is empty */ (e: 'change', value?: FileList): void; /** * Vue2: Emitted when files are added or removed. Updates the v-model. * Provides the entire list of files available. * Can be undefined if the list is empty * @deprecated from version 2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', value?: FileList): void; /** * Vue3: Emitted when files are added or removed. Updates the v-model. * Provides the entire list of files available. * Can be undefined if the list is empty */ (e: 'update:model-value', value?: FileList): void; /** * Emitted when any file uploaded has state === FileState.ERROR. * Provides parent component with boolean. * Emits false when no files have errors. */ (e: 'error:upload', value: boolean): void; }>(); const props = withDefaults(defineProps<BentoFileUploaderProps>(), { accept: null, condensed: false, description: null, disabled: false, errorMessage: null, label: null, maxCount: 1, // One file by default maxDimensions: null, maxSize: null, modelValue: null, optional: false, required: false, tooltipText: null, }); const { emitValue } = useFormFieldEmits<BentoFileUploaderValue>(emit); /** * Model value checks * Filter out type string, array and file as FileList is the only type un-deprecated for modelValue * */ /** * Type guard to check if the value is a FileList. * @param files The value to check. */ function isFileList(files: BentoFileUploaderValue): files is FileList { return ( !!files && typeof (files as FileList).length === 'number' && typeof (files as FileList).item === 'function' ); } /** * Used as vitest recognizes FileList as an Array of Files which then * causes any tests with pre-uploaded files to fail as isFileList will fail. * */ onMounted(() => { // Model is not a FileList if (!isFileList(props.modelValue)) { deprecate( 'BentoFileUploaderValue types: string | Array<string> | File | Array<File>', 'Use FileList instead.', '2.0.0' ); } }); const { supportedFileTypeList } = useFileTypeList(toRef(props, 'accept')); const browseFilesButtonId = generateUid('browse-button'); const customTextId = generateUid('custom-text'); const dropFilesTextId = generateUid('drop-files-text'); const commaTextId = generateUid('comma-text'); const fileUploaderRestrictionsId = generateUid('file-uploader-restrictions'); const files = ref<Array<FileObject>>(null); const fileCardRefs = ref<Array<InstanceType<typeof FileUploaderFileCard>>>([]); const inputRef = ref<HTMLInputElement>(null); const dropAreaRef = ref<HTMLDivElement>(null); const inputFieldId = generateUid('input'); const errorId = generateUid('input-error'); const fileUploadAriaLabelledBy = computed(() => { const formattedDropFileTextId = !props.condensed ? `${dropFilesTextId} ${commaTextId}` : ''; const formattedCustomTextId = props.description ? `${customTextId} ${commaTextId}` : ''; const formattedFileUploaderRestrictionsId = `${fileUploaderRestrictionsId} ${commaTextId}`; return `${formattedDropFileTextId} ${formattedCustomTextId} ${formattedFileUploaderRestrictionsId} ${browseFilesButtonId}`; }); const { dragState, dragErrors, resetDragState, onDragEnterValidations } = useDragDropState({ accept: toRef(props, 'accept'), maxCount: toRef(props, 'maxCount'), }); const fileUploadRestrictionsProps = computed<FileUploaderRestrictionsProps>(() => ({ condensed: props.condensed, disabled: props.disabled, maxCount: isMultiple.value ? { value: props.maxCount, error: dragErrors.maxCountOverflow || maxFileCountError.value, } : null, maxDimensions: props.maxDimensions ? { value: props.maxDimensions, error: files.value?.some(file => file.errors.includes(FileErrorType.ERROR_DIMENSIONS)), } : null, maxSize: props.maxSize ? { value: props.maxSize, error: files.value?.some(file => file.errors.includes(FileErrorType.ERROR_SIZE)), } : null, supportedFileTypes: supportedFileTypeList.value ? { value: supportedFileTypeList.value, error: dragErrors.invalidTypes || dragErrors.partiallyInvalidTypes || files.value?.some(file => file.errors.includes(FileErrorType.ERROR_TYPE)), } : null, })); // Prevents triggering "dragleave" events on children elements const enterTarget = ref<EventTarget>(); const conditionalClasses = computed(() => ({ [`b-file-uploader--${FileUploaderState.DISABLED}`]: props.disabled, [`b-file-uploader--${FileUploaderState.DRAG_OVER}`]: dragState.dragging, [`b-file-uploader--${FileUploaderState.ERROR}`]: hasError.value, [`b-file-uploader--condensed`]: props.condensed, })); const isMultiple = computed(() => props.maxCount > 1); const hideArea = computed(() => (props.maxCount && files.value?.length >= props.maxCount) || props.readonly); const loadingStateExists = computed(() => files.value?.some(file => file.state === 'loading')); const registerFileCardRef = (cardIndex: number) => fileCardRef => { fileCardRefs.value[cardIndex] = fileCardRef; }; /** * Verifies the new list of files and emits the `change` and `update:modelValue` events. * @param newList The new list of files to process and emit. */ function emitUpdate(newList: Array<FileObject>) { if (newList) { verifyFileCount(newList); verifyFileSize(newList); const value = convertToFileList(newList); emit('change', value); emitValue(value); } else { emit('change', undefined); emitValue(undefined); } } /** Errors */ const hasError = computed(() => !!props.errorMessage || dragState.error); const maxFileCountError = computed(() => files.value?.length > props.maxCount); const maxFileError = ref(false); const hasRestrictionError = computed(() => files.value?.some(file => file.state === 'error')); watch(hasRestrictionError, newValue => { // Without this, when files are initially uploaded the component // will immediately and unnecessarily emit 'false' and then // emit 'true' once processing has done, if errors exists. if (loadingStateExists.value) { return; } emit('error:upload', newValue); }); /** * Places the focus to the "drop area" after adding or removing a file. * If more files can be added, the focus is moved to the "drop area". * If no more files can be added, the focus is moved to the first file card. */ async function resetFocus() { await nextTick(); if (files.value.length === props.maxCount) { fileCardRefs.value[0]?.focus(); return; } dropAreaRef.value.focus(); } /** * Removes the file from the list of files when the * "remove" or "cancel" events are triggered from * the file card component * @param file File to be removed */ async function removeFile(file: FileObject) { files.value = files.value.filter(({ id }) => id !== file.id); await resetFocus(); emitUpdate(files.value); } /** Drag n Drop functions */ /** * Creates a FileObject from a native File and tracks its client-load progress. * @param file The native File object. * @returns A FileObject. */ function processFile(file: File) { const fileObject = reactive<FileObject>({ id: generateUid('file'), data: file, errors: [], progress: null, state: FileState.LOADING, type: getFileType(file), }); readFile(fileObject, props.accept, props.maxDimensions); return fileObject; } /** * Prevent opening the file browser when no * more files can be added (until one file is removed). * @param event Triggered event */ function onInputClick(event: MouseEvent) { if (files.value?.length === props.maxCount) { event.preventDefault(); } } /** * Sets the file/files to track their progress while being processed * @param fileList File or list of files to be processed. */ async function onFileInput(fileList: FileList) { // Prevent adding many files if maxCount is 1 if (props.maxCount === 1 && fileList.length > props.maxCount) { resetEnterTarget(); resetDragState(); return; } if (isMultiple.value) { // Add the new files to the file selection. // Removing files should be done by interacting with the file list below the area. files.value = [...(files.value || []), ...Array.from(fileList).map(file => processFile(file))]; } else { files.value = [processFile(fileList[0])]; await resetFocus(); } emitUpdate(files.value); } /** * Handles the "dragenter" event, triggered when the mouse enters the draggable area. * Enables the dragging state and sets the dragging error to true to * prevent dropping files from being added if there's a general error. * @param event Drag event */ function onDragEnter(event: DragEvent) { // Prevent changing state if disabled if (props.disabled) { return; } /** * Allows internal components as part of the dragable area. * Prevents trigerring "dragleave" events on children elements. * Set the "dragarea" as target */ enterTarget.value = event.target; const totalFilesCount = (event.dataTransfer.items?.length ?? 0) + (files.value?.length ?? 0); onDragEnterValidations(totalFilesCount, event); } /** * Resets the reference to the target. * This helps preventing loosing the state when dragging over children */ function resetEnterTarget() { enterTarget.value = null; } /** * Handles the "dragleave" event, triggered when the mouse leaves the draggable area. * Maintains the dragging state while dragging over children. * Resets the states when the mouse leaves the draggable area * @param event Drag event */ function onDragLeave(event: DragEvent) { // If target enter and leave are the same it means the drag has left the "dragarea" if (enterTarget.value === event.target) { resetEnterTarget(); resetDragState(); } } /** * Handles the "dragover" event, triggered when the mouse moves over the draggable area. * Disables the cursor when the component is disabled. * @param event Drag event */ function onDragOver(event: DragEvent) { event.preventDefault(); // Disable drag n drop and change the cursor if disabled if (props.disabled) { // eslint-disable-next-line no-param-reassign event.dataTransfer.dropEffect = 'none'; } } /** * Hadles the "drop" event, triggered when files are dropped inside the draggable area. * Processes the files that are dropped. * @param event Drag event */ async function onDrop(event: DragEvent) { // Prevent events when the input is disabled if (props.disabled) { return; } if (!dragErrors.invalidTypes && event.dataTransfer.files?.length) { onFileInput(event.dataTransfer.files); } resetEnterTarget(); resetDragState(); } /** * Handles the "change" event over the "input" element. * Processes the files that are selected through the browser's file explorer. */ async function onInputChange() { if (inputRef.value.files?.length) { onFileInput(inputRef.value.files); } // Reset input value inputRef.value.value = ''; await resetFocus(); } /** End of drag and drop functions */ /** * Watches the modelValue and if set * and the type is not deprecated, it processes the files * but doesn't emit them as the user already has those files. */ watch( () => props.modelValue, (newValue: BentoFileUploaderValue) => { // Sets the new value if it is a `FileList`. This ignores deprecated value types. if (newValue && isFileList(newValue)) { const fileList = newValue as FileList; const fileListArray: Array<File> = Array.from(fileList); files.value = fileListArray.map(file => processFile(file)); } else if (!newValue && files.value) { // Clear the file list when the value is programmatically unset. files.value = null; } }, { immediate: true } ); function verifyFileCount(fileList: Array<FileObject>) { const isValid = props.maxCount && fileList.length <= props.maxCount; maxFileError.value = !isValid; } function verifyFileSize(fileList: Array<FileObject>) { if (props.maxSize) { fileList.forEach(file => { const item = file; // Prevent eslint no-param-reassign if (item.data.size > props.maxSize) { item.state = FileState.ERROR; item.errors.push(FileErrorType.ERROR_SIZE); } }); } } function convertToFileList(fileList: Array<FileObject>) { // Filter out files with errors const validFiles = fileList.filter( ({ errors }) => !errors.includes(FileErrorType.ERROR_LOAD) && !errors.includes(FileErrorType.ERROR_SIZE) ); if (validFiles.length) { const dataTransfer = new DataTransfer(); validFiles.forEach(({ data, id }) => { dataTransfer.items.add(data); }); return dataTransfer.files; } return undefined; } </script> <script lang="ts"> /** * The file uploader component enables users to upload one or multiple files from their device to our system. * * @example * import { BentoFileUploader, type BentoFileValue } from '@adyen/bento-vue2'; * * export default { * components: { BentoFileUploader }, * template: ` * <bento-file-uploader * accept="image/*,video/*,.pdf,.png" * condensed * description="Custom supporting text" * disabled * errorMessage="Custom error message" * label="Bento file uploader" * :maxCount="1" * :maxDimensions="{ width: 200, height: 100 }" * :maxSize="1000" * v-model="files" * :optional="false" * required * tooltipText="Useful information" * /> * `, * setup() { * const files = ref<BentoFileValue>() * } * } */ export default { model: { prop: 'modelValue' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./file-uploader.scss" />
|
|
1
|
+
<template> <div class="b-file-uploader" :class="conditionalClasses"> <field-label class="b-file-uploader__label" :for="inputFieldId" :label="label" :optional="optional" :required="required" :tooltip-text="tooltipText" /> <input :id="inputFieldId" ref="inputRef" aria-hidden="true" type="file" tabindex="-1" data-testid="input" class="b-file-uploader__input" :accept="accept" :disabled="disabled" :multiple="isMultiple" @change="onInputChange" @click="onInputClick" /> <!-- Used to separate the a11y text with breaks for the reader. Hidden to users --> <span :id="commaTextId" aria-hidden="true" class="b-file-uploader__invisible-comma"> , </span> <div v-if="!hideArea" ref="dropAreaRef" :aria-labelledby="fileUploadAriaLabelledBy" class="b-file-uploader__area" role="button" :tabindex="disabled ? -1 : 0" @click="inputRef.click()" @dragenter.stop.prevent="onDragEnter" @dragleave.stop.prevent="onDragLeave" @drop.stop.prevent="onDrop" @dragover="onDragOver" @keydown.enter.prevent.capture="inputRef.click()" @keydown.space.prevent.capture="inputRef.click()" > <div class="b-file-uploader__icon" aria-hidden="true"> <warning-filled-icon v-if="hasError" /> <upload-icon v-else /> </div> <template v-if="!condensed"> <bento-typography :id="dropFilesTextId" el="span" strongest class="b-file-uploader__title"> {{ tc('dropFiles', maxCount) }} </bento-typography> <bento-typography v-if="description" :id="customTextId" el="span" class="b-file-uploader__description"> {{ description }} </bento-typography> <file-uploader-restrictions v-bind="fileUploadRestrictionsProps" :id="fileUploaderRestrictionsId" /> </template> <bento-typography :id="browseFilesButtonId" stronger class="b-file-uploader__button"> {{ t('browseFiles') }} </bento-typography> </div> <error-message v-if="hasError && errorMessage" :id="errorId" :error-message="errorMessage" class="b-file-uploader__error-message" /> <bento-typography v-if="condensed && description" :id="customTextId" aria-hidden="true" el="span" class="b-file-uploader__description b-file-uploader__description--condensed" > {{ description }} </bento-typography> <file-uploader-restrictions v-if="condensed" v-bind="fileUploadRestrictionsProps" :id="fileUploaderRestrictionsId" /> <bento-alert v-if="maxFileCountError" type="critical"> {{ t('tooManyFiles') }} <template #description> {{ t('youHaveExceededTheMaximum', { count: n(props.maxCount) }) }} </template> </bento-alert> <div v-show="files?.length" class="b-file-uploader__files"> <file-uploader-file-card v-for="(file, index) in files" :ref="registerFileCardRef(index)" :key="file.id" :file="file" :supported-file-types="supportedFileTypeList" :readonly="readonly" @cancel="removeFile(file)" @remove="removeFile(file)" /> </div> <bento-typography v-if="!files?.length && readonly" class="b-file-uploader__no-files" wide>{{ t('noFilesUploaded') }}</bento-typography> </div> </template> <script setup lang="ts"> import { computed, nextTick, onMounted, reactive, ref, toRef, watch } from 'vue'; import UploadIcon from '@adyen/ui-assets-icons-16/vue/upload'; import WarningFilledIcon from '@adyen/ui-assets-icons-16/vue/warning-filled'; import { BentoAlert } from '@/components/alert'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; import { FileUploaderFileCard } from './components/file-uploader-file-card'; import { FileUploaderRestrictions } from './components/file-uploader-restrictions'; import { useI18n } from '@/utils/ts/i18n'; import { generateUid } from '@/core/utils/ts'; import { getFileType, readFile } from './utils'; import { useFileTypeList } from './composables'; import { useDragDropState } from '@/components/file-uploader/composables/use-drag-drop-state/use-drag-drop-state'; import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { type BentoFileUploaderProps, type BentoFileUploaderValue, FileErrorType, type FileObject, FileState, FileUploaderState, } from './file-uploader.types'; import type { FileUploaderRestrictionsProps } from '@/components/file-uploader/components/file-uploader-restrictions/file-uploader-restrictions.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t, tc, n } = useI18n<{ message: MessageSchema }>({ messages }); const emit = defineEmits<{ /** * Emitted when files are added or removed. * Provides the entire list of files available. * Can be undefined if the list is empty */ (e: 'change', value?: FileList): void; /** * Vue2: Emitted when files are added or removed. Updates the v-model. * Provides the entire list of files available. * Can be undefined if the list is empty * @deprecated from version 2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', value?: FileList): void; /** * Vue3: Emitted when files are added or removed. Updates the v-model. * Provides the entire list of files available. * Can be undefined if the list is empty */ (e: 'update:model-value', value?: FileList): void; /** * Emitted when any file uploaded has state === FileState.ERROR. * Provides parent component with boolean. * Emits false when no files have errors. */ (e: 'error:upload', value: boolean): void; }>(); const props = withDefaults(defineProps<BentoFileUploaderProps>(), { accept: null, condensed: false, description: null, disabled: false, errorMessage: null, label: null, maxCount: 1, // One file by default maxDimensions: null, maxSize: null, modelValue: null, optional: false, required: false, tooltipText: null, }); const { emitValue } = useFormFieldEmits<BentoFileUploaderValue>(emit); /** * Model value checks * Filter out type string, array and file as FileList is the only type un-deprecated for modelValue * */ /** * Type guard to check if the value is a FileList. * @param files The value to check. */ function isFileList(files: BentoFileUploaderValue): files is FileList { return ( !!files && typeof (files as FileList).length === 'number' && typeof (files as FileList).item === 'function' ); } /** * Used as vitest recognizes FileList as an Array of Files which then * causes any tests with pre-uploaded files to fail as isFileList will fail. * */ onMounted(() => { // Model is not a FileList if (!isFileList(props.modelValue)) { deprecate( 'BentoFileUploaderValue types: string | Array<string> | File | Array<File>', 'Use FileList instead.', '2.0.0' ); } }); const { supportedFileTypeList } = useFileTypeList(toRef(props, 'accept')); const browseFilesButtonId = generateUid('browse-button'); const customTextId = generateUid('custom-text'); const dropFilesTextId = generateUid('drop-files-text'); const commaTextId = generateUid('comma-text'); const fileUploaderRestrictionsId = generateUid('file-uploader-restrictions'); const files = ref<Array<FileObject>>(null); const fileCardRefs = ref<Array<InstanceType<typeof FileUploaderFileCard>>>([]); const inputRef = ref<HTMLInputElement>(null); const dropAreaRef = ref<HTMLDivElement>(null); const inputFieldId = generateUid('input'); const errorId = generateUid('input-error'); const fileUploadAriaLabelledBy = computed(() => { const formattedDropFileTextId = !props.condensed ? `${dropFilesTextId} ${commaTextId}` : ''; const formattedCustomTextId = props.description ? `${customTextId} ${commaTextId}` : ''; const formattedFileUploaderRestrictionsId = `${fileUploaderRestrictionsId} ${commaTextId}`; return `${formattedDropFileTextId} ${formattedCustomTextId} ${formattedFileUploaderRestrictionsId} ${browseFilesButtonId}`; }); const { dragState, dragErrors, resetDragState, onDragEnterValidations } = useDragDropState({ accept: toRef(props, 'accept'), maxCount: toRef(props, 'maxCount'), }); const fileUploadRestrictionsProps = computed<FileUploaderRestrictionsProps>(() => ({ condensed: props.condensed, disabled: props.disabled, maxCount: isMultiple.value ? { value: props.maxCount, error: dragErrors.maxCountOverflow || maxFileCountError.value, } : null, maxDimensions: props.maxDimensions ? { value: props.maxDimensions, error: files.value?.some(file => file.errors.includes(FileErrorType.ERROR_DIMENSIONS)), } : null, maxSize: props.maxSize ? { value: props.maxSize, error: files.value?.some(file => file.errors.includes(FileErrorType.ERROR_SIZE)), } : null, supportedFileTypes: supportedFileTypeList.value ? { value: supportedFileTypeList.value, error: dragErrors.invalidTypes || dragErrors.partiallyInvalidTypes || files.value?.some(file => file.errors.includes(FileErrorType.ERROR_TYPE)), } : null, })); // Prevents triggering "dragleave" events on children elements const enterTarget = ref<EventTarget>(); const conditionalClasses = computed(() => ({ [`b-file-uploader--${FileUploaderState.DISABLED}`]: props.disabled, [`b-file-uploader--${FileUploaderState.DRAG_OVER}`]: dragState.dragging, [`b-file-uploader--${FileUploaderState.ERROR}`]: hasError.value, [`b-file-uploader--condensed`]: props.condensed, })); const isMultiple = computed(() => props.maxCount > 1); const hideArea = computed(() => (props.maxCount && files.value?.length >= props.maxCount) || props.readonly); const loadingStateExists = computed(() => files.value?.some(file => file.state === 'loading')); const registerFileCardRef = (cardIndex: number) => fileCardRef => { fileCardRefs.value[cardIndex] = fileCardRef; }; /** * Verifies the new list of files and emits the `change` and `update:modelValue` events. * @param newList The new list of files to process and emit. */ function emitUpdate(newList: Array<FileObject>) { if (newList) { verifyFileCount(newList); verifyFileSize(newList); const value = convertToFileList(newList); emit('change', value); emitValue(value); } else { emit('change', undefined); emitValue(undefined); } } /** Errors */ const hasError = computed(() => !!props.errorMessage || dragState.error); const maxFileCountError = computed(() => files.value?.length > props.maxCount); const maxFileError = ref(false); const hasRestrictionError = computed(() => files.value?.some(file => file.state === 'error')); watch(hasRestrictionError, newValue => { // Without this, when files are initially uploaded the component // will immediately and unnecessarily emit 'false' and then // emit 'true' once processing has done, if errors exists. if (loadingStateExists.value) { return; } emit('error:upload', newValue); }); /** * Places the focus to the "drop area" after adding or removing a file. * If more files can be added, the focus is moved to the "drop area". * If no more files can be added, the focus is moved to the first file card. */ async function resetFocus() { await nextTick(); if (files.value.length === props.maxCount) { fileCardRefs.value[0]?.focus(); return; } dropAreaRef.value.focus(); } /** * Removes the file from the list of files when the * "remove" or "cancel" events are triggered from * the file card component * @param file File to be removed */ async function removeFile(file: FileObject) { files.value = files.value.filter(({ id }) => id !== file.id); await resetFocus(); emitUpdate(files.value); } /** Drag n Drop functions */ /** * Creates a FileObject from a native File and tracks its client-load progress. * @param file The native File object. * @returns A FileObject. */ function processFile(file: File) { const fileObject = reactive<FileObject>({ id: generateUid('file'), data: file, errors: [], progress: null, state: FileState.LOADING, type: getFileType(file), }); readFile(fileObject, props.accept, props.maxDimensions); return fileObject; } /** * Prevent opening the file browser when no * more files can be added (until one file is removed). * @param event Triggered event */ function onInputClick(event: MouseEvent) { if (files.value?.length === props.maxCount) { event.preventDefault(); } } /** * Sets the file/files to track their progress while being processed * @param fileList File or list of files to be processed. */ async function onFileInput(fileList: FileList) { // Prevent adding many files if maxCount is 1 if (props.maxCount === 1 && fileList.length > props.maxCount) { resetEnterTarget(); resetDragState(); return; } if (isMultiple.value) { // Add the new files to the file selection. // Removing files should be done by interacting with the file list below the area. files.value = [...(files.value || []), ...Array.from(fileList).map(file => processFile(file))]; } else { files.value = [processFile(fileList[0])]; await resetFocus(); } emitUpdate(files.value); } /** * Handles the "dragenter" event, triggered when the mouse enters the draggable area. * Enables the dragging state and sets the dragging error to true to * prevent dropping files from being added if there's a general error. * @param event Drag event */ function onDragEnter(event: DragEvent) { // Prevent changing state if disabled if (props.disabled) { return; } /** * Allows internal components as part of the dragable area. * Prevents trigerring "dragleave" events on children elements. * Set the "dragarea" as target */ enterTarget.value = event.target; const totalFilesCount = (event.dataTransfer.items?.length ?? 0) + (files.value?.length ?? 0); onDragEnterValidations(totalFilesCount, event); } /** * Resets the reference to the target. * This helps preventing loosing the state when dragging over children */ function resetEnterTarget() { enterTarget.value = null; } /** * Handles the "dragleave" event, triggered when the mouse leaves the draggable area. * Maintains the dragging state while dragging over children. * Resets the states when the mouse leaves the draggable area * @param event Drag event */ function onDragLeave(event: DragEvent) { // If target enter and leave are the same it means the drag has left the "dragarea" if (enterTarget.value === event.target) { resetEnterTarget(); resetDragState(); } } /** * Handles the "dragover" event, triggered when the mouse moves over the draggable area. * Disables the cursor when the component is disabled. * @param event Drag event */ function onDragOver(event: DragEvent) { event.preventDefault(); // Disable drag n drop and change the cursor if disabled if (props.disabled) { // eslint-disable-next-line no-param-reassign event.dataTransfer.dropEffect = 'none'; } } /** * Hadles the "drop" event, triggered when files are dropped inside the draggable area. * Processes the files that are dropped. * @param event Drag event */ async function onDrop(event: DragEvent) { // Prevent events when the input is disabled if (props.disabled) { return; } if (!dragErrors.invalidTypes && event.dataTransfer.files?.length) { onFileInput(event.dataTransfer.files); } resetEnterTarget(); resetDragState(); } /** * Handles the "change" event over the "input" element. * Processes the files that are selected through the browser's file explorer. */ async function onInputChange() { if (inputRef.value.files?.length) { onFileInput(inputRef.value.files); } // Reset input value inputRef.value.value = ''; await resetFocus(); } /** End of drag and drop functions */ /** * Watches the modelValue and if set * and the type is not deprecated, it processes the files * but doesn't emit them as the user already has those files. */ watch( () => props.modelValue, (newValue: BentoFileUploaderValue) => { // Sets the new value if it is a `FileList`. This ignores deprecated value types. if (newValue && isFileList(newValue)) { const fileList = newValue as FileList; const fileListArray: Array<File> = Array.from(fileList); files.value = fileListArray.map(file => processFile(file)); } else if (!newValue && files.value) { // Clear the file list when the value is programmatically unset. files.value = null; } }, { immediate: true } ); function verifyFileCount(fileList: Array<FileObject>) { const isValid = props.maxCount && fileList.length <= props.maxCount; maxFileError.value = !isValid; } function verifyFileSize(fileList: Array<FileObject>) { if (props.maxSize) { fileList.forEach(file => { const item = file; // Prevent eslint no-param-reassign if (item.data.size > props.maxSize) { item.state = FileState.ERROR; if (!item.errors.includes(FileErrorType.ERROR_SIZE)) { item.errors.push(FileErrorType.ERROR_SIZE); } } }); } } function convertToFileList(fileList: Array<FileObject>) { // Filter out files with errors const validFiles = fileList.filter( ({ errors }) => !errors.includes(FileErrorType.ERROR_LOAD) && !errors.includes(FileErrorType.ERROR_SIZE) ); if (validFiles.length) { const dataTransfer = new DataTransfer(); validFiles.forEach(({ data, id }) => { dataTransfer.items.add(data); }); return dataTransfer.files; } return undefined; } </script> <script lang="ts"> /** * The file uploader component enables users to upload one or multiple files from their device to our system. * * @example * import { BentoFileUploader, type BentoFileValue } from '@adyen/bento-vue2'; * * export default { * components: { BentoFileUploader }, * template: ` * <bento-file-uploader * accept="image/*,video/*,.pdf,.png" * condensed * description="Custom supporting text" * disabled * errorMessage="Custom error message" * label="Bento file uploader" * :maxCount="1" * :maxDimensions="{ width: 200, height: 100 }" * :maxSize="1000" * v-model="files" * :optional="false" * required * tooltipText="Useful information" * /> * `, * setup() { * const files = ref<BentoFileValue>() * } * } */ export default { model: { prop: 'modelValue' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./file-uploader.scss" />
|
package/dist/assets/components/filter-bar/components/date-range-filter/date-range-filter.vue
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <bento-base-filter ref="baseFilter" class="b-base-filter-date-range-filter" :button-label="buttonLabel" :button-secondary-label="buttonSecondaryLabel" :controlled="!onlySlot" :disabled="disabled" :disable-apply-button="isApplyButtonDisabled" :disable-secondary-button="isSecondaryButtonDisabled" :default-value="defaultValue" :field="field" :label="label" :only-slot="onlySlot" :tooltip-position="options?.tooltipPosition" :tooltip-text="options?.tooltipText" fit-content @open="openFilter" @clear="clearFilter" @reset="resetFilter" @cancel="onCancel" > <bento-date-range-picker v-if="onlySlot" :allow-time-input="options?.allowTimeInput" :model-value="internalFilterValue" :first-day-of-week="options?.firstDayOfWeek" :is-date-disabled="options?.isDateDisabled" :number-of-months="options?.numberOfMonths" :quick-select-ranges="options?.quickSelectRanges" :variant="options?.variant" :disabled="disabled" :granularities="options?.granularities" :max-range="options?.maxRange" :min="options?.min" :max="options?.max" :date-form-data="formData" :show-end-date-on-open="options?.showEndDateOnOpen" @update:model-value="onDateInputSelected" /> <template v-else> <div class="b-date-range-filter"> <date-range-picker-calendar :allow-time-input="options?.allowTimeInput" :first-day-of-week="options?.firstDayOfWeek" :is-date-disabled="options?.isDateDisabled" :granularities="options?.granularities" :max-range="options?.maxRange" :min="options?.min" :max="options?.max" :number-of-months="options?.numberOfMonths" :quick-select-ranges="adjustedQuickSelectRanges" :value="internalFilterValue" :date-form-data="formData" :show-end-date-on-open="options?.showEndDateOnOpen" :variant="options?.variant" @input="onDateSelected" @custom-range="onRangeSelectorInput" @error="onDatePickerRangeError" @form-date="onDatePickerRangeFormDateInputUpdate" @start-date-selected="isSelectingDate = true" > <template #title> <bento-typography strongest variant="body" el="span"> {{ label }} </bento-typography> </template> <template #actions> <bento-divider class="b-date-range-filter__divider" /> <bento-button-actions class="b-date-range-filter__actions" :actions="actions" layout="space-between" /> </template> </date-range-picker-calendar> </div> </template> </bento-base-filter> </template> <script lang="ts"> import { computed, defineComponent, type PropType, reactive, ref, toRef, watch } from 'vue'; import { BentoBaseFilter, useBaseFilter } from '../base-filter'; import { type BentoDateRangeFilterOptions, type BentoDateRangeFilterValue, BentoFilterEvent, } from '../../filter-bar.types'; import { DateRangePickerCalendar } from '@/components/date-range-picker/components/date-range-picker-calendar'; import { BentoDateRangePicker, type BentoDateRangePickerValue } from '@/components/date-range-picker'; import { BentoDivider } from '@/components/divider'; import { BentoTypography } from '@/components/typography'; import { BentoButtonActions, type BentoButtonActionsList } from '@/components/button/components/button-actions'; import { useI18n } from '@/utils/ts/i18n'; import { getDateLocale } from '@/utils/ts/get-date-locale/get-date-locale'; import { DateFormat } from '@/types/date-format'; import { isSameDay } from 'date-fns/isSameDay'; import { format } from 'date-fns/format'; import { isEqual } from 'date-fns/isEqual'; import { isToday } from 'date-fns/isToday'; import messages from './messages.json'; import { type DateRangePickerCalendarFormData, type DateRangePickerCalendarRangeSelectorItem, type DateRangePickerCalendarRangeSelectorItems, } from '@/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.types'; type MessageSchema = (typeof messages)['en-US']; /** * The Date filter displays a calendar that allows * to select a single date. * * @example * import { BentoDateRangeFilter } from '@adyen/bento-vue2'; * * export default { * components: { BentoDateRangeFilter }, * template: ` * <bento-date-range-filter * :field="filter.field" * :label="filter.label" * :options="filter.options" * :value="filter.value" * @update="updateFilterHandler" * > * ` * } */ export default defineComponent({ i18n: { messages }, name: 'bento-date-range-filter', components: { BentoBaseFilter, BentoButtonActions, BentoDateRangePicker, BentoTypography, BentoDivider, DateRangePickerCalendar, }, props: { /** * Disables the filter from being interacted with. */ disabled: { type: Boolean, default: false }, /** * Default value of the filter to reset to. */ defaultValue: { type: Object as PropType<BentoDateRangeFilterValue>, default: null }, /** * Filter name/key referencing the actual property name in the data set. * Should be unique per filter bar. */ field: { type: String, required: true }, /** * Label text to be displayed */ label: { type: String, required: true }, /** * Renders the base filter's slot content only - leaving out the button and the popover. * Used for the allFiltersModal component. */ onlySlot: { type: Boolean, default: false }, /** * List of options that are needed to setup the filter * Available options: * - allowTimeInput: Allows user to enter time values in the form * - firstDayOfWeek: Allows user to set the first day of the week. 0 is sunday, 1 is monday, 6 is saturday * - isDateDisabled: Indicate if a date should be disabled or not * - maxRange: Set a maximum number of dates to be selectable by the range * - numberOfMonths: Number of months rendered on pane * - quickSelectRanges: An array that sets the custom range dropdown items. @see BentoDateRangePicker * - showEndDateOnOpen: Upon opening date range picker, end date's month will be pre-selected. * - granularities: A list of available granularities. * - variant: The type of calendar to display. Defaults to showing days. * - tooltipPosition: The position of the tooltip attached to the filter button * - tooltipText: The content of the tooltip attached to the filter button */ options: { type: Object as PropType<BentoDateRangeFilterOptions>, default: undefined, }, /** * Current value of the filter */ value: { type: Object as PropType<BentoDateRangeFilterValue>, default: () => ({ startDate: undefined, endDate: undefined }), }, }, emits: [BentoFilterEvent.INPUT, BentoFilterEvent.UPDATE], setup(props, { emit }) { const { locale, t } = useI18n<{ message: MessageSchema }>({ messages }); const locales = getDateLocale(); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled } = useBaseFilter( toRef(props, 'value'), toRef(props, 'defaultValue') ); const isSelectingDate = ref(false); const adjustedQuickSelectRanges = computed<Array<DateRangePickerCalendarRangeSelectorItem>>(() => { return ( props?.options?.quickSelectRanges && props.options.quickSelectRanges.map((range: DateRangePickerCalendarRangeSelectorItem) => { const timeDiff = !range.data.endDate ? new Date(Date.now()).getTime() - range.data.startDate.getTime() : undefined; return { ...range, data: { ...range.data, // Adding timeDifference to calculate an up-to-date endDate on range selection timeDifference: timeDiff, }, }; }) ); }); const formatFormDate = (date: Date) => date ? format(date, DateFormat.NUMERIC_DATE_YEAR_FIRST, { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, }) : undefined; const formatFormTime = (dateToFormat?: Date) => { if (!dateToFormat) { return ''; } return format(dateToFormat, DateFormat.HOUR_MINUTE_SECONDS, { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, }); }; const baseFilter = ref(null); const isDateIncorrect = ref(false); const formData = reactive({ startDate: formatFormDate(props.value?.startDate), endDate: formatFormDate(props.value?.endDate), startTime: formatFormTime(props.value?.startDate), endTime: formatFormTime(props.value?.endDate), }); watch( () => props.value, () => { formData.startDate = formatFormDate(props.value?.startDate); formData.endDate = formatFormDate(props.value?.endDate); formData.startTime = formatFormTime(props.value?.startDate); formData.endTime = formatFormTime(props.value?.endDate); } ); function isQuickSelectAnArray( quickSelectRanges: BentoDateRangeFilterOptions['quickSelectRanges'] ): quickSelectRanges is DateRangePickerCalendarRangeSelectorItems { return (quickSelectRanges as DateRangePickerCalendarRangeSelectorItems)?.length !== undefined; } const calculateSecondaryLabel = (currentValue: BentoDateRangeFilterValue) => { if (!currentValue || (!currentValue?.startDate && !currentValue.endDate)) { return null; } // If quick select range selected then display its label if ( isQuickSelectAnArray(props.options?.quickSelectRanges) && props.options?.quickSelectRanges?.length > 0 ) { const customRangeSelected = props.options?.quickSelectRanges?.find(({ value, data }) => { if (currentValue) { if (currentValue.range) { return value === currentValue.range; } if (!currentValue.startDate || !currentValue.endDate) { return false; } if (!data?.endDate) { // Autoselecting the correct value for open-ended ranges return ( isEqual(data?.startDate, currentValue.startDate) && isToday(currentValue.endDate) ); } // If no range selected but start and end dates correspond to one of the preselected ranges, select it return ( isEqual(data?.startDate, currentValue.startDate) && isEqual(data?.endDate, currentValue.endDate) ); } return false; }); if (customRangeSelected) { return customRangeSelected.label; } } if (!currentValue.startDate || !currentValue.endDate) { return null; } return isSameDay(currentValue.startDate, currentValue.endDate) && props?.options?.allowTimeInput ? `${format(currentValue.startDate, 'dd MMM, yyyy HH:mm:ss', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })} - ${format(currentValue.endDate, 'HH:mm:ss', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })}` : `${format(currentValue.startDate, 'dd MMM, yyyy', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })} - ${format(currentValue.endDate, 'dd MMM, yyyy', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })}`; }; const buttonSecondaryLabel = ref(calculateSecondaryLabel(props.value)); const buttonLabel = computed(() => props.label); const updateFilter = () => { baseFilter.value.closePopover(); // Omit the onlySlot prop as it is not needed const { onlySlot, ...cleanedProps } = props; emit(BentoFilterEvent.UPDATE, { ...cleanedProps, value: internalFilterValue.value }); }; const clearFilter = () => { internalFilterValue.value = undefined; updateFilter(); }; const resetFilter = () => { internalFilterValue.value = props.defaultValue; updateFilter(); }; const openFilter = () => { internalFilterValue.value = props.value; }; // If we click outside or press escape this should cancel and reset the form inputs // set all internal states back to their initial values const onCancel = () => { internalFilterValue.value = props.value; isSelectingDate.value = false; formData.startDate = formatFormDate(props.value?.startDate); formData.endDate = formatFormDate(props.value?.endDate); formData.startTime = formatFormTime(props.value?.startDate); formData.endTime = formatFormTime(props.value?.endDate); }; const onDateSelected = (selectedDate: BentoDateRangePickerValue) => { isSelectingDate.value = false; internalFilterValue.value = selectedDate; formData.startDate = formatFormDate(selectedDate.startDate); formData.endDate = formatFormDate(selectedDate.endDate); emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const onRangeSelectorInput = (quickSelectRanges?: { startDate: Date; endDate: Date }) => { if (quickSelectRanges?.startDate && quickSelectRanges?.endDate) { internalFilterValue.value = quickSelectRanges; formData.startDate = formatFormDate(quickSelectRanges.startDate); formData.endDate = formatFormDate(quickSelectRanges.endDate); emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); } }; // Need to update the internal value and emit the date when using the date-picker input const onDateInputSelected = (selectedDate: BentoDateRangePickerValue) => { onDateSelected(selectedDate); emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const actions = computed( () => [ { title: t('apply') as string, event: updateFilter, disabled: isSelectingDate.value || isDateIncorrect.value || isApplyButtonDisabled.value, }, { title: (props.defaultValue ? t('reset') : t('clear')) as string, event: (props.defaultValue ? resetFilter : clearFilter) as () => void, disabled: isDateIncorrect.value && isSecondaryButtonDisabled.value, }, ] as BentoButtonActionsList ); const onDatePickerRangeError = () => { isDateIncorrect.value = true; internalFilterValue.value = { startDate: undefined, endDate: undefined }; emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const onDatePickerRangeFormDateInputUpdate = (dateFormDate: DateRangePickerCalendarFormData) => { isDateIncorrect.value = false; if (dateFormDate.startTime) { formData.startTime = dateFormDate.startTime; } if (dateFormDate.endTime) { formData.endTime = dateFormDate.endTime; } }; watch( () => props.value, newValue => { buttonSecondaryLabel.value = calculateSecondaryLabel(newValue); }, { deep: true } ); return { // Refs baseFilter, // Values actions, formData, internalFilterValue, isApplyButtonDisabled, isSecondaryButtonDisabled, isSelectingDate, buttonLabel, buttonSecondaryLabel, adjustedQuickSelectRanges, // Events onCancel, onDateSelected, openFilter, clearFilter, resetFilter, updateFilter, onDatePickerRangeError, onDatePickerRangeFormDateInputUpdate, onDateInputSelected, onRangeSelectorInput, }; }, }); </script> <style lang="scss" scoped src="./date-range-filter.scss" />
|
|
1
|
+
<template> <bento-base-filter ref="baseFilter" class="b-base-filter-date-range-filter" :button-label="buttonLabel" :button-secondary-label="buttonSecondaryLabel" :controlled="!onlySlot" :disabled="disabled" :disable-apply-button="isApplyButtonDisabled" :disable-secondary-button="isSecondaryButtonDisabled" :default-value="defaultValue" :field="field" :label="label" :only-slot="onlySlot" :tooltip-position="options?.tooltipPosition" :tooltip-text="options?.tooltipText" fit-content @open="openFilter" @clear="clearFilter" @reset="resetFilter" @cancel="onCancel" > <bento-date-range-picker v-if="onlySlot" :allow-time-input="options?.allowTimeInput" :model-value="internalFilterValue" :first-day-of-week="options?.firstDayOfWeek" :is-date-disabled="options?.isDateDisabled" :number-of-months="options?.numberOfMonths" :quick-select-ranges="options?.quickSelectRanges" :variant="options?.variant" :disabled="disabled" :granularities="options?.granularities" :max-range="options?.maxRange" :min="options?.min" :max="options?.max" :date-form-data="formData" :show-end-date-on-open="options?.showEndDateOnOpen" @update:model-value="onDateInputSelected" /> <template v-else> <div class="b-date-range-filter"> <date-range-picker-calendar :allow-time-input="options?.allowTimeInput" :first-day-of-week="options?.firstDayOfWeek" :is-date-disabled="options?.isDateDisabled" :granularities="options?.granularities" :max-range="options?.maxRange" :min="options?.min" :max="options?.max" :number-of-months="options?.numberOfMonths" :quick-select-ranges="adjustedQuickSelectRanges" :value="internalFilterValue" :date-form-data="formData" :show-end-date-on-open="options?.showEndDateOnOpen" :variant="options?.variant" @input="onDateSelected" @custom-range="onRangeSelectorInput" @error="onDatePickerRangeError" @form-date="onDatePickerRangeFormDateInputUpdate" @start-date-selected="isSelectingDate = true" > <template #title> <bento-typography strongest variant="body" el="span"> {{ label }} </bento-typography> </template> <template #actions> <bento-divider class="b-date-range-filter__divider" /> <bento-button-actions class="b-date-range-filter__actions" :actions="actions" layout="space-between" /> </template> </date-range-picker-calendar> </div> </template> </bento-base-filter> </template> <script lang="ts"> import { computed, defineComponent, type PropType, reactive, ref, toRef, watch } from 'vue'; import { BentoBaseFilter, useBaseFilter } from '../base-filter'; import { type BentoDateRangeFilterOptions, type BentoDateRangeFilterValue, BentoFilterEvent, } from '../../filter-bar.types'; import { DateRangePickerCalendar } from '@/components/date-range-picker/components/date-range-picker-calendar'; import { BentoDateRangePicker, type BentoDateRangePickerValue } from '@/components/date-range-picker'; import { BentoDivider } from '@/components/divider'; import { BentoTypography } from '@/components/typography'; import { BentoButtonActions, type BentoButtonActionsList } from '@/components/button/components/button-actions'; import { useI18n } from '@/utils/ts/i18n'; import { getDateLocale } from '@/utils/ts/get-date-locale/get-date-locale'; import { isSameDay } from 'date-fns/isSameDay'; import { format } from 'date-fns/format'; import { isEqual } from 'date-fns/isEqual'; import { isToday } from 'date-fns/isToday'; import { dateToInputDateString, dateToTimeInputString } from '@/utils/ts/format-date'; import messages from './messages.json'; import { type DateRangePickerCalendarFormData, type DateRangePickerCalendarRangeSelectorItem, type DateRangePickerCalendarRangeSelectorItems, } from '@/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.types'; type MessageSchema = (typeof messages)['en-US']; /** * The Date filter displays a calendar that allows * to select a single date. * * @example * import { BentoDateRangeFilter } from '@adyen/bento-vue2'; * * export default { * components: { BentoDateRangeFilter }, * template: ` * <bento-date-range-filter * :field="filter.field" * :label="filter.label" * :options="filter.options" * :value="filter.value" * @update="updateFilterHandler" * > * ` * } */ export default defineComponent({ i18n: { messages }, name: 'bento-date-range-filter', components: { BentoBaseFilter, BentoButtonActions, BentoDateRangePicker, BentoTypography, BentoDivider, DateRangePickerCalendar, }, props: { /** * Disables the filter from being interacted with. */ disabled: { type: Boolean, default: false }, /** * Default value of the filter to reset to. */ defaultValue: { type: Object as PropType<BentoDateRangeFilterValue>, default: null }, /** * Filter name/key referencing the actual property name in the data set. * Should be unique per filter bar. */ field: { type: String, required: true }, /** * Label text to be displayed */ label: { type: String, required: true }, /** * Renders the base filter's slot content only - leaving out the button and the popover. * Used for the allFiltersModal component. */ onlySlot: { type: Boolean, default: false }, /** * List of options that are needed to setup the filter * Available options: * - allowTimeInput: Allows user to enter time values in the form * - firstDayOfWeek: Allows user to set the first day of the week. 0 is sunday, 1 is monday, 6 is saturday * - isDateDisabled: Indicate if a date should be disabled or not * - maxRange: Set a maximum number of dates to be selectable by the range * - numberOfMonths: Number of months rendered on pane * - quickSelectRanges: An array that sets the custom range dropdown items. @see BentoDateRangePicker * - showEndDateOnOpen: Upon opening date range picker, end date's month will be pre-selected. * - granularities: A list of available granularities. * - variant: The type of calendar to display. Defaults to showing days. * - tooltipPosition: The position of the tooltip attached to the filter button * - tooltipText: The content of the tooltip attached to the filter button */ options: { type: Object as PropType<BentoDateRangeFilterOptions>, default: undefined, }, /** * Current value of the filter */ value: { type: Object as PropType<BentoDateRangeFilterValue>, default: () => ({ startDate: undefined, endDate: undefined }), }, }, emits: [BentoFilterEvent.INPUT, BentoFilterEvent.UPDATE], setup(props, { emit }) { const { locale, t } = useI18n<{ message: MessageSchema }>({ messages }); const locales = getDateLocale(); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled } = useBaseFilter( toRef(props, 'value'), toRef(props, 'defaultValue') ); const isSelectingDate = ref(false); const adjustedQuickSelectRanges = computed<Array<DateRangePickerCalendarRangeSelectorItem>>(() => { return ( props?.options?.quickSelectRanges && props.options.quickSelectRanges.map((range: DateRangePickerCalendarRangeSelectorItem) => { const timeDiff = !range.data.endDate ? new Date(Date.now()).getTime() - range.data.startDate.getTime() : undefined; return { ...range, data: { ...range.data, // Adding timeDifference to calculate an up-to-date endDate on range selection timeDifference: timeDiff, }, }; }) ); }); const baseFilter = ref(null); const isDateIncorrect = ref(false); const formData = reactive({ startDate: dateToInputDateString(props.value?.startDate), endDate: dateToInputDateString(props.value?.endDate), startTime: dateToTimeInputString(props.value?.startDate), endTime: dateToTimeInputString(props.value?.endDate), }); watch( () => props.value, () => { formData.startDate = dateToInputDateString(props.value?.startDate); formData.endDate = dateToInputDateString(props.value?.endDate); formData.startTime = dateToTimeInputString(props.value?.startDate); formData.endTime = dateToTimeInputString(props.value?.endDate); } ); function isQuickSelectAnArray( quickSelectRanges: BentoDateRangeFilterOptions['quickSelectRanges'] ): quickSelectRanges is DateRangePickerCalendarRangeSelectorItems { return (quickSelectRanges as DateRangePickerCalendarRangeSelectorItems)?.length !== undefined; } const calculateSecondaryLabel = (currentValue: BentoDateRangeFilterValue) => { if (!currentValue || (!currentValue?.startDate && !currentValue.endDate)) { return null; } // If quick select range selected then display its label if ( isQuickSelectAnArray(props.options?.quickSelectRanges) && props.options?.quickSelectRanges?.length > 0 ) { const customRangeSelected = props.options?.quickSelectRanges?.find(({ value, data }) => { if (currentValue) { if (currentValue.range) { return value === currentValue.range; } if (!currentValue.startDate || !currentValue.endDate) { return false; } if (!data?.endDate) { // Autoselecting the correct value for open-ended ranges return ( isEqual(data?.startDate, currentValue.startDate) && isToday(currentValue.endDate) ); } // If no range selected but start and end dates correspond to one of the preselected ranges, select it return ( isEqual(data?.startDate, currentValue.startDate) && isEqual(data?.endDate, currentValue.endDate) ); } return false; }); if (customRangeSelected) { return customRangeSelected.label; } } if (!currentValue.startDate || !currentValue.endDate) { return null; } return isSameDay(currentValue.startDate, currentValue.endDate) && props?.options?.allowTimeInput ? `${format(currentValue.startDate, 'dd MMM, yyyy HH:mm:ss', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })} - ${format(currentValue.endDate, 'HH:mm:ss', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })}` : `${format(currentValue.startDate, 'dd MMM, yyyy', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })} - ${format(currentValue.endDate, 'dd MMM, yyyy', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })}`; }; const buttonSecondaryLabel = ref(calculateSecondaryLabel(props.value)); const buttonLabel = computed(() => props.label); const updateFilter = () => { baseFilter.value.closePopover(); // Omit the onlySlot prop as it is not needed const { onlySlot, ...cleanedProps } = props; emit(BentoFilterEvent.UPDATE, { ...cleanedProps, value: internalFilterValue.value }); }; const clearFilter = () => { internalFilterValue.value = undefined; updateFilter(); }; const resetFilter = () => { internalFilterValue.value = props.defaultValue; updateFilter(); }; const openFilter = () => { internalFilterValue.value = props.value; }; // If we click outside or press escape this should cancel and reset the form inputs // set all internal states back to their initial values const onCancel = () => { internalFilterValue.value = props.value; isSelectingDate.value = false; formData.startDate = dateToInputDateString(props.value?.startDate); formData.endDate = dateToInputDateString(props.value?.endDate); formData.startTime = dateToTimeInputString(props.value?.startDate); formData.endTime = dateToTimeInputString(props.value?.endDate); }; const onDateSelected = (selectedDate: BentoDateRangePickerValue) => { isSelectingDate.value = false; internalFilterValue.value = selectedDate; formData.startDate = dateToInputDateString(selectedDate.startDate); formData.endDate = dateToInputDateString(selectedDate.endDate); emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const onRangeSelectorInput = (quickSelectRanges?: { startDate: Date; endDate: Date }) => { if (quickSelectRanges?.startDate && quickSelectRanges?.endDate) { internalFilterValue.value = quickSelectRanges; formData.startDate = dateToInputDateString(quickSelectRanges.startDate); formData.endDate = dateToInputDateString(quickSelectRanges.endDate); emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); } }; // Need to update the internal value and emit the date when using the date-picker input const onDateInputSelected = (selectedDate: BentoDateRangePickerValue) => { onDateSelected(selectedDate); emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const actions = computed( () => [ { title: t('apply') as string, event: updateFilter, disabled: isSelectingDate.value || isDateIncorrect.value || isApplyButtonDisabled.value, }, { title: (props.defaultValue ? t('reset') : t('clear')) as string, event: (props.defaultValue ? resetFilter : clearFilter) as () => void, disabled: isDateIncorrect.value && isSecondaryButtonDisabled.value, }, ] as BentoButtonActionsList ); const onDatePickerRangeError = () => { isDateIncorrect.value = true; internalFilterValue.value = { startDate: undefined, endDate: undefined }; emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const onDatePickerRangeFormDateInputUpdate = (dateFormDate: DateRangePickerCalendarFormData) => { isDateIncorrect.value = false; if (dateFormDate.startTime) { formData.startTime = dateFormDate.startTime; } if (dateFormDate.endTime) { formData.endTime = dateFormDate.endTime; } }; watch( () => props.value, newValue => { buttonSecondaryLabel.value = calculateSecondaryLabel(newValue); }, { deep: true } ); return { // Refs baseFilter, // Values actions, formData, internalFilterValue, isApplyButtonDisabled, isSecondaryButtonDisabled, isSelectingDate, buttonLabel, buttonSecondaryLabel, adjustedQuickSelectRanges, // Events onCancel, onDateSelected, openFilter, clearFilter, resetFilter, updateFilter, onDatePickerRangeError, onDatePickerRangeFormDateInputUpdate, onDateInputSelected, onRangeSelectorInput, }; }, }); </script> <style lang="scss" scoped src="./date-range-filter.scss" />
|
|
@@ -400,7 +400,7 @@ type.
|
|
|
400
400
|
|
|
401
401
|
#### Options
|
|
402
402
|
|
|
403
|
-
- `dynamicFiltering?: boolean`: Enables the search bar on popovers and search
|
|
403
|
+
- `dynamicFiltering?: boolean`: Enables the search bar on popovers and search behavior on the "all filters" modal.
|
|
404
404
|
**Default** is `true`
|
|
405
405
|
- `enableValueLabelPair?: boolean`: Enables value/label value handling. When `true`, the component expects and emits
|
|
406
406
|
value/label pair objects instead of primitive values. This affects both the `value` prop and the `input` event
|
package/dist/assets/components/form-layout/components/form-layout-title/form-layout-title.vue
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div class="b-form-layout-title" :class="conditionalClasses"> <bento-typography :el="el" variant="title" :medium="isSection"> <slot /> </bento-typography> <bento-typography v-if="hasSlot('description')" class="b-form-layout-title__description" el="span" wide> <slot name="description" /> </bento-typography> </div> </template> <script setup lang="ts"> import { computed, useSlots } from 'vue'; import { BentoTypography, BentoTypographyElement } from '@/components/typography'; import { useHasSlot } from '@/composables'; const slots = useSlots(); const props = defineProps({ /** * Defines if the Form Layout title is * a "section" or a "subsection". * Default is "section" (false). Turn into "true" for "subsection" */ subsection: { type: Boolean, default: false, }, /** * Sets the HTML DOM element. * @values h1, h2, h3, h4, h5, h6, div, p, span */ el: { type: BentoTypography.props.el.type, required: true, validator: (value: BentoTypographyElement) => Object.values(BentoTypographyElement).includes(value), }, }); // Uses "medium" typography if it is a section title const isSection = computed(() => !props.subsection); const hasSlot = useHasSlot(slots); const conditionalClasses = computed(() => ({ 'b-form-layout-title--subsection': props.subsection, })); </script> <script lang="ts"> /** * If you’re building a lengthy form,
|
|
1
|
+
<template> <div class="b-form-layout-title" :class="conditionalClasses"> <bento-typography :el="el" variant="title" :medium="isSection"> <slot /> </bento-typography> <bento-typography v-if="hasSlot('description')" class="b-form-layout-title__description" el="span" wide> <slot name="description" /> </bento-typography> </div> </template> <script setup lang="ts"> import { computed, useSlots } from 'vue'; import { BentoTypography, BentoTypographyElement } from '@/components/typography'; import { useHasSlot } from '@/composables'; const slots = useSlots(); const props = defineProps({ /** * Defines if the Form Layout title is * a "section" or a "subsection". * Default is "section" (false). Turn into "true" for "subsection" */ subsection: { type: Boolean, default: false, }, /** * Sets the HTML DOM element. * @values h1, h2, h3, h4, h5, h6, div, p, span */ el: { type: BentoTypography.props.el.type, required: true, validator: (value: BentoTypographyElement) => Object.values(BentoTypographyElement).includes(value), }, }); // Uses "medium" typography if it is a section title const isSection = computed(() => !props.subsection); const hasSlot = useHasSlot(slots); const conditionalClasses = computed(() => ({ 'b-form-layout-title--subsection': props.subsection, })); </script> <script lang="ts"> /** * If you’re building a lengthy form, organize related * fields into sections and, if needed, subsections. * * A form can have a "Section Title" and multiple "Subsection titles". * * One of the values provided in @see BentoTypographyElement must always be provided as a string * to define correct HTML sementic from the application's context * * @example * import { BentoFormLayoutTitle } from '@adyen/bento-vue2'; * * export default { * components: { BentoFormLayoutTitle }, * template: ` * <bento-form-layout-title subsection el="h2"> * Title * <template #description>Description text</template> * </bento-form-layout-title> * ` * } */ export default {}; </script> <style lang="scss" scoped src="./form-layout-title.scss" />
|
|
@@ -42,13 +42,13 @@ be placed within pages, modals, side panels and so on.
|
|
|
42
42
|
### Do not use
|
|
43
43
|
|
|
44
44
|
If you need just one or two input fields that sit within a bigger component, it's better to use those components
|
|
45
|
-
directly, as spacing here is
|
|
45
|
+
directly, as spacing here is optimized for larger forms/configuration flows.
|
|
46
46
|
|
|
47
47
|
## Variations
|
|
48
48
|
|
|
49
49
|
### Sections and sub sections
|
|
50
50
|
|
|
51
|
-
If you're building a lengthy form,
|
|
51
|
+
If you're building a lengthy form, organize related fields into sections and, if needed, subsections. This adds
|
|
52
52
|
additional context and makes filling it out easier.
|
|
53
53
|
|
|
54
54
|
An element (`el`) of type string corresponding to one of the values contained in `BentoTypographyElement` must always be
|
|
@@ -81,7 +81,7 @@ title for the page, though it needs to be unique per page.
|
|
|
81
81
|
`active-page` needs to be updated every time the Inspector window changes and must be kept in sync with
|
|
82
82
|
`update:active-page`.
|
|
83
83
|
|
|
84
|
-
**Important:** this feature is necessary for the correct accessibility
|
|
84
|
+
**Important:** this feature is necessary for the correct accessibility behavior and will be a required prop in
|
|
85
85
|
`Bento 2.0`
|
|
86
86
|
|
|
87
87
|
## Accessibility
|
|
@@ -107,7 +107,7 @@ Check the inspector's code example for a guideline on how to implement this.
|
|
|
107
107
|
- Focus moves to the Inspector first interactive element when Inspector is opened.
|
|
108
108
|
- Inspector first interactive element is `Return to main content` button, which allows the user to immediately leave the
|
|
109
109
|
Inspector and return to the main page.
|
|
110
|
-
- The `Tab` should change focus between interactive elements. This
|
|
110
|
+
- The `Tab` should change focus between interactive elements. This behavior should only exist when the Inspector is
|
|
111
111
|
open.
|
|
112
112
|
- User can use `Ctrl + Shift + S` to move between main page and inspector.
|
|
113
113
|
- When user tabs through all elements of the Inspector, focus returns to the trigger.
|
|
@@ -23,9 +23,9 @@ when the user clicks on will copy the content to the clipboard. Useful, for exam
|
|
|
23
23
|
<Story of={CopyStories.WithSlot} />
|
|
24
24
|
</Canvas>
|
|
25
25
|
|
|
26
|
-
## Button
|
|
26
|
+
## Button customization
|
|
27
27
|
|
|
28
|
-
The copy button variation can be
|
|
28
|
+
The copy button variation can be customized using the `buttonText` and `button` properties.
|
|
29
29
|
|
|
30
30
|
<Canvas withSource="closed">
|
|
31
31
|
<Story of={CopyStories.WithCustomButton} />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <Transition :name="animation" mode="out-in" @after-leave="updateAnimation" @leave="startAnimation"> <div v-if="isOpen && showPage" class="b-dialog-page"> <div v-if="hasSlot('header')" class="b-dialog-page__header" :class="headerConditionalClasses"> <div v-if="showBackButton" class="b-dialog-page__back-button" :style="backButtonStyling" data-testid="back-button" > <bento-button variant="tertiary" @click="goToPreviousPage(previousPage)"> <arrow-left-icon :svg-title="t('goToPreviousPage')" /> </bento-button> </div> <slot name="header"></slot> </div> <div v-if="hasSlot('content')" ref="contentElement" class="b-dialog-page__content" :tabindex="isScrollable ? 0 : null" @scroll="onScroll" > <slot name="content" /> </div> <div v-if="hasSlot('footer')" class="b-dialog-page__footer" :class="footerConditionalClasses"> <slot name="footer" /> </div> </div> </Transition> </template> <script setup lang="ts"> import { computed, inject, nextTick, onUnmounted, type PropType, ref, useSlots, watch } from 'vue'; import { BentoButton } from '@/components/button'; import ArrowLeftIcon from '@adyen/ui-assets-icons-16/vue/arrow-left'; import { useHasSlot } from '@/composables/use-has-slot/use-has-slot'; import { useI18n } from '@/utils/ts/i18n'; import { isDevelopmentEnviroment } from '@/utils/ts/dev-environment'; import { BentoDialogPageAnimation, type BentoDialogPageConfigData, type BentoDialogPageRegisterPage, } from './dialog-page.types'; import type { CSSProperties } from 'vue/types/jsx.d.ts'; import { DIALOG_PAGE_CONFIG_INJECTION_KEY, DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY } from './dialog-page.keys'; import messages from './messages.json'; import { useScroll } from '@vueuse/core'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * Identifier of the page, used with activePage prop to handle navigation */ pageId: { type: String, default: undefined, }, /** * Show header and actions border */ alwaysShowBorder: { type: Boolean, default: false, }, /** * Pass custom styling to the back button to position it correctly with the header slot */ backButtonStyling: { type: Object as PropType<CSSProperties>, default: null }, /** * ID of the page for the back button to navigate to. Only use it to overwrite the default back behavior * Used when the back button should not take the user to the natural previous page * Set to `null` to hide back button in the page */ previousPage: { type: String, default: undefined, }, }); const emit = defineEmits<{ /** * Fires when the animation ends */ (e: 'animationend'): void; }>(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const { t } = useI18n<{ message: MessageSchema }>({ messages }); // Refs const contentElement = ref<HTMLDivElement>(null); const hasScroll = ref(false); const { arrivedState } = useScroll(contentElement); // Logic to choose which page is active/shown const showPage = computed(() => (activePage.value ? activePage.value === props.pageId : true)); // Scrolling styling logic const headerConditionalClasses = computed(() => ({ 'b-dialog-page__header--with-border': props.alwaysShowBorder || (hasScroll.value && !arrivedState.top), })); const footerConditionalClasses = computed(() => ({ 'b-dialog-page__footer--with-border': props.alwaysShowBorder || (hasScroll.value && !arrivedState.bottom), })); const isScrollable = computed(() => !(arrivedState.bottom && arrivedState.top)); const onScroll = () => { if (!contentElement.value) { return; } }; const calculateScroll = async watchedValue => { if (!watchedValue) { return; } // Wait for content to be rendered to be able to calculate the height await nextTick(); hasScroll.value = contentElement.value && contentElement.value.scrollHeight > contentElement.value.clientHeight; }; const { activePage, animation, isAnimationActive, isOpen, pages, previousPages, updatePage } = inject<BentoDialogPageConfigData>(DIALOG_PAGE_CONFIG_INJECTION_KEY); watch(() => isOpen.value, calculateScroll, { immediate: true }); watch(() => showPage.value, calculateScroll, { immediate: true }); // Logic to determine whether the back button should be shown on the page const showBackButton = computed(() => { if (props.previousPage) { return true; } if (props.previousPage === null) { return false; } return hasPreviousPages.value && showPage.value; }); // Animation const updateAnimation = () => { animation.value = BentoDialogPageAnimation.TRANSITION_LEVEL_DEEPER; if (isAnimationActive) { isAnimationActive.value = false; } emit('animationend'); }; const startAnimation = () => { if (isAnimationActive) { isAnimationActive.value = true; } }; // Previous page logic const hasPreviousPages = computed(() => previousPages.value.length > 1); const lastPage = computed(() => previousPages.value[previousPages.value.length - 1]); const isActivePageValid = computed(() => pages.value.some(page => page === activePage.value)); const goToPreviousPage = async (page: string) => { if (page) { const index = previousPages.value.indexOf(page); if (index !== -1) { previousPages.value = previousPages.value.slice(0, index + 1); } else { previousPages.value = [page]; } } else { previousPages.value.pop(); } animation.value = BentoDialogPageAnimation.TRANSITION_LEVEL_UPPER; await nextTick(); updatePage(lastPage.value); }; // Add new page to the previousPage array to keep track of user navigation watch( () => activePage.value, async () => { await nextTick(); if (!isOpen.value) { return; } if (!!activePage.value && !isActivePageValid.value) { if (isDevelopmentEnviroment) { // eslint-disable-next-line no-console console.error( `The activePage passed "${activePage.value}" does not match the id of any of the pages` ); return; } } if (activePage.value !== lastPage.value) { previousPages.value.push(activePage.value); } }, { immediate: true } ); // Register page to parent component and inject data from parent component const registerPage = inject<BentoDialogPageRegisterPage>(DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY); const { unregisterPage } = registerPage(props.pageId); // Unregister page on unmounting the page onUnmounted(() => unregisterPage()); </script> <script lang="ts"> /** * DialogPage is an internal component used to render different pages of dialog components (Modal, ModalFullscreen and Sidepanel) * * @example * import { DialogPage } from '@/internal'; * * export default { * components: { DialogPage }, * template: ` * <dialog-page pageId='step1' always-show-border> * <template #header>Header</template> * <template #content>Content</template> * <template #footer>Footer</template> * </dialog-page> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./dialog-page.scss" />
|
|
1
|
+
<template> <Transition :name="animation" mode="out-in" @after-leave="updateAnimation" @leave="startAnimation"> <div v-if="isOpen && showPage" class="b-dialog-page"> <div v-if="hasSlot('header')" class="b-dialog-page__header" :class="headerConditionalClasses"> <div v-if="showBackButton" class="b-dialog-page__back-button" :style="backButtonStyling" data-testid="back-button" > <bento-button variant="tertiary" @click="goToPreviousPage(previousPage)"> <arrow-left-icon :svg-title="t('goToPreviousPage')" /> </bento-button> </div> <slot name="header"></slot> </div> <div v-if="hasSlot('content')" ref="contentElement" class="b-dialog-page__content" :tabindex="isScrollable ? 0 : null" @scroll="onScroll" > <slot name="content" /> </div> <div v-if="hasSlot('footer')" class="b-dialog-page__footer" :class="footerConditionalClasses"> <slot name="footer" /> </div> </div> </Transition> </template> <script setup lang="ts"> import { computed, inject, nextTick, onMounted, onUnmounted, type PropType, ref, useSlots, watch } from 'vue'; import { BentoButton } from '@/components/button'; import ArrowLeftIcon from '@adyen/ui-assets-icons-16/vue/arrow-left'; import { useHasSlot } from '@/composables/use-has-slot/use-has-slot'; import { useI18n } from '@/utils/ts/i18n'; import { isDevelopmentEnviroment } from '@/utils/ts/dev-environment'; import { BentoDialogPageAnimation, type BentoDialogPageConfigData, type BentoDialogPageRegisterPage, } from './dialog-page.types'; import type { CSSProperties } from 'vue/types/jsx.d.ts'; import { DIALOG_PAGE_CONFIG_INJECTION_KEY, DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY } from './dialog-page.keys'; import messages from './messages.json'; import { useScroll } from '@vueuse/core'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * Identifier of the page, used with activePage prop to handle navigation */ pageId: { type: String, default: undefined, }, /** * Show header and actions border */ alwaysShowBorder: { type: Boolean, default: false, }, /** * Pass custom styling to the back button to position it correctly with the header slot */ backButtonStyling: { type: Object as PropType<CSSProperties>, default: null }, /** * ID of the page for the back button to navigate to. Only use it to overwrite the default back behavior * Used when the back button should not take the user to the natural previous page * Set to `null` to hide back button in the page */ previousPage: { type: String, default: undefined, }, }); const emit = defineEmits<{ /** * Fires when the animation ends */ (e: 'animationend'): void; }>(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const { t } = useI18n<{ message: MessageSchema }>({ messages }); // Refs const contentElement = ref<HTMLDivElement>(null); const hasScroll = ref(false); const isMounted = ref(false); const { arrivedState } = useScroll(contentElement); // Logic to choose which page is active/shown const showPage = computed(() => (activePage.value ? activePage.value === props.pageId : true)); // Scrolling styling logic const headerConditionalClasses = computed(() => ({ 'b-dialog-page__header--with-border': props.alwaysShowBorder || (hasScroll.value && !arrivedState.top), })); const footerConditionalClasses = computed(() => ({ 'b-dialog-page__footer--with-border': props.alwaysShowBorder || (hasScroll.value && !arrivedState.bottom), })); const isScrollable = computed(() => !(arrivedState.bottom && arrivedState.top)); const onScroll = () => { if (!contentElement.value) { return; } }; const calculateScroll = async watchedValue => { if (!watchedValue) { return; } // Wait for content to be rendered to be able to calculate the height await nextTick(); if (!isMounted.value) { return; } hasScroll.value = contentElement.value && contentElement.value.scrollHeight > contentElement.value.clientHeight; }; const { activePage, animation, isAnimationActive, isOpen, pages, previousPages, updatePage } = inject<BentoDialogPageConfigData>(DIALOG_PAGE_CONFIG_INJECTION_KEY); watch(() => isOpen.value, calculateScroll, { immediate: true }); watch(() => showPage.value, calculateScroll, { immediate: true }); // Logic to determine whether the back button should be shown on the page const showBackButton = computed(() => { if (props.previousPage) { return true; } if (props.previousPage === null) { return false; } return hasPreviousPages.value && showPage.value; }); // Animation const updateAnimation = () => { animation.value = BentoDialogPageAnimation.TRANSITION_LEVEL_DEEPER; if (isAnimationActive) { isAnimationActive.value = false; } emit('animationend'); }; const startAnimation = () => { if (isAnimationActive) { isAnimationActive.value = true; } }; // Previous page logic const hasPreviousPages = computed(() => previousPages.value.length > 1); const lastPage = computed(() => previousPages.value[previousPages.value.length - 1]); const isActivePageValid = computed(() => pages.value.some(page => page === activePage.value)); const goToPreviousPage = async (page: string) => { if (page) { const index = previousPages.value.indexOf(page); if (index !== -1) { previousPages.value = previousPages.value.slice(0, index + 1); } else { previousPages.value = [page]; } } else { previousPages.value.pop(); } animation.value = BentoDialogPageAnimation.TRANSITION_LEVEL_UPPER; await nextTick(); if (!isMounted.value) { return; } updatePage(lastPage.value); }; // Add new page to the previousPage array to keep track of user navigation watch( () => activePage.value, async () => { await nextTick(); if (!isMounted.value || !isOpen.value) { return; } if (!!activePage.value && !isActivePageValid.value) { if (isDevelopmentEnviroment) { // eslint-disable-next-line no-console console.error( `The activePage passed "${activePage.value}" does not match the id of any of the pages` ); return; } } if (activePage.value !== lastPage.value) { previousPages.value.push(activePage.value); } }, { immediate: true } ); // Register page to parent component and inject data from parent component const registerPage = inject<BentoDialogPageRegisterPage>(DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY); const { unregisterPage } = registerPage(props.pageId); onMounted(() => { isMounted.value = true; }); // Unregister page on unmounting the page onUnmounted(() => { isMounted.value = false; unregisterPage(); }); </script> <script lang="ts"> /** * DialogPage is an internal component used to render different pages of dialog components (Modal, ModalFullscreen and Sidepanel) * * @example * import { DialogPage } from '@/internal'; * * export default { * components: { DialogPage }, * template: ` * <dialog-page pageId='step1' always-show-border> * <template #header>Header</template> * <template #content>Content</template> * <template #footer>Footer</template> * </dialog-page> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./dialog-page.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div ref="fixedScrollerRef" class="b-fixed-scroller"> <div class="b-fixed-scroller__container"> <div ref="contentRef" class="b-fixed-scroller__content" :style="{ transform: `translateX(${offset}px)`, paddingTop: shouldShowLeftButton || shouldShowRightButton ? `${extraPaddingTop}px` : null, }" > <slot></slot> </div> </div> <div v-if="shouldShowLeftButton" ref="leftButtonRef" class="b-fixed-scroller__left"> <bento-button variant="secondary" :aria-label="t('scrollLeft')" :condensed="condensed" @click="scrollLeft"> <template #iconLeft> <chevron-left-icon svg-title="checked" aria-hidden="true" /> </template> </bento-button> </div> <div v-if="shouldShowRightButton" ref="rightButtonRef" class="b-fixed-scroller__right"> <bento-button variant="secondary" :aria-label="t('scrollRight')" :condensed="condensed" @click="scrollRight" > <template #iconLeft> <chevron-right-icon svg-title="checked" aria-hidden="true" /> </template> </bento-button> </div> </div> </template> <script setup lang="ts"> import { ref } from 'vue'; import { BentoButton } from '@/components/button'; import { useFixedScroller } from './composables'; import { useI18n } from '@/utils/ts/i18n'; import ChevronLeftIcon from '@adyen/ui-assets-icons-16/vue/chevron-left'; import ChevronRightIcon from '@adyen/ui-assets-icons-16/vue/chevron-right'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const contentRef = ref(null); const fixedScrollerRef = ref(null); const leftButtonRef = ref(null); const rightButtonRef = ref(null); defineProps({ /** * Applies the condensed style to the buttons */ condensed: { type: Boolean, default: false }, /** * Padding to be applied to the top of the scroller container */ extraPaddingTop: { type: Number, default: 0 }, }); const { offset, shouldShowLeftButton, shouldShowRightButton, scrollLeft, scrollRight } = useFixedScroller({ contentRef, containerRef: fixedScrollerRef, leftButtonRef, rightButtonRef, }); </script> <script lang="ts"> /** * Allows to horizontally scroll content by one view at a time. * * @example * import { FixedScroller } from '@/internal'; * * export default { * components: { FixedScroller }, * template: ` * <fixed-scroller> * <div style="display: flex; gap: 8px;"> * {{ Content here }} * </div> * </fixed-scroller> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./fixed-scroller.scss" />
|
|
1
|
+
<template> <div ref="fixedScrollerRef" class="b-fixed-scroller"> <div class="b-fixed-scroller__container"> <div ref="contentRef" class="b-fixed-scroller__content" :style="{ transform: `translateX(${offset}px)`, paddingTop: shouldShowLeftButton || shouldShowRightButton ? `${extraPaddingTop}px` : null, }" > <slot></slot> </div> </div> <div v-if="shouldShowLeftButton" ref="leftButtonRef" class="b-fixed-scroller__left b-fixed-scroller__button" :class="buttonConditionalClasses" > <bento-button variant="secondary" :aria-label="t('scrollLeft')" :condensed="condensed" @click="scrollLeft"> <template #iconLeft> <chevron-left-icon svg-title="checked" aria-hidden="true" /> </template> </bento-button> </div> <div v-if="shouldShowRightButton" ref="rightButtonRef" class="b-fixed-scroller__right b-fixed-scroller__button" :class="buttonConditionalClasses" > <bento-button variant="secondary" :aria-label="t('scrollRight')" :condensed="condensed" @click="scrollRight" > <template #iconLeft> <chevron-right-icon svg-title="checked" aria-hidden="true" /> </template> </bento-button> </div> </div> </template> <script setup lang="ts"> import { computed, ref } from 'vue'; import { BentoButton } from '@/components/button'; import { useFixedScroller } from './composables'; import { useI18n } from '@/utils/ts/i18n'; import ChevronLeftIcon from '@adyen/ui-assets-icons-16/vue/chevron-left'; import ChevronRightIcon from '@adyen/ui-assets-icons-16/vue/chevron-right'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const contentRef = ref(null); const fixedScrollerRef = ref(null); const leftButtonRef = ref(null); const rightButtonRef = ref(null); const props = defineProps({ /** * Applies the condensed style to the buttons */ condensed: { type: Boolean, default: false }, /** * Padding to be applied to the top of the scroller container */ extraPaddingTop: { type: Number, default: 0 }, /** * Padding to be applied to the top of the scroller container */ centered: { type: Boolean, default: false }, }); const { offset, shouldShowLeftButton, shouldShowRightButton, scrollLeft, scrollRight } = useFixedScroller({ contentRef, containerRef: fixedScrollerRef, leftButtonRef, rightButtonRef, }); const buttonConditionalClasses = computed(() => ({ 'b-fixed-scroller__button--centered': props.centered, })); </script> <script lang="ts"> /** * Allows to horizontally scroll content by one view at a time. * * @example * import { FixedScroller } from '@/internal'; * * export default { * components: { FixedScroller }, * template: ` * <fixed-scroller> * <div style="display: flex; gap: 8px;"> * {{ Content here }} * </div> * </fixed-scroller> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./fixed-scroller.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div> <div v-for="option in items" :key="`selected-${option.value}`"> <template v-if="searching && isMultiLevel"> <bento-listbox-option class="b-listbox-multi-select-options" :class="conditionalClasses" :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :tabindex="computedTabindex(option.value)" :aria-selected="`${isOptionSelected(option)}`" :aria-checked="`${isOptionSelected(option)}`" :aria-disabled="isOptionDisabled ? isOptionDisabled(option) : null" :role="$attrs['role']" @click="toggleSearchOption(option)" @enter-pressed="toggleSearchOption(option)" @space-pressed.prevent="toggleSearchOption(option)" > <decorative-checkbox :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :checked="isOptionSelected(option)" /> <slot v-bind="option"> <span class="b-listbox-multi-select__option-text"> <bento-typography el="span">{{ option.label }}</bento-typography> <bento-typography v-if="shouldShowDescription(option)" class="b-listbox-multi-select__option-description" el="span" > {{ option.description }} </bento-typography> </span> </slot> </bento-listbox-option> </template> <template v-else> <!-- Category --> <template v-if="option.items"> <listbox-multi-select-category :category="option" :static-categories="staticCategories" :is-option-disabled="isOptionDisabled" :selected-items-list="selectedItemsList" :tabindex="computedTabindex(option.value)" @select-category="$emit(ListboxMultiSelectOptionsEvent.SELECT_CATEGORY, option)" @unselect-category="$emit(ListboxMultiSelectOptionsEvent.UNSELECT_CATEGORY, option)" @toggle-category-visibility=" $emit(ListboxMultiSelectOptionsEvent.TOGGLE_CATEGORY_VISIBILITY, $event) " > <listbox-multi-select-options category-items :items="option.items" :is-option-disabled="isOptionDisabled" :items-index="itemsIndex" :focused-index="focusedIndex" :is-multi-level="isMultiLevel" :static-categories="staticCategories" :selected-items-list="selectedItemsList" :searching="searching" :role="$attrs['role']" @toggle-checkbox-selection="value => toggleCheckboxSelection(value)" /> </listbox-multi-select-category> </template> <!-- Option --> <bento-listbox-option v-else class="b-listbox-multi-select-options" :class="conditionalClasses" :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :tabindex="computedTabindex(option.value)" :aria-selected="`${isOptionSelected(option)}`" :aria-checked="`${isOptionSelected(option)}`" :aria-disabled="isOptionDisabled ? isOptionDisabled(option) : null" :role="$attrs['role']" @click="toggleCheckboxSelection(option)" @enter-pressed="toggleCheckboxSelection(option)" @space-pressed.prevent="toggleCheckboxSelection(option)" > <decorative-checkbox :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :checked="isOptionSelected(option)" /> <slot v-bind="option"> <span class="b-listbox-multi-select__option-text"> <bento-typography el="span">{{ option.label }}</bento-typography> <bento-typography v-if="shouldShowDescription(option)" class="b-listbox-multi-select__option-description" el="span" > {{ option.description }} </bento-typography> </span> </slot> </bento-listbox-option> </template> </div> </div> </template> <script lang="ts"> import { computed, defineComponent, type PropType } from 'vue'; import { BentoListboxOption } from '../../../listbox-option'; import { BentoTypography } from '@/components/typography'; import { ListboxMultiSelectCategory } from './components/listbox-multi-select-category'; import { DecorativeCheckbox } from '@/internal/decorative-checkbox'; import { BentoListboxEvent, type BentoListboxIsOptionDisabled, type BentoListboxOptionItem, type BentoListboxOptions, type BentoListboxValue, } from '@/types/listbox'; import { ListboxMultiSelectOptionsEvent } from './listbox-multi-select-options.types'; /** * Listbox multi-select options list * * @example * import { ListboxMultiSelectOptions } from './components/listbox-multi-select-options'; * * export default { * components: { ListboxMultiSelect }, * template: ` * <listbox-multi-select * :items="[{ label: 'Option 1', value: 'option-1' }]" * :selected-values="['option-1']" * /> * ` * } */ export default defineComponent({ name: 'listbox-multi-select-options', components: { BentoListboxOption, BentoTypography, ListboxMultiSelectCategory, DecorativeCheckbox, }, inheritAttrs: false, props: { /** * Indicates if the items listed are inside a category * so they can be padded to right by one level in recursion. * Items of categories are automatically defined as "categoryItems" */ categoryItems: { type: Boolean, default: false }, /** * Current focused option index */ focusedIndex: { type: Number, default: 0 }, /** * Function that allows the options to be disabled * * @type {BentoListboxIsOptionDisabled} * @param {BentoListboxOptionItem} option - listbox option object * @param {string} option.label - Option's label text * @param {string|number} option.value - Option's value * @param {unknown} option.data - Option's extra data to be passed to the default slot */ isOptionDisabled: { type: Function as PropType<BentoListboxIsOptionDisabled>, default: () => false, }, /** * The option elements to populate the listbox with. * It must be an array of {@see BentoListboxOptionItem }. * * @property {string} value.label - Text to be displayed in the option * @property {string,number} value.value - Value of the option */ items: { type: Array as PropType<BentoListboxOptions>, required: true, }, /** * Indicates if the elements options are inside a multi-level category */ isMultiLevel: { type: Boolean, default: false }, /** * Flag that indicates that an external search it's being made * to filter out the items inside the the multi-select listbox * * If enabled, hides the "Select all" checkbox in "multiple" mode. */ searching: { type: Boolean, default: false }, /** * List of selected items. * Providing an empty array will select no options. */ selectedItemsList: { type: Array as PropType<Array<BentoListboxValue>>, default: () => [], }, /** * If enabled, dropdown will display non-selectable, static categories */ staticCategories: { type: Boolean, default: false }, /** * Object with the index of each item. */ itemsIndex: { type: Object as PropType<Record<string, number>>, default: () => ({}), }, }, emits: [ ListboxMultiSelectOptionsEvent.TOGGLE_CHECKBOX_SELECTION, ListboxMultiSelectOptionsEvent.SELECT_CATEGORY, ListboxMultiSelectOptionsEvent.UNSELECT_CATEGORY, ListboxMultiSelectOptionsEvent.TOGGLE_CATEGORY_VISIBILITY, ], setup(props, { emit }) { const toggleCheckboxSelection = (option: BentoListboxOptionItem) => { if (props.isOptionDisabled(option)) { return; } emit(ListboxMultiSelectOptionsEvent.TOGGLE_CHECKBOX_SELECTION, option); }; const isCategory = (option: BentoListboxOptionItem) => !!option.items; const shouldShowDescription = (option: BentoListboxOptionItem) => { const isSearchingMultiLevel = props.isMultiLevel && props.searching; return option.description && (!props.isMultiLevel || isSearchingMultiLevel); }; const isOptionSelected = option => { if (option.items) { return !!option.items.find(item => props.selectedItemsList.includes(item.value)); } return props.selectedItemsList.includes(option.value); }; const toggleSearchOption = option => { if (isCategory(option)) { const event = isOptionSelected(option) ? ListboxMultiSelectOptionsEvent.UNSELECT_CATEGORY : ListboxMultiSelectOptionsEvent.SELECT_CATEGORY; emit(event, option); } else { toggleCheckboxSelection(option); } }; const conditionalClasses = computed(() => ({ 'b-listbox-multi-select-options__static-category-option': props.staticCategories, 'b-listbox-multi-select-options__category-option': props.categoryItems, })); const computedTabindex = value => (props.itemsIndex[value] === props.focusedIndex ? 0 : -1); return { // Values conditionalClasses, // Methods isOptionSelected, shouldShowDescription, toggleCheckboxSelection, toggleSearchOption, computedTabindex, // Enums BentoListboxEvent, ListboxMultiSelectOptionsEvent, }; }, }); </script> <style lang="scss" scoped src="./listbox-multi-select-options.scss" />
|
|
1
|
+
<template> <div> <div v-for="option in items" :key="`selected-${option.value}`"> <template v-if="searching && isMultiLevel"> <bento-listbox-option class="b-listbox-multi-select-options" :class="conditionalClasses" :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :tabindex="computedTabindex(option.value)" :aria-selected="`${isOptionSelected(option)}`" :aria-checked="`${isOptionSelected(option)}`" :aria-disabled="isOptionDisabled ? isOptionDisabled(option) : null" :role="$attrs['role'] || 'option'" @click="toggleSearchOption(option)" @enter-pressed="toggleSearchOption(option)" @space-pressed.prevent="toggleSearchOption(option)" > <decorative-checkbox :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :checked="isOptionSelected(option)" /> <slot v-bind="option"> <span class="b-listbox-multi-select__option-text"> <bento-typography el="span">{{ option.label }}</bento-typography> <bento-typography v-if="shouldShowDescription(option)" class="b-listbox-multi-select__option-description" el="span" > {{ option.description }} </bento-typography> </span> </slot> </bento-listbox-option> </template> <template v-else> <!-- Category --> <template v-if="option.items"> <listbox-multi-select-category :category="option" :static-categories="staticCategories" :is-option-disabled="isOptionDisabled" :selected-items-list="selectedItemsList" :tabindex="computedTabindex(option.value)" @select-category="$emit(ListboxMultiSelectOptionsEvent.SELECT_CATEGORY, option)" @unselect-category="$emit(ListboxMultiSelectOptionsEvent.UNSELECT_CATEGORY, option)" @toggle-category-visibility=" $emit(ListboxMultiSelectOptionsEvent.TOGGLE_CATEGORY_VISIBILITY, $event) " > <listbox-multi-select-options category-items :items="option.items" :is-option-disabled="isOptionDisabled" :items-index="itemsIndex" :focused-index="focusedIndex" :is-multi-level="isMultiLevel" :static-categories="staticCategories" :selected-items-list="selectedItemsList" :searching="searching" :role="$attrs['role'] || 'option'" @toggle-checkbox-selection="value => toggleCheckboxSelection(value)" /> </listbox-multi-select-category> </template> <!-- Option --> <bento-listbox-option v-else class="b-listbox-multi-select-options" :class="conditionalClasses" :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :tabindex="computedTabindex(option.value)" :aria-selected="`${isOptionSelected(option)}`" :aria-checked="`${isOptionSelected(option)}`" :aria-disabled="isOptionDisabled ? isOptionDisabled(option) : null" :role="$attrs['role'] || 'option'" @click="toggleCheckboxSelection(option)" @enter-pressed="toggleCheckboxSelection(option)" @space-pressed.prevent="toggleCheckboxSelection(option)" > <decorative-checkbox :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :checked="isOptionSelected(option)" /> <slot v-bind="option"> <span class="b-listbox-multi-select__option-text"> <bento-typography el="span">{{ option.label }}</bento-typography> <bento-typography v-if="shouldShowDescription(option)" class="b-listbox-multi-select__option-description" el="span" > {{ option.description }} </bento-typography> </span> </slot> </bento-listbox-option> </template> </div> </div> </template> <script lang="ts"> import { computed, defineComponent, type PropType } from 'vue'; import { BentoListboxOption } from '../../../listbox-option'; import { BentoTypography } from '@/components/typography'; import { ListboxMultiSelectCategory } from './components/listbox-multi-select-category'; import { DecorativeCheckbox } from '@/internal/decorative-checkbox'; import { BentoListboxEvent, type BentoListboxIsOptionDisabled, type BentoListboxOptionItem, type BentoListboxOptions, type BentoListboxValue, } from '@/types/listbox'; import { ListboxMultiSelectOptionsEvent } from './listbox-multi-select-options.types'; /** * Listbox multi-select options list * * @example * import { ListboxMultiSelectOptions } from './components/listbox-multi-select-options'; * * export default { * components: { ListboxMultiSelect }, * template: ` * <listbox-multi-select * :items="[{ label: 'Option 1', value: 'option-1' }]" * :selected-values="['option-1']" * /> * ` * } */ export default defineComponent({ name: 'listbox-multi-select-options', components: { BentoListboxOption, BentoTypography, ListboxMultiSelectCategory, DecorativeCheckbox, }, inheritAttrs: false, props: { /** * Indicates if the items listed are inside a category * so they can be padded to right by one level in recursion. * Items of categories are automatically defined as "categoryItems" */ categoryItems: { type: Boolean, default: false }, /** * Current focused option index */ focusedIndex: { type: Number, default: 0 }, /** * Function that allows the options to be disabled * * @type {BentoListboxIsOptionDisabled} * @param {BentoListboxOptionItem} option - listbox option object * @param {string} option.label - Option's label text * @param {string|number} option.value - Option's value * @param {unknown} option.data - Option's extra data to be passed to the default slot */ isOptionDisabled: { type: Function as PropType<BentoListboxIsOptionDisabled>, default: () => false, }, /** * The option elements to populate the listbox with. * It must be an array of {@see BentoListboxOptionItem }. * * @property {string} value.label - Text to be displayed in the option * @property {string,number} value.value - Value of the option */ items: { type: Array as PropType<BentoListboxOptions>, required: true, }, /** * Indicates if the elements options are inside a multi-level category */ isMultiLevel: { type: Boolean, default: false }, /** * Flag that indicates that an external search it's being made * to filter out the items inside the the multi-select listbox * * If enabled, hides the "Select all" checkbox in "multiple" mode. */ searching: { type: Boolean, default: false }, /** * List of selected items. * Providing an empty array will select no options. */ selectedItemsList: { type: Array as PropType<Array<BentoListboxValue>>, default: () => [], }, /** * If enabled, dropdown will display non-selectable, static categories */ staticCategories: { type: Boolean, default: false }, /** * Object with the index of each item. */ itemsIndex: { type: Object as PropType<Record<string, number>>, default: () => ({}), }, }, emits: [ ListboxMultiSelectOptionsEvent.TOGGLE_CHECKBOX_SELECTION, ListboxMultiSelectOptionsEvent.SELECT_CATEGORY, ListboxMultiSelectOptionsEvent.UNSELECT_CATEGORY, ListboxMultiSelectOptionsEvent.TOGGLE_CATEGORY_VISIBILITY, ], setup(props, { emit }) { const toggleCheckboxSelection = (option: BentoListboxOptionItem) => { if (props.isOptionDisabled(option)) { return; } emit(ListboxMultiSelectOptionsEvent.TOGGLE_CHECKBOX_SELECTION, option); }; const isCategory = (option: BentoListboxOptionItem) => !!option.items; const shouldShowDescription = (option: BentoListboxOptionItem) => { const isSearchingMultiLevel = props.isMultiLevel && props.searching; return option.description && (!props.isMultiLevel || isSearchingMultiLevel); }; const isOptionSelected = option => { if (option.items) { return !!option.items.find(item => props.selectedItemsList.includes(item.value)); } return props.selectedItemsList.includes(option.value); }; const toggleSearchOption = option => { if (isCategory(option)) { const event = isOptionSelected(option) ? ListboxMultiSelectOptionsEvent.UNSELECT_CATEGORY : ListboxMultiSelectOptionsEvent.SELECT_CATEGORY; emit(event, option); } else { toggleCheckboxSelection(option); } }; const conditionalClasses = computed(() => ({ 'b-listbox-multi-select-options__static-category-option': props.staticCategories, 'b-listbox-multi-select-options__category-option': props.categoryItems, })); const computedTabindex = value => (props.itemsIndex[value] === props.focusedIndex ? 0 : -1); return { // Values conditionalClasses, // Methods isOptionSelected, shouldShowDescription, toggleCheckboxSelection, toggleSearchOption, computedTabindex, // Enums BentoListboxEvent, ListboxMultiSelectOptionsEvent, }; }, }); </script> <style lang="scss" scoped src="./listbox-multi-select-options.scss" />
|
|
@@ -5,7 +5,7 @@ import * as Layout from './layout.stories';
|
|
|
5
5
|
|
|
6
6
|
# Layout
|
|
7
7
|
|
|
8
|
-
Layout is a basic scaffolding of a page that defines spacing, some functionality and responsive
|
|
8
|
+
Layout is a basic scaffolding of a page that defines spacing, some functionality and responsive behavior of its top
|
|
9
9
|
level sections.
|
|
10
10
|
|
|
11
11
|
Layouts are responsive and use media queries to determine the layout, as well as margins and gutter sizes.
|
|
@@ -29,9 +29,9 @@ columns) or broken up into two uneven (3+9, 9+3, 4+8 or 8+4) or two even (6+6) c
|
|
|
29
29
|
|
|
30
30
|
It's also possible to mix and match the different layouts in multiple rows, for example:
|
|
31
31
|
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
32
|
+
- First row: 12 columns,
|
|
33
|
+
- Second row: 6 + 6 columns,
|
|
34
|
+
- Third row: 12 columns, and so on
|
|
35
35
|
|
|
36
36
|
and so on, as long as each row spans 12 columns in total.
|
|
37
37
|
|
|
@@ -41,7 +41,7 @@ Full width content layout is the most flexible and generic layout, suitable for
|
|
|
41
41
|
applications. It can be used for detail pages, dashboards, and all sort of other full width pages, with the exception of
|
|
42
42
|
full page data grids (use Data Grid Template).
|
|
43
43
|
|
|
44
|
-
#### Responsive
|
|
44
|
+
#### Responsive behavior
|
|
45
45
|
|
|
46
46
|
Full width (12 columns) layout is always full width on all devices. Uneven (3+9, 9+3, 4+8 and 8+4 columns) and even (6+6
|
|
47
47
|
columns) layouts turn into a single column layout from 767px and below.
|
|
@@ -61,7 +61,7 @@ Narrow and half width column layouts have a single column of content that takes
|
|
|
61
61
|
These layouts work great for pages that have a relatively narrow content and don't need a secondary column with
|
|
62
62
|
additional information or some form of navigation. An example would be a single step form page.
|
|
63
63
|
|
|
64
|
-
#### Responsive
|
|
64
|
+
#### Responsive behavior
|
|
65
65
|
|
|
66
66
|
As the screen size gets smaller, the single column takes more and more of the grid space, becoming a full width layout
|
|
67
67
|
at 767px and less.
|
|
@@ -89,7 +89,7 @@ component, as `layout` only supports `layout-row` to be passed inside the slot.
|
|
|
89
89
|
|
|
90
90
|
## Additional content guidelines
|
|
91
91
|
|
|
92
|
-
Uneven layouts (3+9, 9+3, 4+8 and 8+4 columns) can be used to **
|
|
92
|
+
Uneven layouts (3+9, 9+3, 4+8 and 8+4 columns) can be used to **organize information in terms of importance**. Choose a
|
|
93
93
|
layout with a wide left column and narrow right column to put the main content on the left and secondary (less important
|
|
94
94
|
or a summary of what's in the wide column) on the right. If you're using secondary side navigation or stepper, we
|
|
95
95
|
recommend to use the mirrored layout, with narrow column on the left (to hold these components) and a wide column on the
|
|
@@ -105,4 +105,4 @@ or to visually compare two sets of data.
|
|
|
105
105
|
|
|
106
106
|
## Resources
|
|
107
107
|
|
|
108
|
-
-
|
|
108
|
+
- [Figma link](https://www.figma.com/design/Diqr47KAACSr6ohrQhcNgO/Bento---Product---Fundamentals?node-id=3763-15379&m=dev)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <component :is="wrapperElement" class="b-layout" :class="conditionalClasses"> <slot /> </component> </template> <script setup lang="ts"> import { computed, provide, toRef } from 'vue'; import { type BentoLayoutProps } from './layout.types'; import { LAYOUT_SIZE_INJECTION_KEY } from '@/components/layout/layout.keys'; const props = withDefaults(defineProps<BentoLayoutProps>(), { size: 'full-width', variant: 'page', }); provide(LAYOUT_SIZE_INJECTION_KEY, toRef(props, 'size')); const conditionalClasses = computed(() => ({ 'b-layout--component': props.variant === 'component', })); const wrapperElement = props.variant === 'page' ? 'main' : 'div'; </script> <script lang="ts"> /** * Layout is a basic scaffolding of a page that defines spacing, some functionality and responsive
|
|
1
|
+
<template> <component :is="wrapperElement" class="b-layout" :class="conditionalClasses"> <slot /> </component> </template> <script setup lang="ts"> import { computed, provide, toRef } from 'vue'; import { type BentoLayoutProps } from './layout.types'; import { LAYOUT_SIZE_INJECTION_KEY } from '@/components/layout/layout.keys'; const props = withDefaults(defineProps<BentoLayoutProps>(), { size: 'full-width', variant: 'page', }); provide(LAYOUT_SIZE_INJECTION_KEY, toRef(props, 'size')); const conditionalClasses = computed(() => ({ 'b-layout--component': props.variant === 'component', })); const wrapperElement = props.variant === 'page' ? 'main' : 'div'; </script> <script lang="ts"> /** * Layout is a basic scaffolding of a page that defines spacing, some functionality and responsive behavior of its top level sections. * * @example * import { BentoLayout, BentoLayoutRow } from '@adyen/bento-vue2'; * * export default { * components: { BentoLayout, BentoLayoutRow }, * template: ` * <bento-layout size="narrow"> * <bento-layout-row> * Row 1 * </bento-layout-row> * <bento-layout-row> * Row 2 * </bento-layout-row> * </bento-layout> * ` * } */ export default {}; </script> <style lang="scss" scoped src="./layout.scss" />
|