@adyen/bento-mcp 0.5.4 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +7 -6
  3. package/bin/add-skill.js +12 -1
  4. package/dist/assets/components/anchor-scroller/anchor-scroller.vue +1 -1
  5. package/dist/assets/components/anchor-scroller/components/anchor-scroller-list/anchor-scroller-list.types.ts +1 -1
  6. package/dist/assets/components/anchor-scroller/components/anchor-scroller-list/anchor-scroller-list.vue +1 -1
  7. package/dist/assets/components/button/button.docs.mdx +24 -0
  8. package/dist/assets/components/button/button.vue +1 -1
  9. package/dist/assets/components/button/components/button-actions/button-actions.docs.mdx +14 -1
  10. package/dist/assets/components/data-grid/data-grid.stories.ts +1 -1
  11. package/dist/assets/components/date-range-picker/date-range-picker.vue +1 -1
  12. package/dist/assets/components/draggable/components/draggable-handle/draggable-handle.vue +1 -1
  13. package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue +1 -1
  14. package/dist/assets/components/dropdown/components/dropdown-options-container/dropdown-options-container.vue +1 -1
  15. package/dist/assets/components/dropdown/dropdown.docs.mdx +58 -0
  16. package/dist/assets/components/dropdown/dropdown.stories.ts +1 -1
  17. package/dist/assets/components/dropdown/dropdown.vue +1 -1
  18. package/dist/assets/components/filter-bar/components/filter-bar-button/filter-bar-button.vue +1 -1
  19. package/dist/assets/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.vue +1 -1
  20. package/dist/assets/components/filter-bar/components/range-filter/range-filter.types.ts +1 -0
  21. package/dist/assets/components/filter-bar/components/range-filter/range-filter.vue +1 -1
  22. package/dist/assets/components/filter-bar/filter-bar.docs.mdx +74 -1
  23. package/dist/assets/components/filter-bar/filter-bar.stories.ts +1 -1
  24. package/dist/assets/components/filter-bar/filter-bar.types.ts +1 -1
  25. package/dist/assets/components/filter-bar/filter-bar.vue +1 -1
  26. package/dist/assets/components/header/header.docs.mdx +8 -0
  27. package/dist/assets/components/header-with-views/header-with-views.docs.mdx +12 -0
  28. package/dist/assets/components/input-field/input-field.vue +1 -1
  29. package/dist/assets/components/internal/listbox/components/listbox-lazy-load/listbox-lazy-load.types.ts +1 -0
  30. package/dist/assets/components/internal/listbox/components/listbox-lazy-load/listbox-lazy-load.vue +1 -0
  31. package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-category/listbox-multi-select-category.vue +1 -1
  32. package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-nested-lazy-load/listbox-multi-select-nested-lazy-load.types.ts +1 -0
  33. package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-nested-lazy-load/listbox-multi-select-nested-lazy-load.vue +1 -0
  34. package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/listbox-multi-select-options.vue +1 -1
  35. package/dist/assets/components/internal/listbox/components/listbox-multi-select/listbox-multi-select.vue +1 -1
  36. package/dist/assets/components/internal/listbox/components/listbox-single-select/listbox-single-select.vue +1 -1
  37. package/dist/assets/components/internal/listbox/listbox.vue +1 -1
  38. package/dist/assets/components/table-of-contents/components/table-of-contents-list/table-of-contents-list.types.ts +1 -0
  39. package/dist/assets/components/table-of-contents/components/table-of-contents-list/table-of-contents-list.vue +1 -0
  40. package/dist/assets/components/table-of-contents/table-of-contents.docs.mdx +34 -0
  41. package/dist/assets/components/table-of-contents/table-of-contents.stories.ts +1 -0
  42. package/dist/assets/components/table-of-contents/table-of-contents.types.ts +1 -0
  43. package/dist/assets/components/table-of-contents/table-of-contents.vue +1 -0
  44. package/dist/assets/components/toast/toast.docs.mdx +7 -0
  45. package/dist/assets/components.json +1 -0
  46. package/dist/assets/composables/use-numeric-input/use-numeric-input.types.ts +1 -0
  47. package/dist/assets/composables/use-section-navigation-keyboard/use-section-navigation-keyboard.types.ts +1 -0
  48. package/dist/assets/deprecations.md +1586 -136
  49. package/dist/assets/index.ts +1 -1
  50. package/dist/assets/usage.json +80 -79
  51. package/dist/assets/utils/ts/scroll-to-section/scroll-to-section.types.ts +1 -0
  52. package/dist/main.js +1 -1
  53. package/package.json +1 -1
@@ -1 +1 @@
1
- <template> <bento-base-filter v-bind="$props" :button-label="buttonLabel" :button-secondary-label="buttonSecondaryLabel" :disable-apply-button="shouldDisableApply" :disable-secondary-button="isSecondaryButtonDisabled" :only-slot="onlySlot" :tooltip-position="options?.tooltipPosition" :tooltip-text="options?.tooltipText" @apply="updateFilter" @clear="clearFilter" @open="openFilter" @reset="resetFilter" > <div class="b-input-with-dropdown-filter" :class="conditionalClasses" @keydown.enter="updateFilter" @input="onInput" > <bento-input-field ref="inputTextRef" :dropdown="dropdownOptions" :dropdown-position="options?.dropdownPosition" variant="dropdown" :model-value="internalFilterValue?.inputValue" :disabled="disabled" @update:model-value="setInternalFilterValue" @dropdown-input="onDropdownInput" /> </div> </bento-base-filter> </template> <script setup lang="ts"> import { computed, onBeforeUpdate, provide, ref, toRef, watch } from 'vue'; import { BentoBaseFilter, useBaseFilter } from '../base-filter'; import { BentoFilterEvent, type BentoInputWithDropdownFilterOptions, type BentoInpuWithDropdowntFilterValue, } from '../../filter-bar.types'; import { type BentoInputDropdownProps, BentoInputField } from '@/components/input-field'; import { truncate } from '@/utils/ts/truncate'; import { type BentoListboxSelectedValue } from '@/types/listbox'; import { INPUT_FILTER_WITH_DROPDOWN_POPOVER_INJECTION_KEY } from '@/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.keys'; const props = withDefaults( defineProps<{ /** * Disables the filter from being interacted with. */ disabled?: boolean; /** * Default value of the filter to reset to. */ defaultValue?: BentoInpuWithDropdowntFilterValue; /** * Filter name/key referencing the actual property name in the data set. * Should be unique per filter bar. */ field: string; /** * Label text to be displayed */ label: string; /** * Renders the base filter's slot content only - leaving out the button and the popover. * Used for the allFiltersModal component. */ onlySlot?: boolean; /** * List of options that are needed to setup the filter * Available options: * - dropdown: all the dropdown properties that can be set in the inner dropdown of the input field. @see BentoDropdown for more info. * - dropdownPosition: allow the user to set the dropdown on the left or right side. Accepts the `start` or `end` values. * - tooltipPosition: The position of the tooltip attached to the filter button * - tooltipText: The content of the tooltip attached to the filter button */ options?: BentoInputWithDropdownFilterOptions; /** * Current value of the filter. * If the options.variant is equal to 'dropdown', the value should be of type should be `{ inputValue: string | null; dropdownValue?: string }`. */ value?: BentoInpuWithDropdowntFilterValue; }>(), { defaultValue: undefined, onlySlot: false, options: undefined, value: undefined, } ); const emit = defineEmits([BentoFilterEvent.INPUT, BentoFilterEvent.UPDATE]); const inputTextRef = ref(null); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled } = useBaseFilter<BentoInpuWithDropdowntFilterValue>(toRef(props, 'value'), toRef(props, 'defaultValue')); const buttonLabel = computed(() => props.label); const buttonSecondaryLabel = computed(() => truncate(props.value?.inputValue, 12)); const dropdownSelection = computed( () => internalFilterValue.value?.dropdownValue ?? props.options?.dropdown?.modelValue ?? props.options?.dropdown?.value ); const dropdownOptions = computed<BentoInputDropdownProps>(() => ({ ...((props.options?.dropdown || {}) as BentoInputDropdownProps), modelValue: dropdownSelection.value, })); const conditionalClasses = computed(() => ({ [`b-input-with-dropdown-filter--only-slot`]: props.onlySlot, })); const shouldDisableApply = computed(() => { const allFieldsAreSet = !!internalFilterValue.value?.inputValue && !!dropdownSelection.value; return !allFieldsAreSet || isApplyButtonDisabled.value; }); onBeforeUpdate(() => { // Set value to null when the string is empty if (internalFilterValue.value?.inputValue === '') { internalFilterValue.value = { ...internalFilterValue.value, inputValue: undefined }; } }); watch(inputTextRef, () => { // Focus on the input when it's opened inputTextRef.value?.focusInput(); }); const openFilter = () => { internalFilterValue.value = props.value; }; const updateFilter = () => { if (!internalFilterValue.value?.inputValue && props.defaultValue !== null) { // If value set to empty, reset back to default value internalFilterValue.value = { ...props.defaultValue }; } emit(BentoFilterEvent.UPDATE, { ...props, value: internalFilterValue.value, }); }; const resetFilter = () => emit(BentoFilterEvent.UPDATE, { ...props, value: props.defaultValue }); const clearFilter = () => { internalFilterValue.value = null; updateFilter(); }; const onInput = () => { emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const setInternalFilterValue = (event: string) => { internalFilterValue.value = { dropdownValue: dropdownSelection.value, inputValue: event }; }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { internalFilterValue.value = { ...internalFilterValue.value, dropdownValue: selectedValue }; }; // We need to change the base filter's popover to have overflow visible so the dropdown options is // not hidden inside the filter when opened. provide(INPUT_FILTER_WITH_DROPDOWN_POPOVER_INJECTION_KEY, true); </script> <script lang="ts"> /** * */ export default { name: 'bento-input-with-dropdown-filter', }; </script> <style lang="scss" scoped src="./input-with-dropdown-filter.scss" />
1
+ <template> <bento-base-filter v-bind="$props" :applied-filters-count="selectedDropdownLabel" :button-label="buttonLabel" :button-secondary-label="buttonSecondaryLabel" :disable-apply-button="shouldDisableApply" :disable-secondary-button="isSecondaryButtonDisabled" :only-slot="onlySlot" :tooltip-position="options?.tooltipPosition" :tooltip-text="options?.tooltipText" @apply="updateFilter" @clear="clearFilter" @open="openFilter" @reset="resetFilter" > <div class="b-input-with-dropdown-filter" :class="conditionalClasses" @keydown.enter="updateFilter" @input="onInput" > <bento-input-field ref="inputTextRef" :dropdown="dropdownOptions" :dropdown-position="options?.dropdownPosition" variant="dropdown" :model-value="internalFilterValue?.inputValue" :disabled="disabled" @update:model-value="setInternalFilterValue" @dropdown-input="onDropdownInput" /> </div> </bento-base-filter> </template> <script setup lang="ts"> import { computed, onBeforeUpdate, provide, ref, toRef, watch } from 'vue'; import { BentoBaseFilter, useBaseFilter } from '../base-filter'; import { BentoFilterEvent, type BentoInputWithDropdownFilterOptions, type BentoInpuWithDropdowntFilterValue, } from '../../filter-bar.types'; import { type BentoInputDropdownProps, BentoInputField } from '@/components/input-field'; import { truncate } from '@/utils/ts/truncate'; import { type BentoListboxSelectedValue } from '@/types/listbox'; import { INPUT_FILTER_WITH_DROPDOWN_POPOVER_INJECTION_KEY } from '@/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.keys'; const props = withDefaults( defineProps<{ /** * Disables the filter from being interacted with. */ disabled?: boolean; /** * Default value of the filter to reset to. */ defaultValue?: BentoInpuWithDropdowntFilterValue; /** * Filter name/key referencing the actual property name in the data set. * Should be unique per filter bar. */ field: string; /** * Label text to be displayed */ label: string; /** * Renders the base filter's slot content only - leaving out the button and the popover. * Used for the allFiltersModal component. */ onlySlot?: boolean; /** * List of options that are needed to setup the filter * Available options: * - dropdown: all the dropdown properties that can be set in the inner dropdown of the input field. @see BentoDropdown for more info. * - dropdownPosition: allow the user to set the dropdown on the left or right side. Accepts the `start` or `end` values. * - tooltipPosition: The position of the tooltip attached to the filter button * - tooltipText: The content of the tooltip attached to the filter button */ options?: BentoInputWithDropdownFilterOptions; /** * Current value of the filter. * If the options.variant is equal to 'dropdown', the value should be of type should be `{ inputValue: string | null; dropdownValue?: string }`. */ value?: BentoInpuWithDropdowntFilterValue; }>(), { defaultValue: undefined, onlySlot: false, options: undefined, value: undefined, } ); const emit = defineEmits([BentoFilterEvent.INPUT, BentoFilterEvent.UPDATE]); const inputTextRef = ref(null); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled } = useBaseFilter<BentoInpuWithDropdowntFilterValue>(toRef(props, 'value'), toRef(props, 'defaultValue')); const selectedDropdownLabel = computed(() => { const selectedValue = props.value?.dropdownValue ?? props.options?.dropdown?.modelValue ?? props.options?.dropdown?.value; if ( !buttonSecondaryLabel.value || selectedValue === undefined || selectedValue === null || !props.options?.dropdown?.items ) { return null; } const label = props.options.dropdown.items.find(item => item.value === selectedValue)?.label; return label ? truncate(label, 12) : null; }); const buttonLabel = computed(() => props.label); const buttonSecondaryLabel = computed(() => props.value?.inputValue ? truncate(props.value.inputValue, 12) : undefined ); const dropdownSelection = computed( () => internalFilterValue.value?.dropdownValue ?? props.options?.dropdown?.modelValue ?? props.options?.dropdown?.value ); const dropdownOptions = computed<BentoInputDropdownProps>(() => ({ ...((props.options?.dropdown || {}) as BentoInputDropdownProps), modelValue: dropdownSelection.value, })); const conditionalClasses = computed(() => ({ [`b-input-with-dropdown-filter--only-slot`]: props.onlySlot, })); const shouldDisableApply = computed(() => { const allFieldsAreSet = !!internalFilterValue.value?.inputValue && !!dropdownSelection.value; return !allFieldsAreSet || isApplyButtonDisabled.value; }); onBeforeUpdate(() => { // Set value to null when the string is empty if (internalFilterValue.value?.inputValue === '') { internalFilterValue.value = { ...internalFilterValue.value, inputValue: undefined }; } }); watch(inputTextRef, () => { // Focus on the input when it's opened inputTextRef.value?.focusInput(); }); const openFilter = () => { internalFilterValue.value = props.value; }; const updateFilter = () => { if (!internalFilterValue.value?.inputValue && props.defaultValue !== null) { // If value set to empty, reset back to default value internalFilterValue.value = { ...props.defaultValue }; } emit(BentoFilterEvent.UPDATE, { ...props, value: internalFilterValue.value, }); }; const resetFilter = () => emit(BentoFilterEvent.UPDATE, { ...props, value: props.defaultValue }); const clearFilter = () => { internalFilterValue.value = null; updateFilter(); }; const onInput = () => { emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const setInternalFilterValue = (event: string) => { internalFilterValue.value = { dropdownValue: dropdownSelection.value, inputValue: event }; }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { internalFilterValue.value = { ...internalFilterValue.value, dropdownValue: selectedValue }; }; // We need to change the base filter's popover to have overflow visible so the dropdown options is // not hidden inside the filter when opened. provide(INPUT_FILTER_WITH_DROPDOWN_POPOVER_INJECTION_KEY, true); </script> <script lang="ts"> /** * */ export default { name: 'bento-input-with-dropdown-filter', }; </script> <style lang="scss" scoped src="./input-with-dropdown-filter.scss" />
@@ -0,0 +1 @@
1
+ import type { BentoRangeFilterOptions } from '../../filter-bar.types'; export type BentoRangeFilterValue = Array<string> | undefined; export interface BentoRangeFilterProps { /** * Disables the filter from being interacted with. */ disabled?: boolean; /** * Default value of the filter to reset to. */ defaultValue?: Array<string> | null; /** * Filter name/key referencing the actual property name in the data set. * Should be unique per filter bar. */ field: string; /** * Label text to be displayed */ label: string; /** * Current value of the filter */ value?: Array<string>; /** * Renders the base filter's slot content only - leaving out the button and the popover. * Used for the allFiltersModal component. */ onlySlot?: boolean; /** * List of options that are needed to setup the filter */ options?: BentoRangeFilterOptions; }
@@ -1 +1 @@
1
- <template> <bento-base-filter v-bind="$props" :disable-apply-button="isApplyButtonDisabled" :disable-secondary-button="isSecondaryButtonDisabled" :button-label="buttonLabel" :button-secondary-label="buttonSecondaryLabel" :only-slot="onlySlot" :tooltip-position="options?.tooltipPosition" :tooltip-text="options?.tooltipText" @apply="updateFilter" @clear="clearFilter" @open="openFilter" @reset="resetFilter" > <div class="b-range-filter" :class="conditionalClasses" @keydown.enter="updateFilter"> <bento-input-field ref="inputTextRef" :model-value="internalFilterValue ? internalFilterValue[0] : ''" type="number" :aria-label="t('from').toString()" :disabled="disabled" @update:model-value="updateFrom" > {{ t('from') }} </bento-input-field> <bento-input-field class="b-range-filter__input-field" :model-value="internalFilterValue ? internalFilterValue[1] : ''" type="number" :aria-label="t('to')" :disabled="disabled" @update:model-value="updateTo" > {{ t('to') }} </bento-input-field> </div> </bento-base-filter> </template> <script lang="ts"> import { computed, defineComponent, nextTick, type PropType, ref, toRef } from 'vue'; import { BentoFilterEvent, type BentoRangeFilterOptions } from '../../filter-bar.types'; import { BentoBaseFilter, useBaseFilter } from '@/components/filter-bar/components/base-filter'; import { BentoInputField } from '@/components/input-field'; import { truncate } from '@/utils/ts/truncate'; import { useI18n } from '@/utils/ts/i18n'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; /** * Range filter used in the filter-bar component. * * @example * import { BentoRangeFilter } from '@adyen/bento-vue2'; * * export default { * components: { BentoRangeFilter }, * template: ` * <bento-range-filter field="filter-field" label="Filter label" value="['0', '1']"/> * ` * } */ export default defineComponent({ name: 'bento-range-filter', i18n: { messages }, components: { BentoBaseFilter, BentoInputField }, props: { /** * Disables the filter from being interacted with. */ disabled: { type: Boolean, default: false }, /** * Default value of the filter to reset to. */ defaultValue: { type: Array as PropType<Array<string>>, 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 }, /** * Current value of the filter */ value: { type: Array as PropType<Array<string>>, default: () => ['', ''] }, /** * 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: * - 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<BentoRangeFilterOptions>, default: undefined }, }, emits: [BentoFilterEvent.INPUT, BentoFilterEvent.UPDATE], setup(props, { emit }) { const { t } = useI18n<{ message: MessageSchema }>({ messages }); const inputTextRef = ref(null); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled } = useBaseFilter( toRef(props, 'value'), toRef(props, 'defaultValue') ); const truncatedRangeValues = computed(() => { if (!props.value?.[0] && !props.value?.[1]) { return null; } // Set prefix is one range value is not set let prefix = ''; if (!props.value?.[0]) { prefix = '≤'; } else if (!props.value?.[1]) { prefix = '≥'; } const rangeFromLabel = props.value?.[0] ? `${truncate(props.value[0], 5)}` : ''; const seperator = props.value?.[0] && props.value?.[1] ? ' - ' : ''; const rangeToLabel = props.value?.[1] ? `${truncate(props.value[1], 5)}` : ''; return `${prefix}${rangeFromLabel}${seperator}${rangeToLabel}`; }); const buttonLabel = computed(() => props.label); const buttonSecondaryLabel = computed(() => truncatedRangeValues.value); const conditionalClasses = computed(() => ({ [`b-range-filter--only-slot`]: props.onlySlot, })); const openFilter = () => { internalFilterValue.value = props.value; nextTick(() => inputTextRef.value.focusInput()); }; const onInput = () => { emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const updateFrom = newValue => { internalFilterValue.value = [newValue, internalFilterValue.value[1]]; onInput(); }; const updateTo = newValue => { internalFilterValue.value = [internalFilterValue.value[0], newValue]; onInput(); }; const updateFilter = () => { if (!internalFilterValue.value.some(x => x)) { // If both range values are empty, // If persistent filter, reset back to default value, else reset back to undefined internalFilterValue.value = props.defaultValue !== null ? props.defaultValue : undefined; } emit(BentoFilterEvent.UPDATE, { ...props, value: internalFilterValue.value, }); }; const resetFilter = () => emit(BentoFilterEvent.UPDATE, { ...props, value: props.defaultValue }); const clearFilter = () => { internalFilterValue.value = ['', '']; updateFilter(); }; return { // Values inputTextRef, conditionalClasses, internalFilterValue, isApplyButtonDisabled, isSecondaryButtonDisabled, buttonLabel, buttonSecondaryLabel, // Events updateFrom, updateTo, updateFilter, clearFilter, openFilter, resetFilter, // Translations t, }; }, }); </script> <style lang="scss" scoped src="./range-filter.scss" />
1
+ <template> <bento-base-filter v-bind="props" :disable-apply-button="isApplyButtonDisabled" :disable-secondary-button="isSecondaryButtonDisabled" :button-label="buttonLabel" :button-secondary-label="buttonSecondaryLabel" :only-slot="props.onlySlot" :tooltip-position="props.options?.tooltipPosition" :tooltip-text="props.options?.tooltipText" @apply="updateFilter" @clear="clearFilter" @open="openFilter" @reset="resetFilter" > <div class="b-range-filter" :class="conditionalClasses" @keydown.enter="updateFilter"> <bento-input-field ref="fromFieldRef" :model-value="displayFrom" type="text" inputmode="numeric" :aria-label="t('from').toString()" :disabled="props.disabled" @update:model-value="handleModelValueFrom" @keydown="handleKeyDownFrom" @focus="onFocusFrom" @blur="onBlurFrom" > {{ t('from') }} </bento-input-field> <bento-input-field ref="toFieldRef" class="b-range-filter__input-field" :model-value="displayTo" type="text" inputmode="numeric" :aria-label="t('to')" :disabled="props.disabled" @update:model-value="handleModelValueTo" @keydown="handleKeyDownTo" @focus="onFocusTo" @blur="onBlurTo" > {{ t('to') }} </bento-input-field> </div> </bento-base-filter> </template> <script lang="ts"> /** * Range filter used in the filter-bar component. * * @example * import { BentoRangeFilter } from '@adyen/bento-vue2'; * * export default { * components: { BentoRangeFilter }, * template: ` * <bento-range-filter field="filter-field" label="Filter label" value="['0', '1']"/> * ` * } */ export default { name: 'bento-range-filter', i18n: { messages }, }; </script> <script setup lang="ts"> import { computed, nextTick, onMounted, ref, toRef, watch } from 'vue'; import { BentoFilterEvent } from '../../filter-bar.types'; import type { BentoRangeFilterProps, BentoRangeFilterValue } from './range-filter.types'; import { BentoBaseFilter, useBaseFilter } from '@/components/filter-bar/components/base-filter'; import { BentoInputField, type BentoInputFieldValue } from '@/components/input-field'; import { truncate } from '@/utils/ts/truncate'; import { useI18n } from '@/utils/ts/i18n'; import { useNumericInput } from '@/composables/use-numeric-input/use-numeric-input'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; interface RangeFilterInputField { inputFieldElement: HTMLInputElement | null; focusInput: () => void; } const props = withDefaults(defineProps<BentoRangeFilterProps>(), { disabled: false, defaultValue: null, value: () => ['', ''], onlySlot: false, options: undefined, }); const emit = defineEmits([BentoFilterEvent.INPUT, BentoFilterEvent.UPDATE]); const { t, n } = useI18n<{ message: MessageSchema }>({ messages }); const fromFieldRef = ref<RangeFilterInputField | null>(null); const toFieldRef = ref<RangeFilterInputField | null>(null); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled } = useBaseFilter<BentoRangeFilterValue>(toRef(props, 'value'), toRef(props, 'defaultValue')); const fromNum = ref<number | null>(null); const toNum = ref<number | null>(null); const stringifyInputValue = (val: number | null) => val !== null ? (Object.is(val, -0) ? '-0' : val.toString()) : ''; const { displayValue: displayFrom, handleInput: handleInputFrom, handleKeyDown: handleKeyDownFrom, onFocus: onFocusFrom, onBlur: onBlurFrom, inputRef: inputRefFrom, } = useNumericInput({ value: fromNum, prettyOutput: true, onInput: val => { const currentValue = internalFilterValue.value ?? ['', '']; internalFilterValue.value = [stringifyInputValue(val), currentValue[1]]; onInput(); }, }); const handleModelValueFrom = (val: BentoInputFieldValue) => handleInputFrom(String(val)); const { displayValue: displayTo, handleInput: handleInputTo, handleKeyDown: handleKeyDownTo, onFocus: onFocusTo, onBlur: onBlurTo, inputRef: inputRefTo, } = useNumericInput({ value: toNum, prettyOutput: true, onInput: val => { const currentValue = internalFilterValue.value ?? ['', '']; internalFilterValue.value = [currentValue[0], stringifyInputValue(val)]; onInput(); }, }); const handleModelValueTo = (val: BentoInputFieldValue) => handleInputTo(String(val)); onMounted(() => { inputRefFrom.value = fromFieldRef.value?.inputFieldElement ?? null; inputRefTo.value = toFieldRef.value?.inputFieldElement ?? null; }); watch( () => internalFilterValue.value, newVal => { const nextFrom = newVal?.[0] ? parseFloat(newVal[0]) : null; const nextTo = newVal?.[1] ? parseFloat(newVal[1]) : null; if (fromNum.value !== nextFrom) { fromNum.value = nextFrom; } if (toNum.value !== nextTo) { toNum.value = nextTo; } }, { immediate: true } ); const truncatedRangeValues = computed(() => { if (!props.value?.[0] && !props.value?.[1]) { return null; } // Set prefix is one range value is not set let prefix = ''; if (!props.value?.[0]) { prefix = '≤'; } else if (!props.value?.[1]) { prefix = '≥'; } const fromVal = props.value?.[0] ? parseFloat(props.value[0]) : null; const toVal = props.value?.[1] ? parseFloat(props.value[1]) : null; const rangeFromLabel = fromVal !== null ? n(fromVal, { style: 'decimal' }) : ''; const seperator = props.value?.[0] && props.value?.[1] ? ' - ' : ''; const rangeToLabel = toVal !== null ? n(toVal, { style: 'decimal' }) : ''; return `${prefix}${truncate(rangeFromLabel, 10)}${seperator}${truncate(rangeToLabel, 10)}`; }); const buttonLabel = computed(() => props.label); const buttonSecondaryLabel = computed(() => truncatedRangeValues.value); const conditionalClasses = computed(() => ({ [`b-range-filter--only-slot`]: props.onlySlot, })); const openFilter = () => { internalFilterValue.value = props.value; nextTick(() => fromFieldRef.value?.focusInput()); }; const onInput = () => { emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const updateFilter = () => { if (!internalFilterValue.value?.some(x => x)) { // If both range values are empty, // If persistent filter, reset back to default value, else reset back to undefined internalFilterValue.value = props.defaultValue !== null ? props.defaultValue : undefined; } emit(BentoFilterEvent.UPDATE, { ...props, value: internalFilterValue.value, }); }; const resetFilter = () => emit(BentoFilterEvent.UPDATE, { ...props, value: props.defaultValue }); const clearFilter = () => { internalFilterValue.value = ['', '']; updateFilter(); }; </script> <style lang="scss" scoped src="./range-filter.scss" />
@@ -51,6 +51,7 @@ Filter bar allows users to filter the content so that only the information they'
51
51
  - [External filtering](#external-filtering)
52
52
  - [External filtering and Lazy Loading simultaneously](#external-filtering-and-lazy-loading-simultaneously)
53
53
  - [Nested multi-select filter](#nested-multi-select-filter)
54
+ - [Category Selection & Select All Behavior](#category-selection--select-all-behavior)
54
55
  - [Date filter](#date-filter)
55
56
  - [Options](#options-3)
56
57
  - [Configuration Interface](#configuration-interface-5)
@@ -585,7 +586,79 @@ The Multi select can also be set up as a nested multi-select with the following
585
586
  ],
586
587
  ```
587
588
 
588
- For more details see the `bento-dropdown` documentation.
589
+ ##### Nested multi-select with lazy loading
590
+
591
+ The nested multi-select variant supports lazy loading individual child item lists (categories). This is highly useful
592
+ when a category contains a large number of nested options, avoiding the need to load all elements upfront.
593
+
594
+ To enable this feature, configure the `lazyLoad` property on any parent option item that contains a nested list of
595
+ `items`:
596
+
597
+ - **`lazyLoad`**: An object containing the following properties:
598
+ - **`hasMoreItems`**: A boolean indicating whether more child options are available to load. Once all options for
599
+ the group are loaded, set this to `false` to hide the "Show more" button.
600
+ - **`lazyLoadEvent`**: A function triggered when the group's "Show more" button is clicked (or space/enter is
601
+ pressed). It must return a `Promise<void>`. The function receives an object with:
602
+ - `value`: The value of the category option item that triggered the lazy load event.
603
+ - `index`: The index of the category option item.
604
+
605
+ When the `lazyLoadEvent` promise is pending, a loading indicator is displayed. If the promise is rejected, an error
606
+ message is shown with a retry option, allowing the user to click the "Show more" button again.
607
+
608
+ ```tyepscript
609
+ [
610
+ {
611
+ label: 'Belgium',
612
+ description: 'Country',
613
+ value: 'belgium',
614
+ },
615
+ {
616
+ label: 'Netherlands',
617
+ description: 'Country',
618
+ value: 'netherlands',
619
+ lazyLoad: {
620
+ hasMoreItems: true,
621
+ lazyLoadEvent: ({index, value}) => void
622
+ },
623
+ items: [
624
+ {
625
+ label: 'Amsterdam',
626
+ description: 'City',
627
+ value: 'amsterdam',
628
+ },
629
+ {
630
+ label: 'Den Haag',
631
+ description: 'City',
632
+ value: 'den-haag',
633
+ },
634
+ ],
635
+ },
636
+ ],
637
+ ```
638
+
639
+ ##### Category Selection & Select All Behavior
640
+
641
+ When using nested multi-select with lazy loading, special rules apply to selecting parent categories and how the "select
642
+ all" mechanism operates:
643
+
644
+ - **Required Configuration**: The dropdown **must** have `enableValueLabelPair` set to `true` (or
645
+ `enable-value-label-pair`).
646
+ - **Category Selection**: Selection of a parent category is done using the category option's `"value"` (and `"label"`)
647
+ from the parent `BentoListboxOptionItem`. Selecting a parent category will select all of its child items.
648
+
649
+ Example of selected categories with Lazy load:
650
+
651
+ ```tyepscript
652
+ {
653
+ field: 'multi-select'
654
+ value: [
655
+ {
656
+ label: 'Netherlands',
657
+ value: 'netherlands',
658
+ },
659
+ ]
660
+ },
661
+ ```
589
662
 
590
663
  ### Date filter
591
664
 
@@ -1 +1 @@
1
- import { computed, onMounted, ref } from 'vue'; import { action } from '@storybook/addon-actions'; import { type BentoDateRangeFilterOptions, type BentoFilterBarContainerSizeLayout, type BentoFilterBarModel, type BentoFilterBarSearchBarProps, type BentoFilterBarValue, BentoFilterItemType, type BentoFilterValueObject, type BentoFilterValues, } from './filter-bar.types'; import { getPokemonFromApi, storybookDocsParameter, withToastDecorator } from '@/utils/ts/storybook'; import { type BentoListboxOptions } from '@/types/listbox'; import { BentoCountryCode } from '@/components/country/country.types'; import { BentoCurrencyISOCode } from '@/components/currency/currency.types'; import { isVue2 } from 'vue-demi'; import { type Meta, type StoryObj } from '@storybook/vue'; import { BentoToggle } from '@/components/toggle'; import BentoFilterBar from './filter-bar.vue'; import { endOfWeek } from 'date-fns/endOfWeek'; import { startOfWeek } from 'date-fns/startOfWeek'; import { subDays } from 'date-fns/subDays'; import { subWeeks } from 'date-fns/subWeeks'; import { subMonths } from 'date-fns/subMonths'; import { startOfMonth } from 'date-fns/startOfMonth'; import { endOfDay, startOfDay, subHours, subMinutes } from 'date-fns'; // Examples import FilterBarCustomFilterSubComponentSingleValueExample from './__tests__/filter-bar-custom-filter-sub-component-single-value-example.vue'; import FilterBarCustomFilterSubComponentMultiValueExample from './__tests__/filter-bar-custom-filter-sub-component-multiple-value-example.vue'; import FilterBarCustomFilterSubComponentWithPopoverExample from './__tests__/filter-bar-custom-filter-sub-component-with-popover-example.vue'; import FilterBarCustomFiltersExample from './__tests__/filter-bar-custom-filters-example.vue?raw'; // Generate array from 1 to # // { label: 'Option #', value: # }, const DEFAULT_OPTIONS: BentoListboxOptions = new Array(10) .fill(0) .map((_, index) => ({ label: `Option ${index + 1}`, value: index + 1 })); // To be used by as the current time across all date filters const CURRENT_DATE_AND_TIME = new Date(); const meta: Meta = { title: 'Filter bar', component: BentoFilterBar, decorators: [withToastDecorator], argTypes: { // Props searchTerm: { name: 'searchTerm', defaultValue: undefined, description: "Search term useful to filter all the data by text. If the value is non-existant (`null` or `undefined`) the search bar won't be enabled.", control: { type: 'text', }, }, // Events input: { name: 'input', defaultValue: '', description: 'Triggered whenever the filters are updated.', table: { type: { summary: '([{ field, label, value, type }]) => void' } }, control: { disable: true }, }, // Search Events 'search-input': { name: 'search-input', defaultValue: '', description: "Triggered whenever the filter's search is updated.", table: { type: { summary: '(newSearchTerm) => void' } }, control: { disable: true }, }, 'search-clear': { name: 'search-clear', defaultValue: '', description: "Triggered whenever the filter's search is cleared.", table: { type: { summary: '() => void' } }, control: { disable: true }, }, 'update:search-term': { name: 'update:search-term', defaultValue: '', description: "Triggered whenever the filter's search is updated to sync the model.", table: { type: { summary: '(newSearchTerm) => void' } }, control: { disable: true }, }, }, }; export default meta; type Story = StoryObj<typeof BentoFilterBar>; const FILTER_VALUES: BentoFilterValues = [ { field: 'input', value: undefined, }, { field: 'boolean', value: 'yes', }, { field: 'input-with-dropdown', value: undefined, }, { field: 'hidden-input', value: 'Active value of a hidden filter', }, { field: 'merchant', value: 2, }, { field: 'company', value: [1, 3, 5], }, { field: 'nested-select', value: ['amsterdam', 'den-haag'], }, { field: 'currency', value: [], }, { field: 'range', value: ['', ''], }, { field: 'date', value: CURRENT_DATE_AND_TIME, }, { field: 'date-range', value: { startDate: subDays(startOfDay(CURRENT_DATE_AND_TIME), 2), endDate: endOfDay(CURRENT_DATE_AND_TIME) }, }, { field: 'granular-date-range', value: { startDate: subDays(CURRENT_DATE_AND_TIME, 2), endDate: CURRENT_DATE_AND_TIME }, }, { field: 'date-range-month', value: { startDate: startOfMonth(subMonths(CURRENT_DATE_AND_TIME, 2)), endDate: startOfMonth(CURRENT_DATE_AND_TIME), }, }, { field: 'paymentMethod', value: undefined, }, { field: 'checkbox-group', value: [], }, { field: 'toggle', value: true, }, ]; const FILTERS: BentoFilterBarModel = [ { field: 'input', label: 'Input filter', type: BentoFilterItemType.INPUT, options: { placeholder: 'Input filter placeholder', tooltipPosition: 'bottom', tooltipText: 'Input filter tooltip text', }, visible: true, }, { field: 'input-with-dropdown', label: 'Input with dropdown filter', type: BentoFilterItemType.INPUT_DROPDOWN, visible: true, options: { dropdown: { items: [ { label: 'EUR', value: 'EUR' }, { label: 'USD', value: 'USD' }, ], modelValue: 'EUR', dynamicFiltering: true, }, }, }, { field: 'hidden-input', label: 'Hidden input filter', description: 'This is a hidden input filter only visible through the modal', type: BentoFilterItemType.INPUT, }, { field: 'country', label: 'Country/Region', options: { listboxItems: [ { label: 'Option 1', value: BentoCountryCode.NL, }, { label: 'Option 2', value: BentoCountryCode.BE, }, ], multiple: true, variant: 'capital', }, type: BentoFilterItemType.SELECT_COUNTRY, }, { field: 'avatar', label: 'Avatar', options: { listboxItems: [ { label: 'Avatar 1', value: 'avatar-1', }, { label: 'Avatar 2', value: 'avatar-2', }, ], }, type: BentoFilterItemType.SELECT_AVATAR, }, { field: 'merchant', label: 'Merchant', options: { listboxItems: DEFAULT_OPTIONS, enableValueLabelPair: true, isOptionDisabled: option => option.value === 3, }, type: BentoFilterItemType.SELECT, visible: true, }, { field: 'company', label: 'Company', options: { listboxItems: DEFAULT_OPTIONS, enableValueLabelPair: true, isOptionDisabled: option => option.value === 5, multiple: true, }, type: BentoFilterItemType.SELECT, visible: true, }, { field: 'nested-select', label: 'Nested select', options: { listboxItems: [ { label: 'Belgium', description: 'Country/Region', value: 'belgium', }, { label: 'Netherlands', description: 'Country/Region', value: 'netherlands', items: [ { label: 'Amsterdam', description: 'City', value: 'amsterdam', }, { label: 'Den Haag', description: 'City', value: 'den-haag', }, ], }, ], multiple: true, }, type: BentoFilterItemType.SELECT, visible: true, }, { field: 'currency', label: 'Currency select', options: { listboxItems: [ { label: 'Euro', value: BentoCurrencyISOCode.EUR, }, { label: 'US Dollars', value: BentoCurrencyISOCode.USD, }, ], multiple: true, }, type: BentoFilterItemType.SELECT_CURRENCY, }, { field: 'range', label: 'Range', type: BentoFilterItemType.RANGE, visible: true, }, { field: 'date', label: 'Date', type: BentoFilterItemType.DATE, visible: true, }, { field: 'date-range', label: 'Date range', options: { allowTimeInput: true, numberOfMonths: 1, quickSelectRanges: [ { label: 'Last 15 min', value: 'lastFifteenMin', data: { startDate: subMinutes(CURRENT_DATE_AND_TIME, 15), isRelativeToNow: true, }, }, { label: 'Last 24 hours', value: 'lastTwentyFourHours', data: { startDate: subHours(CURRENT_DATE_AND_TIME, 24), isRelativeToNow: true, }, }, { label: 'Last three days', value: 'lastThreeDays', data: { startDate: subDays(startOfDay(CURRENT_DATE_AND_TIME), 2), }, }, { label: 'Last week', value: 'lastWeek', data: { startDate: subWeeks(startOfDay(CURRENT_DATE_AND_TIME), 1), }, }, ], } as BentoDateRangeFilterOptions, type: BentoFilterItemType.DATE_RANGE, visible: true, }, { field: 'granular-date-range', label: 'Granular date range', options: { allowTimeInput: true, numberOfMonths: 2, granularities: [{ type: 'daily' }, { type: 'weekly' }, { type: 'monthly', disabled: true }], min: subWeeks(startOfWeek(CURRENT_DATE_AND_TIME, { weekStartsOn: 1 }), 12), max: endOfWeek(CURRENT_DATE_AND_TIME, { weekStartsOn: 1 }), } as BentoDateRangeFilterOptions, type: BentoFilterItemType.DATE_RANGE, visible: true, }, { field: 'date-range-month', label: 'Date range (month)', options: { numberOfMonths: 2, variant: 'month', } as BentoDateRangeFilterOptions, type: BentoFilterItemType.DATE_RANGE, visible: true, }, { field: 'paymentMethod', label: 'Payment method', options: { listboxItems: [ { label: 'iDEAL', value: 'ideal' }, { label: 'Visa', value: 'visa' }, ], multiple: true, }, type: BentoFilterItemType.SELECT_PAYMENT_METHOD, }, { field: 'tag', label: 'Tag', options: { listboxItems: [ { label: 'Offline', value: 'unique-id-1', variant: 'red' }, { label: 'Online', value: 'unique-id-2', variant: 'green' }, { label: 'Standby', value: 'unique-id-3', variant: 'orange' }, { label: 'Busy', value: 'unique-id-4', variant: 'blue' }, ], multiple: true, }, type: BentoFilterItemType.SELECT_TAG, }, { field: 'radio-group', label: 'Radio group', options: { items: [ { label: 'Option 1', value: 'option-1' }, { label: 'Option 2', value: 'option-2' }, { label: 'Option 3', value: 'option-3' }, ], }, visible: false, type: BentoFilterItemType.RADIO_GROUP, }, { field: 'checkbox-group', label: 'Checkbox group', options: { checkboxItems: [ { label: 'Item 1', value: 'unique-id-1' }, { label: 'Item 2', value: 'unique-id-2' }, { label: 'Item 3', value: 'unique-id-3' }, { label: 'Item with a very long name', value: 'unique-id-4' }, ], }, type: BentoFilterItemType.CHECKBOX_GROUP, visible: true, }, { field: 'toggle', label: 'Toggle', type: BentoFilterItemType.TOGGLE, visible: true, description: 'Optional description for the toggle filter', options: { tooltipPosition: 'bottom', tooltipText: 'Toggle filter tooltip text', }, }, ]; const FILTERS_WITH_DEFAULTS: BentoFilterBarModel = [ { field: 'input', label: 'Input filter', defaultValue: 'hello', type: BentoFilterItemType.INPUT, }, { field: 'input-with-dropdown', label: 'Input with dropdown filter', type: BentoFilterItemType.INPUT_DROPDOWN, visible: true, defaultValue: { inputValue: 'Peristent input filter', dropdownValue: 'EUR', }, options: { dropdown: { items: [ { label: 'EUR', value: 'EUR' }, { label: 'USD', value: 'USD' }, ], }, }, }, { field: 'range', label: 'Range', defaultValue: ['1', '4'], type: BentoFilterItemType.RANGE, }, { field: 'merchant', label: 'Merchants', defaultValue: ['merchant_1'], options: { listboxItems: [ { label: 'Merchant 1', value: 'merchant_1', }, { label: 'Merchant 2', value: 'merchant_2', }, { label: 'Merchant 3', value: 'merchant_3', }, ], multiple: true, enableValueLabelPair: true, }, type: BentoFilterItemType.SELECT, }, { field: 'thing', label: 'Things', defaultValue: 'merchant_1', options: { listboxItems: [ { label: 'Thing 1', value: 'merchant_1', }, { label: 'Thing 2', value: 'merchant_2', }, { label: 'Thing 3', value: 'merchant_3', }, ], }, type: BentoFilterItemType.SELECT, }, { field: 'country', label: 'Country/Region', options: { listboxItems: [ { label: 'Option 1', value: BentoCountryCode.NL, }, { label: 'Option 2', value: BentoCountryCode.BE, }, ], multiple: true, variant: 'capital', }, type: BentoFilterItemType.SELECT_COUNTRY, }, { field: 'radio-group', label: 'Radio group', defaultValue: 'option-1', options: { items: [ { label: 'Option 1', value: 'option-1' }, { label: 'Option 2', value: 'option-2' }, { label: 'Option 3', value: 'option-3' }, ], }, visible: false, type: BentoFilterItemType.RADIO_GROUP, }, { field: 'checkbox-group', label: 'Checkbox group', defaultValue: ['unique-id-1'], options: { checkboxItems: [ { label: 'Item 1', value: 'unique-id-1' }, { label: 'Item 2', value: 'unique-id-2' }, { label: 'Item 3', value: 'unique-id-3' }, { label: 'Item 4', value: 'unique-id-4' }, ], }, type: BentoFilterItemType.CHECKBOX_GROUP, }, { field: 'date-range', label: 'Date range', defaultValue: { startDate: subDays(startOfDay(CURRENT_DATE_AND_TIME), 2), endDate: endOfDay(CURRENT_DATE_AND_TIME), }, options: { numberOfMonths: 1, quickSelectRanges: [ { label: 'Last three days', value: 'lastThreeDays', data: { startDate: subDays(startOfDay(CURRENT_DATE_AND_TIME), 2), }, }, { label: 'Last week', value: 'lastWeek', data: { startDate: subWeeks(startOfDay(CURRENT_DATE_AND_TIME), 1), }, }, ], } as BentoDateRangeFilterOptions, type: BentoFilterItemType.DATE_RANGE, }, ]; const defaultFilterTemplate = storybookDocsParameter(` <template> <bento-filter-bar :config="config" :filter-values="filterValues" :search-term.sync="searchTerm" @input="onFilter" @search-input="onSearchInput" @search-clear="onSearchClear" /> </template> <script setup lang="ts"> import { BentoFilterBar, BentoFilterItemType } from '@adyen/bento-vue2' import type { BentoFilterBarModel, BentoFilterValues } from '@adyen/bento-vue2' const searchTerm = ref<string>(undefined) const config = [{ field: 'name', label: 'Name', type: BentoFilterItemType.INPUT, visible: true, }, { field: 'merchant', label: 'Merchant', options: { listboxItems: DEFAULT_OPTIONS, }, type: BentoFilterItemType.SELECT, }, { field: 'company', label: 'Company', options: { listboxItems: DEFAULT_OPTIONS, isOptionDisabled: (option) => option.value === 5, multiple: true, }, type: BentoFilterItemType.SELECT, }] const filterValues = ref<BentoFilterValues>([{ field: 'merchant', value: 2, }, { field: 'company', value: [1, 3, 5], }]) const onFilter = (newFilters: BentoFilterBarModel) => { } const onSearchInput = (newTerm: string) => { } const onSearchClear = () => { } </script> `); export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoFilterBar }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const filtersConfig = ref<BentoFilterBarModel>(args.config); const filterValues = ref<BentoFilterValues>(args.filterValues); const filterValuesUpdate = (newFilterValues: BentoFilterValues) => { filterValues.value = newFilterValues; action('update:filter-values')(newFilterValues); }; const filterValuesChanged = (newFilterValues: BentoFilterValues) => { action('input')(newFilterValues); }; const searchTermField = computed(() => args.searchTerm); const searchInput = (newSearchTerm: string) => action('search-input')(newSearchTerm); const searchClear = () => action('search-clear')(); const searchTermSync = (newSearchTerm: string) => action('update:search-term')(newSearchTerm); return { // Props args, // Values filtersConfig, filterValues, searchTermField, // Events filterValuesChanged, filterValuesUpdate, searchInput, searchClear, searchTermSync, }; }, template: ` <div style="padding-top: 24px"> <bento-filter-bar v-bind="args" :config="filtersConfig" :search="{width: '800px'}" :searchTerm="searchTermField" :filter-values="filterValues" @input="filterValuesChanged" @update:filter-values="filterValuesUpdate" @update:search-term="searchTermSync" @search-input="searchInput" @search-clear="searchClear" /> </div> `, }), args: { config: FILTERS, filterValues: FILTER_VALUES, containerSizeLayout: { medium: 'multi-line', small: 'one-line', } satisfies BentoFilterBarContainerSizeLayout, search: { inputFieldAriaLabel: 'Input field aria label', debounceTime: 500, hint: 'Hint', placeholder: 'Placeholder content', } satisfies BentoFilterBarSearchBarProps, searchTerm: 'something', }, parameters: defaultFilterTemplate, }; const persistentFilterTemplate = storybookDocsParameter(` <bento-filter-bar :config="[ { field: 'input', label: 'Input filter', defaultValue: 'hello', type: BentoFilterItemType.INPUT, }, { field: 'range', label: 'Range', defaultValue: ['1', '4'], type: BentoFilterItemType.RANGE, }, { field: 'merchant', label: 'Merchants', defaultValue: ['merchant_1'], options: { listboxItems: [ { label: 'Merchant 1', value: 'merchant_1', }, { label: 'Merchant 2', value: 'merchant_2', }, { label: 'Merchant 3', value: 'merchant_3', }, ], multiple: true, }, type: BentoFilterItemType.SELECT, }, { field: 'thing', label: 'Things', defaultValue: 'merchant_1', options: { listboxItems: [ { label: 'Thing 1', value: 'merchant_1', }, { label: 'Thing 2', value: 'merchant_2', }, { label: 'Thing 3', value: 'merchant_3', }, ], }, type: BentoFilterItemType.SELECT, }, { field: 'country', label: 'Country/Region', options: { listboxItems: [ { label: 'Option 1', value: BentoCountryCode.NL, }, { label: 'Option 2', value: BentoCountryCode.BE, }, ], multiple: true, variant: 'capital', }, type: BentoFilterItemType.SELECT_COUNTRY, }, ]" :filter-values="[ { field: 'input', value: 'non-default value', }, ]" :search="{ inputFieldAriaLabel: 'Input field aria label', debounceTime: 500, hint: 'Hint', placeholder: 'Placeholder content', }" :search-term.sync="searchTerm" @input="onFilter" @search-input="onSearchInput" @search-clear="onSearchClear" /> `); export const Persistent: Story = { ...Default, args: { config: FILTERS_WITH_DEFAULTS, filterValues: [ { field: 'input', value: 'non-default value', }, { field: 'checkbox-group', value: ['unique-id-2'], }, ], searchTerm: null, }, parameters: persistentFilterTemplate, }; const externalFilteringTemplate = storybookDocsParameter(` <template> <bento-filter-bar :config="filtersConfig" :filter-values.sync="filterValues" /> </template> <script setup lang="ts"> import { ref, computed, onMounted } from 'vue'; import { BentoFilterBar, BentoFilterItemType, BentoFilterBarModel, BentoFilterValues, BentoSelectFilterOptions, } from '@adyen/bento-vue2'; // --- Mock API for demonstration --- const getPokemonFromApi = async (searchTerm?: string) => { // In a real application, this would be an API call. // We're mocking it with a timeout and static data. console.log(\`Searching for: \${searchTerm}\`); await new Promise(resolve => setTimeout(resolve, 1000)); const allPokemon = [ { label: 'Bulbasaur', value: 'bulbasaur' }, { label: 'Ivysaur', value: 'ivysaur' }, { label: 'Venusaur', value: 'venusaur' }, { label: 'Charmander', value: 'charmander' }, { label: 'Charmeleon', value: 'charmeleon' }, { label: 'Charizard', value: 'charizard' }, ]; const items = searchTerm ? allPokemon.filter(p => p.label.toLowerCase().includes(searchTerm.toLowerCase())) : allPokemon; return { items }; }; // --- End Mock API --- const filterValues = ref<BentoFilterValues>([{ field: 'pokemon', value: 'bulbasaur' }]); const apiItems = ref([]); const isLoading = ref(false); const getAndSetPokemonFromApi = async (val?: string) => { isLoading.value = true; const { items } = await getPokemonFromApi(val); apiItems.value = items; isLoading.value = false; }; onMounted(() => { getAndSetPokemonFromApi(); }); const filtersConfig = computed<BentoFilterBarModel>(() => [ { field: 'name', label: 'Name', type: BentoFilterItemType.INPUT }, { field: 'pokemon', label: 'Pokemon', options: { isOptionDisabled: option => ['ivysaur', 'charmeleon'].includes(option.value as string), listboxItems: apiItems.value, search: { debounceTime: 500, searchEvent: getAndSetPokemonFromApi }, loading: isLoading.value, }, type: BentoFilterItemType.SELECT, }, ]); </script> `); export const ExternalFilteringSelectFilter: Story = { render: (_args, { argTypes }) => ({ components: { BentoFilterBar, }, props: Object.keys(argTypes), setup(props) { const apiItems = ref([]); const filterValues = ref<BentoFilterValues>([{ field: 'pokemon', value: 'bulbasaur' }]); const isLoading = ref(false); const getAndSetPokemonFromApi = async (val?: string) => { isLoading.value = true; await new Promise(resolve => setTimeout(resolve, 1000)); const { items } = await getPokemonFromApi(val); apiItems.value = items; isLoading.value = false; }; const search = { debounceTime: 500, searchEvent: getAndSetPokemonFromApi, }; onMounted(() => { getAndSetPokemonFromApi(); }); const filtersConfig = computed(() => [ { field: 'name', label: 'Name', type: BentoFilterItemType.INPUT, }, { field: 'pokemon', label: 'Pokemon', options: { isOptionDisabled: option => ['ivysaur', 'charmeleon'].includes(option.value as string), listboxItems: apiItems.value, search, loading: isLoading.value, }, type: BentoFilterItemType.SELECT, visible: false, }, ]); return { args: isVue2 ? props : _args, // Values filtersConfig, filterValues, }; }, template: ` <div style="padding-top: 24px"> <bento-filter-bar v-bind="args" :config="filtersConfig" :filter-values.sync="filterValues" /> </div> `, }), args: { config: FILTERS, searchTerm: null }, parameters: externalFilteringTemplate, }; const SearchAndLazyLoadTemplate = storybookDocsParameter(` <template> <bento-filter-bar :config="filtersConfig" :filter-values.sync="filterValues" /> </template> <script setup lang="ts"> import { ref, computed, onMounted } from 'vue'; import { BentoFilterBar, BentoFilterItemType, BentoFilterBarModel, BentoFilterValues, BentoSelectFilterOptions, BentoListboxOptions, } from '@adyen/bento-vue2'; // --- Mock API for demonstration --- const ALL_ITEMS = Array.from({ length: 100 }, (_, i) => ({ label: \`Item \${i + 1}\`, value: i + 1, })); const fetchItems = (searchTerm: string, page: number, pageSize: number = 20) => { console.log(\`Fetching items with searchTerm: "\${searchTerm}", page: \${page}\`); return new Promise<{ items: BentoListboxOptions; hasMore: boolean }>(resolve => { setTimeout(() => { const filtered = ALL_ITEMS.filter(item => item.label.toLowerCase().includes(searchTerm.toLowerCase()) ); const start = (page - 1) * pageSize; const end = start + pageSize; const pageItems = filtered.slice(start, end); resolve({ items: pageItems, hasMore: end < filtered.length, }); }, 500); }); }; // --- End Mock API --- const filterValues = ref<BentoFilterValues>([]); // State for the select filter const listboxItems = ref<BentoListboxOptions>([]); const isLoading = ref(false); const hasMoreItems = ref(true); const currentPage = ref(1); const currentSearchTerm = ref(''); /** * Handles the search event from the filter. * This should reset pagination and fetch a new list of items. */ const onSearch = async (searchTerm: string) => { currentSearchTerm.value = searchTerm; currentPage.value = 1; isLoading.value = true; const { items, hasMore } = await fetchItems(searchTerm, currentPage.value); listboxItems.value = items; hasMoreItems.value = hasMore; isLoading.value = false; }; /** * Handles the lazy load event. * This should fetch the next page of items and append them. */ const onLazyLoad = async () => { if (isLoading.value || !hasMoreItems.value) { return; } currentPage.value++; isLoading.value = true; const { items, hasMore } = await fetchItems(currentSearchTerm.value, currentPage.value); // Append new items to the existing list listboxItems.value = [...listboxItems.value, ...items]; hasMoreItems.value = hasMore; isLoading.value = false; }; // Load initial data when the component mounts onMounted(() => { onSearch(''); }); const filtersConfig = computed<BentoFilterBarModel>(() => [ { field: 'lazy-select', label: 'Lazy Load Select', type: BentoFilterItemType.SELECT, options: { listboxItems: listboxItems.value, multiple: true, // External filtering configuration search: { searchEvent: onSearch, }, // Lazy loading configuration lazy: { hasMoreItems: hasMoreItems.value, lazyLoadType: 'automatic', // or 'button' loading: isLoading.value, showMoreEvent: onLazyLoad, }, }, }, ]); </script> `); export const ExternalFilteringAndLazyLoadingSelectFilter: Story = { ...ExternalFilteringSelectFilter, render: (_args, { argTypes }) => ({ components: { BentoFilterBar, }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const filterValues = ref<BentoFilterValues>([]); const lazyLoadingPageSize = 20; // State for the select filter const listboxItems = ref<BentoListboxOptions>([]); const isSearchLoading = ref(false); const isLazyLoading = ref(false); const hasMoreItems = ref(true); const currentPage = ref(1); const currentSearchTerm = ref(''); /** * Handles the search event from the filter. * This should reset pagination and fetch a new list of items. */ const onSearch = async (searchTerm: string) => { currentSearchTerm.value = searchTerm; currentPage.value = 1; isSearchLoading.value = true; const { items, hasMore } = await getPokemonFromApi(searchTerm, currentPage.value, lazyLoadingPageSize); listboxItems.value = items; hasMoreItems.value = hasMore; isSearchLoading.value = false; }; /** * Handles the lazy load event. * This should fetch the next page of items and append them. */ const onLazyLoad = async () => { if (isLazyLoading.value || !hasMoreItems.value) { return; } currentPage.value++; isLazyLoading.value = true; const { items, hasMore } = await getPokemonFromApi( currentSearchTerm.value, currentPage.value, lazyLoadingPageSize ); // Append new items to the existing list listboxItems.value = [...listboxItems.value, ...items]; hasMoreItems.value = hasMore; isLazyLoading.value = false; }; // Load initial data when the component mounts onMounted(() => { onSearch(''); }); const filtersConfig = computed<BentoFilterBarModel>(() => [ { field: 'lazy-select', label: 'External filtering and lazy loading', type: BentoFilterItemType.SELECT, options: { listboxItems: listboxItems.value, multiple: true, loading: isSearchLoading.value, // External filtering configuration search: { searchEvent: onSearch, }, // Lazy loading configuration lazy: { hasMoreItems: hasMoreItems.value, lazyLoadType: 'automatic', // or 'button' loading: isLazyLoading.value, showMoreEvent: onLazyLoad, }, }, }, ]); return { args, filtersConfig, filterValues, }; }, template: ` <div style="padding-top: 24px"> <bento-filter-bar v-bind="args" :config="filtersConfig" :filter-values.sync="filterValues" /> </div> `, }), args: { ...ExternalFilteringSelectFilter.args, }, parameters: SearchAndLazyLoadTemplate, }; export const CustomFilters: Story = { render: (_args, { argTypes }) => ({ components: { BentoFilterBar, BentoToggle, FilterBarCustomFilterSubComponentSingleValueExample, FilterBarCustomFilterSubComponentMultiValueExample, }, props: Object.keys(argTypes), template: ` <div style="padding-top: 24px"> <bento-filter-bar :config="filtersConfig" :filter-values-object="filterValues" @update:filter-values-object="filterValuesChanged" /> </div> `, setup() { interface CustomFilterOptions { required?: boolean; optional?: boolean; disabled?: boolean; } const filtersConfig: BentoFilterBarModel<CustomFilterOptions> = [ { field: 'input', label: 'Input filter', type: BentoFilterItemType.INPUT, options: { placeholder: 'Input filter placeholder', }, visible: true, }, { field: 'custom-single', label: 'Custom single value filter', type: FilterBarCustomFilterSubComponentSingleValueExample, visible: true, }, { field: 'custom-multi', label: 'Custom multi value filter', type: FilterBarCustomFilterSubComponentMultiValueExample, options: { // ICustomFilterOptions disabled: false, optional: false, required: false, }, visible: false, }, { field: 'custom-popover', label: 'Custom filter with popover', type: FilterBarCustomFilterSubComponentWithPopoverExample, visible: true, }, ]; /** * Custom filter values */ type FilterKeys = 'input' | 'custom-single' | 'custom-multi' | 'custom-popover'; type CustomFilterValues = | BentoFilterBarValue | boolean | { // ICustomFilterValues first: boolean; second: boolean; } | undefined; const filterValues = ref<BentoFilterValueObject<FilterKeys, CustomFilterValues>>({ input: undefined, 'custom-single': undefined, 'custom-multi': { first: false, second: false, }, 'custom-popover': undefined, }); const filterValuesChanged = (newFilterValues: BentoFilterValueObject<FilterKeys, CustomFilterValues>) => { filterValues.value = newFilterValues; action('input')(newFilterValues); }; return { filtersConfig, filterValues, filterValuesChanged, }; }, }), args: false, parameters: storybookDocsParameter(FilterBarCustomFiltersExample), };
1
+ import { computed, onMounted, ref } from 'vue'; import { action } from '@storybook/addon-actions'; import { type BentoDateRangeFilterOptions, type BentoFilterBarContainerSizeLayout, type BentoFilterBarModel, type BentoFilterBarSearchBarProps, type BentoFilterBarValue, BentoFilterItemType, type BentoFilterValueObject, type BentoFilterValues, } from './filter-bar.types'; import { getPokemonFromApi, storybookDocsParameter, withToastDecorator } from '@/utils/ts/storybook'; import { type BentoListboxOptions } from '@/types/listbox'; import { BentoCountryCode } from '@/components/country/country.types'; import { BentoCurrencyISOCode } from '@/components/currency/currency.types'; import { isVue2 } from 'vue-demi'; import { type Meta, type StoryObj } from '@storybook/vue'; import { BentoToggle } from '@/components/toggle'; import BentoFilterBar from './filter-bar.vue'; import { endOfWeek } from 'date-fns/endOfWeek'; import { startOfWeek } from 'date-fns/startOfWeek'; import { subDays } from 'date-fns/subDays'; import { subWeeks } from 'date-fns/subWeeks'; import { subMonths } from 'date-fns/subMonths'; import { startOfMonth } from 'date-fns/startOfMonth'; import { endOfDay, startOfDay, subHours, subMinutes } from 'date-fns'; // Examples import FilterBarCustomFilterSubComponentSingleValueExample from './__tests__/filter-bar-custom-filter-sub-component-single-value-example.vue'; import FilterBarCustomFilterSubComponentMultiValueExample from './__tests__/filter-bar-custom-filter-sub-component-multiple-value-example.vue'; import FilterBarCustomFilterSubComponentWithPopoverExample from './__tests__/filter-bar-custom-filter-sub-component-with-popover-example.vue'; import FilterBarCustomFiltersExample from './__tests__/filter-bar-custom-filters-example.vue?raw'; import FilterBarDefaultExample from './__tests__/filter-bar-default-example.vue?raw'; // Generate array from 1 to # // { label: 'Option #', value: # }, const DEFAULT_OPTIONS: BentoListboxOptions = new Array(10) .fill(0) .map((_, index) => ({ label: `Option ${index + 1}`, value: index + 1 })); // To be used by as the current time across all date filters const CURRENT_DATE_AND_TIME = new Date(); const meta: Meta = { title: 'Filter bar', component: BentoFilterBar, decorators: [withToastDecorator], argTypes: { // Props searchTerm: { name: 'searchTerm', defaultValue: undefined, description: "Search term useful to filter all the data by text. If the value is non-existant (`null` or `undefined`) the search bar won't be enabled.", control: { type: 'text', }, }, // Events input: { name: 'input', defaultValue: '', description: 'Triggered whenever the filters are updated.', table: { type: { summary: '([{ field, label, value, type }]) => void' } }, control: { disable: true }, }, // Search Events 'search-input': { name: 'search-input', defaultValue: '', description: "Triggered whenever the filter's search is updated.", table: { type: { summary: '(newSearchTerm) => void' } }, control: { disable: true }, }, 'search-clear': { name: 'search-clear', defaultValue: '', description: "Triggered whenever the filter's search is cleared.", table: { type: { summary: '() => void' } }, control: { disable: true }, }, 'update:search-term': { name: 'update:search-term', defaultValue: '', description: "Triggered whenever the filter's search is updated to sync the model.", table: { type: { summary: '(newSearchTerm) => void' } }, control: { disable: true }, }, }, }; export default meta; type Story = StoryObj<typeof BentoFilterBar>; const FILTER_VALUES: BentoFilterValues = [ { field: 'input', value: undefined, }, { field: 'boolean', value: 'yes', }, { field: 'input-with-dropdown', value: undefined, }, { field: 'hidden-input', value: 'Active value of a hidden filter', }, { field: 'merchant', value: 2, }, { field: 'company', value: [1, 3, 5], }, { field: 'nested-select', value: ['amsterdam', 'den-haag'], }, { field: 'currency', value: [], }, { field: 'range', value: ['', ''], }, { field: 'date', value: CURRENT_DATE_AND_TIME, }, { field: 'date-range', value: { startDate: subDays(startOfDay(CURRENT_DATE_AND_TIME), 2), endDate: endOfDay(CURRENT_DATE_AND_TIME) }, }, { field: 'granular-date-range', value: { startDate: subDays(CURRENT_DATE_AND_TIME, 2), endDate: CURRENT_DATE_AND_TIME }, }, { field: 'date-range-month', value: { startDate: startOfMonth(subMonths(CURRENT_DATE_AND_TIME, 2)), endDate: startOfMonth(CURRENT_DATE_AND_TIME), }, }, { field: 'paymentMethod', value: undefined, }, { field: 'checkbox-group', value: [], }, { field: 'toggle', value: true, }, ]; const FILTERS: BentoFilterBarModel = [ { field: 'input', label: 'Input filter', type: BentoFilterItemType.INPUT, options: { placeholder: 'Input filter placeholder', tooltipPosition: 'bottom', tooltipText: 'Input filter tooltip text', }, visible: true, }, { field: 'input-with-dropdown', label: 'Input with dropdown filter', type: BentoFilterItemType.INPUT_DROPDOWN, visible: true, options: { dropdown: { items: [ { label: 'EUR', value: 'EUR' }, { label: 'USD', value: 'USD' }, ], modelValue: 'EUR', dynamicFiltering: true, }, }, }, { field: 'hidden-input', label: 'Hidden input filter', description: 'This is a hidden input filter only visible through the modal', type: BentoFilterItemType.INPUT, }, { field: 'country', label: 'Country/Region', options: { listboxItems: [ { label: 'Option 1', value: BentoCountryCode.NL, }, { label: 'Option 2', value: BentoCountryCode.BE, }, ], multiple: true, variant: 'capital', }, type: BentoFilterItemType.SELECT_COUNTRY, }, { field: 'avatar', label: 'Avatar', options: { listboxItems: [ { label: 'Avatar 1', value: 'avatar-1', }, { label: 'Avatar 2', value: 'avatar-2', }, ], }, type: BentoFilterItemType.SELECT_AVATAR, }, { field: 'merchant', label: 'Merchant', options: { listboxItems: DEFAULT_OPTIONS, enableValueLabelPair: true, isOptionDisabled: option => option.value === 3, }, type: BentoFilterItemType.SELECT, visible: true, }, { field: 'company', label: 'Company', options: { listboxItems: DEFAULT_OPTIONS, enableValueLabelPair: true, isOptionDisabled: option => option.value === 5, multiple: true, }, type: BentoFilterItemType.SELECT, visible: true, }, { field: 'nested-select', label: 'Nested select', options: { listboxItems: [ { label: 'Belgium', description: 'Country/Region', value: 'belgium', }, { label: 'Netherlands', description: 'Country/Region', value: 'netherlands', items: [ { label: 'Amsterdam', description: 'City', value: 'amsterdam', }, { label: 'Den Haag', description: 'City', value: 'den-haag', }, ], }, ], multiple: true, }, type: BentoFilterItemType.SELECT, visible: true, }, { field: 'currency', label: 'Currency select', options: { listboxItems: [ { label: 'Euro', value: BentoCurrencyISOCode.EUR, }, { label: 'US Dollars', value: BentoCurrencyISOCode.USD, }, ], multiple: true, }, type: BentoFilterItemType.SELECT_CURRENCY, }, { field: 'range', label: 'Range', type: BentoFilterItemType.RANGE, visible: true, }, { field: 'date', label: 'Date', type: BentoFilterItemType.DATE, visible: true, }, { field: 'date-range', label: 'Date range', options: { allowTimeInput: true, numberOfMonths: 1, quickSelectRanges: [ { label: 'Last 15 min', value: 'lastFifteenMin', data: { startDate: subMinutes(CURRENT_DATE_AND_TIME, 15), isRelativeToNow: true, }, }, { label: 'Last 24 hours', value: 'lastTwentyFourHours', data: { startDate: subHours(CURRENT_DATE_AND_TIME, 24), isRelativeToNow: true, }, }, { label: 'Last three days', value: 'lastThreeDays', data: { startDate: subDays(startOfDay(CURRENT_DATE_AND_TIME), 2), }, }, { label: 'Last week', value: 'lastWeek', data: { startDate: subWeeks(startOfDay(CURRENT_DATE_AND_TIME), 1), }, }, ], } as BentoDateRangeFilterOptions, type: BentoFilterItemType.DATE_RANGE, visible: true, }, { field: 'granular-date-range', label: 'Granular date range', options: { allowTimeInput: true, numberOfMonths: 2, granularities: [{ type: 'daily' }, { type: 'weekly' }, { type: 'monthly', disabled: true }], min: subWeeks(startOfWeek(CURRENT_DATE_AND_TIME, { weekStartsOn: 1 }), 12), max: endOfWeek(CURRENT_DATE_AND_TIME, { weekStartsOn: 1 }), } as BentoDateRangeFilterOptions, type: BentoFilterItemType.DATE_RANGE, visible: true, }, { field: 'date-range-month', label: 'Date range (month)', options: { numberOfMonths: 2, variant: 'month', } as BentoDateRangeFilterOptions, type: BentoFilterItemType.DATE_RANGE, visible: true, }, { field: 'paymentMethod', label: 'Payment method', options: { listboxItems: [ { label: 'iDEAL', value: 'ideal' }, { label: 'Visa', value: 'visa' }, ], multiple: true, }, type: BentoFilterItemType.SELECT_PAYMENT_METHOD, }, { field: 'tag', label: 'Tag', options: { listboxItems: [ { label: 'Offline', value: 'unique-id-1', variant: 'red' }, { label: 'Online', value: 'unique-id-2', variant: 'green' }, { label: 'Standby', value: 'unique-id-3', variant: 'orange' }, { label: 'Busy', value: 'unique-id-4', variant: 'blue' }, ], multiple: true, }, type: BentoFilterItemType.SELECT_TAG, }, { field: 'radio-group', label: 'Radio group', options: { items: [ { label: 'Option 1', value: 'option-1' }, { label: 'Option 2', value: 'option-2' }, { label: 'Option 3', value: 'option-3' }, ], }, visible: false, type: BentoFilterItemType.RADIO_GROUP, }, { field: 'checkbox-group', label: 'Checkbox group', options: { checkboxItems: [ { label: 'Item 1', value: 'unique-id-1' }, { label: 'Item 2', value: 'unique-id-2' }, { label: 'Item 3', value: 'unique-id-3' }, { label: 'Item with a very long name', value: 'unique-id-4' }, ], }, type: BentoFilterItemType.CHECKBOX_GROUP, visible: true, }, { field: 'toggle', label: 'Toggle', type: BentoFilterItemType.TOGGLE, visible: true, description: 'Optional description for the toggle filter', options: { tooltipPosition: 'bottom', tooltipText: 'Toggle filter tooltip text', }, }, ]; const FILTERS_WITH_DEFAULTS: BentoFilterBarModel = [ { field: 'input', label: 'Input filter', defaultValue: 'hello', type: BentoFilterItemType.INPUT, }, { field: 'input-with-dropdown', label: 'Input with dropdown filter', type: BentoFilterItemType.INPUT_DROPDOWN, visible: true, defaultValue: { inputValue: 'Peristent input filter', dropdownValue: 'EUR', }, options: { dropdown: { items: [ { label: 'EUR', value: 'EUR' }, { label: 'USD', value: 'USD' }, ], }, }, }, { field: 'range', label: 'Range', defaultValue: ['1', '4'], type: BentoFilterItemType.RANGE, }, { field: 'merchant', label: 'Merchants', defaultValue: ['merchant_1'], options: { listboxItems: [ { label: 'Merchant 1', value: 'merchant_1', }, { label: 'Merchant 2', value: 'merchant_2', }, { label: 'Merchant 3', value: 'merchant_3', }, ], multiple: true, enableValueLabelPair: true, }, type: BentoFilterItemType.SELECT, }, { field: 'thing', label: 'Things', defaultValue: 'merchant_1', options: { listboxItems: [ { label: 'Thing 1', value: 'merchant_1', }, { label: 'Thing 2', value: 'merchant_2', }, { label: 'Thing 3', value: 'merchant_3', }, ], }, type: BentoFilterItemType.SELECT, }, { field: 'country', label: 'Country/Region', options: { listboxItems: [ { label: 'Option 1', value: BentoCountryCode.NL, }, { label: 'Option 2', value: BentoCountryCode.BE, }, ], multiple: true, variant: 'capital', }, type: BentoFilterItemType.SELECT_COUNTRY, }, { field: 'radio-group', label: 'Radio group', defaultValue: 'option-1', options: { items: [ { label: 'Option 1', value: 'option-1' }, { label: 'Option 2', value: 'option-2' }, { label: 'Option 3', value: 'option-3' }, ], }, visible: false, type: BentoFilterItemType.RADIO_GROUP, }, { field: 'checkbox-group', label: 'Checkbox group', defaultValue: ['unique-id-1'], options: { checkboxItems: [ { label: 'Item 1', value: 'unique-id-1' }, { label: 'Item 2', value: 'unique-id-2' }, { label: 'Item 3', value: 'unique-id-3' }, { label: 'Item 4', value: 'unique-id-4' }, ], }, type: BentoFilterItemType.CHECKBOX_GROUP, }, { field: 'date-range', label: 'Date range', defaultValue: { startDate: subDays(startOfDay(CURRENT_DATE_AND_TIME), 2), endDate: endOfDay(CURRENT_DATE_AND_TIME), }, options: { numberOfMonths: 1, quickSelectRanges: [ { label: 'Last three days', value: 'lastThreeDays', data: { startDate: subDays(startOfDay(CURRENT_DATE_AND_TIME), 2), }, }, { label: 'Last week', value: 'lastWeek', data: { startDate: subWeeks(startOfDay(CURRENT_DATE_AND_TIME), 1), }, }, ], } as BentoDateRangeFilterOptions, type: BentoFilterItemType.DATE_RANGE, }, ]; const defaultFilterTemplate = storybookDocsParameter(FilterBarDefaultExample); export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoFilterBar }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const filtersConfig = ref<BentoFilterBarModel>(args.config); const filterValues = ref<BentoFilterValues>(args.filterValues); const filterValuesUpdate = (newFilterValues: BentoFilterValues) => { filterValues.value = newFilterValues; action('update:filter-values')(newFilterValues); }; const filterValuesChanged = (newFilterValues: BentoFilterValues) => { action('input')(newFilterValues); }; const searchTermField = computed(() => args.searchTerm); const searchInput = (newSearchTerm: string) => action('search-input')(newSearchTerm); const searchClear = () => action('search-clear')(); const searchTermSync = (newSearchTerm: string) => action('update:search-term')(newSearchTerm); return { // Props args, // Values filtersConfig, filterValues, searchTermField, // Events filterValuesChanged, filterValuesUpdate, searchInput, searchClear, searchTermSync, }; }, template: ` <div style="padding-top: 24px"> <bento-filter-bar v-bind="args" :config="filtersConfig" :search="{width: '800px'}" :searchTerm="searchTermField" :filter-values="filterValues" @input="filterValuesChanged" @update:filter-values="filterValuesUpdate" @update:search-term="searchTermSync" @search-input="searchInput" @search-clear="searchClear" /> </div> `, }), args: { config: FILTERS, filterValues: FILTER_VALUES, containerSizeLayout: { large: 'multi-line', medium: 'one-line', small: 'multi-line', } satisfies BentoFilterBarContainerSizeLayout, search: { inputFieldAriaLabel: 'Input field aria label', debounceTime: 500, hint: 'Hint', placeholder: 'Placeholder content', } satisfies BentoFilterBarSearchBarProps, searchTerm: 'something', }, parameters: defaultFilterTemplate, }; const persistentFilterTemplate = storybookDocsParameter(` <bento-filter-bar :config="[ { field: 'input', label: 'Input filter', defaultValue: 'hello', type: BentoFilterItemType.INPUT, }, { field: 'range', label: 'Range', defaultValue: ['1', '4'], type: BentoFilterItemType.RANGE, }, { field: 'merchant', label: 'Merchants', defaultValue: ['merchant_1'], options: { listboxItems: [ { label: 'Merchant 1', value: 'merchant_1', }, { label: 'Merchant 2', value: 'merchant_2', }, { label: 'Merchant 3', value: 'merchant_3', }, ], multiple: true, }, type: BentoFilterItemType.SELECT, }, { field: 'thing', label: 'Things', defaultValue: 'merchant_1', options: { listboxItems: [ { label: 'Thing 1', value: 'merchant_1', }, { label: 'Thing 2', value: 'merchant_2', }, { label: 'Thing 3', value: 'merchant_3', }, ], }, type: BentoFilterItemType.SELECT, }, { field: 'country', label: 'Country/Region', options: { listboxItems: [ { label: 'Option 1', value: BentoCountryCode.NL, }, { label: 'Option 2', value: BentoCountryCode.BE, }, ], multiple: true, variant: 'capital', }, type: BentoFilterItemType.SELECT_COUNTRY, }, ]" :filter-values="[ { field: 'input', value: 'non-default value', }, ]" :search="{ inputFieldAriaLabel: 'Input field aria label', debounceTime: 500, hint: 'Hint', placeholder: 'Placeholder content', }" :search-term.sync="searchTerm" @input="onFilter" @search-input="onSearchInput" @search-clear="onSearchClear" /> `); export const Persistent: Story = { ...Default, args: { config: FILTERS_WITH_DEFAULTS, filterValues: [ { field: 'input', value: 'non-default value', }, { field: 'checkbox-group', value: ['unique-id-2'], }, ], searchTerm: null, }, parameters: persistentFilterTemplate, }; const externalFilteringTemplate = storybookDocsParameter(` <template> <bento-filter-bar :config="filtersConfig" :filter-values.sync="filterValues" /> </template> <script setup lang="ts"> import { ref, computed, onMounted } from 'vue'; import { BentoFilterBar, BentoFilterItemType, BentoFilterBarModel, BentoFilterValues, BentoSelectFilterOptions, } from '@adyen/bento-vue2'; // --- Mock API for demonstration --- const getPokemonFromApi = async (searchTerm?: string) => { // In a real application, this would be an API call. // We're mocking it with a timeout and static data. console.log(\`Searching for: \${searchTerm}\`); await new Promise(resolve => setTimeout(resolve, 1000)); const allPokemon = [ { label: 'Bulbasaur', value: 'bulbasaur' }, { label: 'Ivysaur', value: 'ivysaur' }, { label: 'Venusaur', value: 'venusaur' }, { label: 'Charmander', value: 'charmander' }, { label: 'Charmeleon', value: 'charmeleon' }, { label: 'Charizard', value: 'charizard' }, ]; const items = searchTerm ? allPokemon.filter(p => p.label.toLowerCase().includes(searchTerm.toLowerCase())) : allPokemon; return { items }; }; // --- End Mock API --- const filterValues = ref<BentoFilterValues>([{ field: 'pokemon', value: 'bulbasaur' }]); const apiItems = ref([]); const isLoading = ref(false); const getAndSetPokemonFromApi = async (val?: string) => { isLoading.value = true; const { items } = await getPokemonFromApi(val); apiItems.value = items; isLoading.value = false; }; onMounted(() => { getAndSetPokemonFromApi(); }); const filtersConfig = computed<BentoFilterBarModel>(() => [ { field: 'name', label: 'Name', type: BentoFilterItemType.INPUT }, { field: 'pokemon', label: 'Pokemon', options: { isOptionDisabled: option => ['ivysaur', 'charmeleon'].includes(option.value as string), listboxItems: apiItems.value, search: { debounceTime: 500, searchEvent: getAndSetPokemonFromApi }, loading: isLoading.value, }, type: BentoFilterItemType.SELECT, }, ]); </script> `); export const ExternalFilteringSelectFilter: Story = { render: (_args, { argTypes }) => ({ components: { BentoFilterBar, }, props: Object.keys(argTypes), setup(props) { const apiItems = ref([]); const filterValues = ref<BentoFilterValues>([{ field: 'pokemon', value: 'bulbasaur' }]); const isLoading = ref(false); const getAndSetPokemonFromApi = async (val?: string) => { isLoading.value = true; await new Promise(resolve => setTimeout(resolve, 1000)); const { items } = await getPokemonFromApi(val); apiItems.value = items; isLoading.value = false; }; const search = { debounceTime: 500, searchEvent: getAndSetPokemonFromApi, }; onMounted(() => { getAndSetPokemonFromApi(); }); const filtersConfig = computed(() => [ { field: 'name', label: 'Name', type: BentoFilterItemType.INPUT, }, { field: 'pokemon', label: 'Pokemon', options: { isOptionDisabled: option => ['ivysaur', 'charmeleon'].includes(option.value as string), listboxItems: apiItems.value, search, loading: isLoading.value, }, type: BentoFilterItemType.SELECT, visible: false, }, ]); return { args: isVue2 ? props : _args, // Values filtersConfig, filterValues, }; }, template: ` <div style="padding-top: 24px"> <bento-filter-bar v-bind="args" :config="filtersConfig" :filter-values.sync="filterValues" /> </div> `, }), args: { config: FILTERS, searchTerm: null }, parameters: externalFilteringTemplate, }; const SearchAndLazyLoadTemplate = storybookDocsParameter(` <template> <bento-filter-bar :config="filtersConfig" :filter-values.sync="filterValues" /> </template> <script setup lang="ts"> import { ref, computed, onMounted } from 'vue'; import { BentoFilterBar, BentoFilterItemType, BentoFilterBarModel, BentoFilterValues, BentoSelectFilterOptions, BentoListboxOptions, } from '@adyen/bento-vue2'; // --- Mock API for demonstration --- const ALL_ITEMS = Array.from({ length: 100 }, (_, i) => ({ label: \`Item \${i + 1}\`, value: i + 1, })); const fetchItems = (searchTerm: string, page: number, pageSize: number = 20) => { console.log(\`Fetching items with searchTerm: "\${searchTerm}", page: \${page}\`); return new Promise<{ items: BentoListboxOptions; hasMore: boolean }>(resolve => { setTimeout(() => { const filtered = ALL_ITEMS.filter(item => item.label.toLowerCase().includes(searchTerm.toLowerCase()) ); const start = (page - 1) * pageSize; const end = start + pageSize; const pageItems = filtered.slice(start, end); resolve({ items: pageItems, hasMore: end < filtered.length, }); }, 500); }); }; // --- End Mock API --- const filterValues = ref<BentoFilterValues>([]); // State for the select filter const listboxItems = ref<BentoListboxOptions>([]); const isLoading = ref(false); const hasMoreItems = ref(true); const currentPage = ref(1); const currentSearchTerm = ref(''); /** * Handles the search event from the filter. * This should reset pagination and fetch a new list of items. */ const onSearch = async (searchTerm: string) => { currentSearchTerm.value = searchTerm; currentPage.value = 1; isLoading.value = true; const { items, hasMore } = await fetchItems(searchTerm, currentPage.value); listboxItems.value = items; hasMoreItems.value = hasMore; isLoading.value = false; }; /** * Handles the lazy load event. * This should fetch the next page of items and append them. */ const onLazyLoad = async () => { if (isLoading.value || !hasMoreItems.value) { return; } currentPage.value++; isLoading.value = true; const { items, hasMore } = await fetchItems(currentSearchTerm.value, currentPage.value); // Append new items to the existing list listboxItems.value = [...listboxItems.value, ...items]; hasMoreItems.value = hasMore; isLoading.value = false; }; // Load initial data when the component mounts onMounted(() => { onSearch(''); }); const filtersConfig = computed<BentoFilterBarModel>(() => [ { field: 'lazy-select', label: 'Lazy Load Select', type: BentoFilterItemType.SELECT, options: { listboxItems: listboxItems.value, multiple: true, // External filtering configuration search: { searchEvent: onSearch, }, // Lazy loading configuration lazy: { hasMoreItems: hasMoreItems.value, lazyLoadType: 'automatic', // or 'button' loading: isLoading.value, showMoreEvent: onLazyLoad, }, }, }, ]); </script> `); export const ExternalFilteringAndLazyLoadingSelectFilter: Story = { ...ExternalFilteringSelectFilter, render: (_args, { argTypes }) => ({ components: { BentoFilterBar, }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const filterValues = ref<BentoFilterValues>([]); const lazyLoadingPageSize = 20; // State for the select filter const listboxItems = ref<BentoListboxOptions>([]); const isSearchLoading = ref(false); const isLazyLoading = ref(false); const hasMoreItems = ref(true); const currentPage = ref(1); const currentSearchTerm = ref(''); /** * Handles the search event from the filter. * This should reset pagination and fetch a new list of items. */ const onSearch = async (searchTerm: string) => { currentSearchTerm.value = searchTerm; currentPage.value = 1; isSearchLoading.value = true; const { items, hasMore } = await getPokemonFromApi(searchTerm, currentPage.value, lazyLoadingPageSize); listboxItems.value = items; hasMoreItems.value = hasMore; isSearchLoading.value = false; }; /** * Handles the lazy load event. * This should fetch the next page of items and append them. */ const onLazyLoad = async () => { if (isLazyLoading.value || !hasMoreItems.value) { return; } currentPage.value++; isLazyLoading.value = true; const { items, hasMore } = await getPokemonFromApi( currentSearchTerm.value, currentPage.value, lazyLoadingPageSize ); // Append new items to the existing list listboxItems.value = [...listboxItems.value, ...items]; hasMoreItems.value = hasMore; isLazyLoading.value = false; }; // Load initial data when the component mounts onMounted(() => { onSearch(''); }); const filtersConfig = computed<BentoFilterBarModel>(() => [ { field: 'lazy-select', label: 'External filtering and lazy loading', type: BentoFilterItemType.SELECT, options: { listboxItems: listboxItems.value, multiple: true, loading: isSearchLoading.value, // External filtering configuration search: { searchEvent: onSearch, }, // Lazy loading configuration lazy: { hasMoreItems: hasMoreItems.value, lazyLoadType: 'automatic', // or 'button' loading: isLazyLoading.value, showMoreEvent: onLazyLoad, }, }, }, ]); return { args, filtersConfig, filterValues, }; }, template: ` <div style="padding-top: 24px"> <bento-filter-bar v-bind="args" :config="filtersConfig" :filter-values.sync="filterValues" /> </div> `, }), args: { ...ExternalFilteringSelectFilter.args, }, parameters: SearchAndLazyLoadTemplate, }; export const CustomFilters: Story = { render: (_args, { argTypes }) => ({ components: { BentoFilterBar, BentoToggle, FilterBarCustomFilterSubComponentSingleValueExample, FilterBarCustomFilterSubComponentMultiValueExample, }, props: Object.keys(argTypes), template: ` <div style="padding-top: 24px"> <bento-filter-bar :config="filtersConfig" :filter-values-object="filterValues" @update:filter-values-object="filterValuesChanged" /> </div> `, setup() { interface CustomFilterOptions { required?: boolean; optional?: boolean; disabled?: boolean; } const filtersConfig: BentoFilterBarModel<CustomFilterOptions> = [ { field: 'input', label: 'Input filter', type: BentoFilterItemType.INPUT, options: { placeholder: 'Input filter placeholder', }, visible: true, }, { field: 'custom-single', label: 'Custom single value filter', type: FilterBarCustomFilterSubComponentSingleValueExample, visible: true, }, { field: 'custom-multi', label: 'Custom multi value filter', type: FilterBarCustomFilterSubComponentMultiValueExample, options: { // ICustomFilterOptions disabled: false, optional: false, required: false, }, visible: false, }, { field: 'custom-popover', label: 'Custom filter with popover', type: FilterBarCustomFilterSubComponentWithPopoverExample, visible: true, }, ]; /** * Custom filter values */ type FilterKeys = 'input' | 'custom-single' | 'custom-multi' | 'custom-popover'; type CustomFilterValues = | BentoFilterBarValue | boolean | { // ICustomFilterValues first: boolean; second: boolean; } | undefined; const filterValues = ref<BentoFilterValueObject<FilterKeys, CustomFilterValues>>({ input: undefined, 'custom-single': undefined, 'custom-multi': { first: false, second: false, }, 'custom-popover': undefined, }); const filterValuesChanged = (newFilterValues: BentoFilterValueObject<FilterKeys, CustomFilterValues>) => { filterValues.value = newFilterValues; action('input')(newFilterValues); }; return { filtersConfig, filterValues, filterValuesChanged, }; }, }), args: false, parameters: storybookDocsParameter(FilterBarCustomFiltersExample), };
@@ -1 +1 @@
1
- import { type BentoAvatarListboxOptions } from '@/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.types'; import { type BentoListboxEmptyStateProps, type BentoListboxIsOptionDisabled, type BentoListboxLazyLoadType, type BentoListboxOptions, type BentoListboxSearchOptions, type BentoListboxSelectedValue, type BentoListboxVirtulisationOptions, } from '@/types/listbox'; import { type OmitStrict } from '@/types/omit-strict'; import { type VueNodeElement } from '@/types/elements'; import { type BentoCountryListboxOptions } from '@/components/dropdown/components/variants/dropdown-country/dropdown-country.types'; import { type BentoTagListboxOptions } from '@/components/dropdown/components/variants/dropdown-tag/dropdown-tag.types'; import { type BentoCountryVariant } from '@/components/country'; import { type BentoCurrencyListboxOptions } from '@/components/dropdown/components/variants/dropdown-currency/dropdown-currency.types'; import { type BentoPaymentMethodListboxOptions } from '@/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.types'; import { type BentoInputDropdownProps, type BentoInputFieldElementPosition, type BentoInputFieldValue, } from '@/components/input-field'; import { type BentoDatePickerFirstDayOfWeek, type BentoDatePickerIsDateDisabled, type BentoDatePickerVariant, type BentoDatePickerYearRange, } from '@/components/date-picker'; import { type BentoDateRangePickerGranularityConfig, type BentoDateRangePickerValue, type BentoDateRangePickerVariant, type BentoDateRangePickerYearRange, } from '@/components/date-range-picker'; import { type BentoRadioGroupFilterOptions, type BentoRadioGroupFilterValue, } from './components/radio-group-filter/radio-group-filter.types'; import { type DateRangePickerCalendarRangeSelectorItems } from '../date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.types'; import { type BentoSearchBarProps } from '@/components/search-bar/search-bar.types'; import type { BentoInputFilterOptions, BentoInputFilterValue } from './components/input-filter/input-filter.types'; import type { BentoToggleFilterOptions, BentoToggleFilterValue } from './components/toggle-filter/toggle-filter.types'; import type { BentoTooltipPosition } from '@/directives'; import { type BentoCheckboxGroupFilterOptions, type BentoCheckboxGroupFilterValue, } from './components/checkbox-group-filter/checkbox-group-filter.types'; /** * Re-export filter component types */ export type * from './components/input-filter/input-filter.types'; export type * from './components/radio-group-filter/radio-group-filter.types'; export type * from './components/toggle-filter/toggle-filter.types'; export interface BentoSelectFilterOptions { /** * Empty state props that will be used with the inner empty state component to be displayed when no search results are found. */ emptyState?: BentoListboxEmptyStateProps; /** * Option items for the list-box */ listboxItems: BentoListboxOptions; /** * Defines if the list-box should be a multi-select filter */ multiple?: boolean; /** * @deprecated Since v2.0.0. Do not use. The persistent filter label now uses a counter instead of custom strings. This property is now redundant. * The translation object to handle any custom translations. e.g. persistent filter labels. */ messages?: { [key: string]: { persistentButtonLabelPartiallySelected?: string; persistentButtonLabelAllSelected?: string; [key: string]: string; }; }; /** * @deprecated Since v2.0.0. Do not use. The persistent filter label now uses a counter instead of custom strings. This property is now redundant. * The unique key to use in the messages translation object to use for the persistent button label * when the select filter has partially selected items. */ messagesPersistentButtonLabelPartiallySelectedKey?: string; /** * @deprecated Since v2.0.0. Do not use. The persistent filter label now uses a counter instead of custom strings. This property is now redundant. * The unique key to use in the messages translation object to use for the persistent button label * when the select filter has all items selected. */ messagesPersistentButtonLabelAllSelectedKey?: string; /** * The event fire when the filter is opened. */ onOpen?: () => void; lazy?: { /** * Indicates whether there are more items to load */ hasMoreItems: boolean; /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button */ lazyLoadType: BentoListboxLazyLoadType | `${BentoListboxLazyLoadType}`; /** * Indicates if new options are loading. */ loading: boolean; /** * The click event that fires when the user press on the "show more" button OR when the user * scrolls down to bottom of the select filter box. */ showMoreEvent?: () => void; }; /** * Toggles the loading state of the listbox. */ loading?: boolean; /** * Search options and events for the listbox filtering. */ search?: BentoListboxSearchOptions; /** * Defines if the list-box should support static categories */ staticCategories?: boolean; /** * A function that defines which option in the list-box is disabled */ isOptionDisabled?: BentoListboxIsOptionDisabled; /** * Enables virtual scrolling if set to true or by providing an object with itemHeight function. * The itemHeight function is used to calculate the height of rendered item given it's index. */ virtualScroll?: BentoListboxVirtulisationOptions; /** * Attaches a `BentoTooltip` if present. Text showed in the tooltip on hover. */ tooltipText?: string; /** * Position the tooltip should take if space is available. */ tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; /** * Enables value/label value handling. When `true`, the component expects and emits value/label * pair objects instead of primitive values. * * This affects both the `value` prop and the `input` event payload. * * - **Input (`value` prop):** Expects a `{ value: string | number, label: string }` object (or an array of them for multi-select). * - **Output (`input` event):** Emits a `{ value: string | number, label: string }` object (or an array of them) upon selection. * * This is particularly useful when a selected item's label is not available in the `items` prop, * allowing the component to display the correct label for values loaded from external sources. * @default false */ enableValueLabelPair?: boolean; /** * Enables a search bar that dynamically filters values * @default true */ dynamicFiltering?: boolean; } export type BentoSelectAvatarFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoAvatarListboxOptions; }; export type BentoSelectCountryFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoCountryListboxOptions; variant?: BentoCountryVariant | `${BentoCountryVariant}`; }; export type BentoSelectCurrencyFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoCurrencyListboxOptions; }; export type BentoSelectTagFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoTagListboxOptions; }; export type BentoSelectPaymentMethodFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoPaymentMethodListboxOptions; }; export interface BentoBooleanFilterOption { label: string; value: string | boolean; } export interface BentoBooleanFilterOptions { positiveOption?: BentoBooleanFilterOption; negativeOption?: BentoBooleanFilterOption; tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoInputWithDropdownFilterOptions { dropdown: BentoInputDropdownProps; dropdownPosition?: BentoInputFieldElementPosition | `${BentoInputFieldElementPosition}`; tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoRangeFilterOptions { tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } // Date export interface BentoDateFilterOptions { firstDayOfWeek?: BentoDatePickerFirstDayOfWeek; isDateDisabled?: BentoDatePickerIsDateDisabled; yearRange?: BentoDatePickerYearRange; variant?: `${BentoDatePickerVariant}`; tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoDateRangeFilterOptions { allowTimeInput?: boolean; firstDayOfWeek?: BentoDatePickerFirstDayOfWeek; isDateDisabled?: BentoDatePickerIsDateDisabled; yearRange?: BentoDateRangePickerYearRange; granularities?: Array<BentoDateRangePickerGranularityConfig>; maxRange?: number; min?: Date; max?: Date; numberOfMonths?: number; showEndDateOnOpen?: boolean; quickSelectRanges?: DateRangePickerCalendarRangeSelectorItems | boolean; variant?: `${BentoDateRangePickerVariant}`; tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoInpuWithDropdowntFilterValue { inputValue: BentoInputFieldValue; dropdownValue: BentoInputDropdownProps['value']; } export type BentoSelectFilterValue = BentoListboxSelectedValue; export type BentoBooleanFilterValue = string | null; export type BentoDateFilterValue = Date | null; export type BentoDateRangeFilterValue = BentoDateRangePickerValue | null; export type BentoFilterBarValue = | BentoInputFilterValue | BentoInpuWithDropdowntFilterValue | BentoSelectFilterValue | BentoBooleanFilterValue | BentoDateFilterValue | BentoDateRangeFilterValue | BentoRadioGroupFilterValue | BentoCheckboxGroupFilterValue | BentoToggleFilterValue; export type BentoFilterOptions = | BentoSelectFilterOptions | BentoSelectAvatarFilterOptions | BentoSelectCountryFilterOptions | BentoSelectCurrencyFilterOptions | BentoSelectPaymentMethodFilterOptions | BentoSelectTagFilterOptions | BentoDateFilterOptions | BentoDateRangeFilterOptions | BentoBooleanFilterOptions | BentoInputFilterOptions | BentoInputWithDropdownFilterOptions | BentoRadioGroupFilterOptions | BentoCheckboxGroupFilterOptions | BentoToggleFilterOptions | undefined; export enum BentoFilterEvent { // Fires upon the user interacting with the filter INPUT = 'input', // Fires on the (new) filter values being applied UPDATE = 'update', } export enum BentoFilterBarEvent { INPUT = 'input', UPDATE_FILTER_VALUES_OBJECT = 'update:filter-values-object', UPDATE_FILTER_VALUES_ARRAY = 'update:filter-values', } export enum BentoFilterItemType { /** * @deprecated Since v2.0.0. Use `BentoFilterItemType.RADIO_GROUP` instead. */ BOOLEAN = 'BentoBooleanFilter', RANGE = 'BentoRangeFilter', // Input INPUT = 'BentoInputFilter', INPUT_DROPDOWN = 'BentoInputWithDropdownFilter', // Select SELECT = 'BentoSelectFilter', SELECT_AVATAR = 'BentoSelectAvatarFilter', SELECT_COUNTRY = 'BentoSelectCountryFilter', SELECT_CURRENCY = 'BentoSelectCurrencyFilter', SELECT_PAYMENT_METHOD = 'BentoSelectPaymentMethodFilter', SELECT_TAG = 'BentoSelectTagFilter', // Date DATE = 'BentoDateFilter', DATE_RANGE = 'BentoDateRangeFilter', // Radio group RADIO_GROUP = 'BentoRadioGroupFilter', // Checkbox CHECKBOX_GROUP = 'BentoCheckboxGroupFilter', // Toggle TOGGLE = 'BentoToggleFilter', } export interface BentoFilterValue { field: string; value?: BentoFilterBarValue; } export interface BentoFilterModel<CustomOptions = any> extends BentoFilterValue { label: string; description?: string; options?: BentoFilterOptions | CustomOptions; disabled?: boolean; defaultValue?: BentoFilterBarValue; visible?: boolean; /** * Defines the filter to be used. * * Can be one filter defined in the {@link BentoFilterItemType} enum or a * custom filter component defined as a {@link VueNodeElement} */ type: BentoFilterItemType | VueNodeElement; /** * @deprecated Since v2.0.0. Use `config` and `filter-values` instead. */ value?: BentoFilterBarValue; } export type BentoFilterBarModel<CustomOptions = any> = Array<BentoFilterModel<CustomOptions>>; export type BentoFilterValues = Array<BentoFilterValue>; export type BentoFilterValueObject<T extends PropertyKey = string, CustomValue = BentoFilterBarValue> = Record< T, CustomValue >; export type BentoFilterBarSearchBarProps = OmitStrict<BentoSearchBarProps, 'value' | 'condensed'> & { /** * CSS width value applied to the search box element (e.g. `'200px'`, `'50%'`). */ width?: string; }; export type BentoFilterBarSearchTerm = BentoSearchBarProps['value']; export type BentoFilterBarScreenOptionsLayout = 'one-line' | 'multi-line'; export interface BentoFilterBarContainerSizeLayout { /** * @default 'one-line' */ medium?: BentoFilterBarScreenOptionsLayout; /** * @default 'multi-line' */ small?: BentoFilterBarScreenOptionsLayout; } export interface BentoFilterBarProps<CustomValue = BentoFilterBarValue | any, CustomOptions = any> { /** * Array of filters based on @see {@link BentoFilterBarModel}. * Each one of the elements in the array correspond to a filter that will * be created and rendered in the FilterBar. * * @example * [{ * field: 'company', * label: 'Company', * type: FilterType.TEXT, * }, { * field: 'name', * label: 'Name', * type: FilterType.TEXT, * }] */ config?: BentoFilterBarModel<CustomOptions>; /** * An array of the values for each filter in the filter bar. * @see {@link BentoFilterValues} * * @example * [{ * field: 'company', * value: '200', * }, { * field: 'name', * value: 'some text', * }] */ filterValues?: BentoFilterValues; /** * Defines the layout (multi-line or one-line) to be used in the filter bar for different * container sizes: * - Large (>=700px): Always multi-line * - Medium (>=430px & <700px) * - Small (<430px) * * @example * { * medium: 'multi-line', * small: 'one-line' * } */ containerSizeLayout?: BentoFilterBarContainerSizeLayout; /** * Search bar properties with which the placeholder, hint and other * "bento-search-bar" properties can be set. @see {@link BentoFilterBarSearchBarProps} */ search?: BentoFilterBarSearchBarProps; /** * Search term useful to filter all the data by text. * If the value is non-existant (null or undefined) the search bar won't be enabled. * Can be used with `.sync` */ searchTerm?: BentoFilterBarSearchTerm; /** * Will show all applied filters even if their visibility is set to false. */ showAppliedHiddenFilters?: boolean; /** * Array of filters based on @see {@link BentoFilterBarModel}. * Each one of the elements in the array correspond to a filter that will * be created and rendered in the FilterBar. * * Can be used with v-model or independently with the @input event. * * @example * [{ * field: 'company', * label: 'Company', * value: '200', * type: FilterType.TEXT, * }, { * field: 'name', * label: 'Name', * value: null, * type: FilterType.TEXT, * }] * * @deprecated Since v2.0.0. Use `config` instead. */ value?: BentoFilterBarModel; /** * An object of the values for each filter in the filter bar where key is a the field name and the value is the BentoFilterBarValue. * * @example * { * 'company': '200', * 'name': 'some text' * }, */ filterValuesObject?: BentoFilterValueObject<string, CustomValue>; }
1
+ import { type BentoAvatarListboxOptions } from '@/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.types'; import { type BentoListboxEmptyStateProps, type BentoListboxIsOptionDisabled, type BentoListboxLazyLoadType, type BentoListboxOptions, type BentoListboxSearchOptions, type BentoListboxSelectedValue, type BentoListboxVirtulisationOptions, } from '@/types/listbox'; import { type OmitStrict } from '@/types/omit-strict'; import { type VueNodeElement } from '@/types/elements'; import { type BentoCountryListboxOptions } from '@/components/dropdown/components/variants/dropdown-country/dropdown-country.types'; import { type BentoTagListboxOptions } from '@/components/dropdown/components/variants/dropdown-tag/dropdown-tag.types'; import { type BentoCountryVariant } from '@/components/country'; import { type BentoCurrencyListboxOptions } from '@/components/dropdown/components/variants/dropdown-currency/dropdown-currency.types'; import { type BentoPaymentMethodListboxOptions } from '@/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.types'; import { type BentoInputDropdownProps, type BentoInputFieldElementPosition, type BentoInputFieldValue, } from '@/components/input-field'; import { type BentoDatePickerFirstDayOfWeek, type BentoDatePickerIsDateDisabled, type BentoDatePickerVariant, type BentoDatePickerYearRange, } from '@/components/date-picker'; import { type BentoDateRangePickerGranularityConfig, type BentoDateRangePickerValue, type BentoDateRangePickerVariant, type BentoDateRangePickerYearRange, } from '@/components/date-range-picker'; import { type BentoRadioGroupFilterOptions, type BentoRadioGroupFilterValue, } from './components/radio-group-filter/radio-group-filter.types'; import { type DateRangePickerCalendarRangeSelectorItems } from '../date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.types'; import { type BentoSearchBarProps } from '@/components/search-bar/search-bar.types'; import type { BentoInputFilterOptions, BentoInputFilterValue } from './components/input-filter/input-filter.types'; import type { BentoToggleFilterOptions, BentoToggleFilterValue } from './components/toggle-filter/toggle-filter.types'; import type { BentoTooltipPosition } from '@/directives'; import { type BentoCheckboxGroupFilterOptions, type BentoCheckboxGroupFilterValue, } from './components/checkbox-group-filter/checkbox-group-filter.types'; /** * Re-export filter component types */ export type * from './components/input-filter/input-filter.types'; export type * from './components/radio-group-filter/radio-group-filter.types'; export type * from './components/toggle-filter/toggle-filter.types'; export interface BentoSelectFilterOptions { /** * Empty state props that will be used with the inner empty state component to be displayed when no search results are found. */ emptyState?: BentoListboxEmptyStateProps; /** * Option items for the list-box */ listboxItems: BentoListboxOptions; /** * Defines if the list-box should be a multi-select filter */ multiple?: boolean; /** * @deprecated Since v2.0.0. Do not use. The persistent filter label now uses a counter instead of custom strings. This property is now redundant. * The translation object to handle any custom translations. e.g. persistent filter labels. */ messages?: { [key: string]: { persistentButtonLabelPartiallySelected?: string; persistentButtonLabelAllSelected?: string; [key: string]: string; }; }; /** * @deprecated Since v2.0.0. Do not use. The persistent filter label now uses a counter instead of custom strings. This property is now redundant. * The unique key to use in the messages translation object to use for the persistent button label * when the select filter has partially selected items. */ messagesPersistentButtonLabelPartiallySelectedKey?: string; /** * @deprecated Since v2.0.0. Do not use. The persistent filter label now uses a counter instead of custom strings. This property is now redundant. * The unique key to use in the messages translation object to use for the persistent button label * when the select filter has all items selected. */ messagesPersistentButtonLabelAllSelectedKey?: string; /** * The event fire when the filter is opened. */ onOpen?: () => void; lazy?: { /** * Indicates whether there are more items to load */ hasMoreItems: boolean; /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button */ lazyLoadType: BentoListboxLazyLoadType | `${BentoListboxLazyLoadType}`; /** * Indicates if new options are loading. */ loading: boolean; /** * The click event that fires when the user press on the "show more" button OR when the user * scrolls down to bottom of the select filter box. */ showMoreEvent?: () => void; }; /** * Toggles the loading state of the listbox. */ loading?: boolean; /** * Search options and events for the listbox filtering. */ search?: BentoListboxSearchOptions; /** * Defines if the list-box should support static categories */ staticCategories?: boolean; /** * A function that defines which option in the list-box is disabled */ isOptionDisabled?: BentoListboxIsOptionDisabled; /** * Enables virtual scrolling if set to true or by providing an object with itemHeight function. * The itemHeight function is used to calculate the height of rendered item given it's index. */ virtualScroll?: BentoListboxVirtulisationOptions; /** * Attaches a `BentoTooltip` if present. Text showed in the tooltip on hover. */ tooltipText?: string; /** * Position the tooltip should take if space is available. */ tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; /** * Enables value/label value handling. When `true`, the component expects and emits value/label * pair objects instead of primitive values. * * This affects both the `value` prop and the `input` event payload. * * - **Input (`value` prop):** Expects a `{ value: string | number, label: string }` object (or an array of them for multi-select). * - **Output (`input` event):** Emits a `{ value: string | number, label: string }` object (or an array of them) upon selection. * * This is particularly useful when a selected item's label is not available in the `items` prop, * allowing the component to display the correct label for values loaded from external sources. * @default false */ enableValueLabelPair?: boolean; /** * Enables a search bar that dynamically filters values * @default true */ dynamicFiltering?: boolean; } export type BentoSelectAvatarFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoAvatarListboxOptions; }; export type BentoSelectCountryFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoCountryListboxOptions; variant?: BentoCountryVariant | `${BentoCountryVariant}`; }; export type BentoSelectCurrencyFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoCurrencyListboxOptions; }; export type BentoSelectTagFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoTagListboxOptions; }; export type BentoSelectPaymentMethodFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoPaymentMethodListboxOptions; }; export interface BentoBooleanFilterOption { label: string; value: string | boolean; } export interface BentoBooleanFilterOptions { positiveOption?: BentoBooleanFilterOption; negativeOption?: BentoBooleanFilterOption; tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoInputWithDropdownFilterOptions { dropdown: BentoInputDropdownProps; dropdownPosition?: BentoInputFieldElementPosition | `${BentoInputFieldElementPosition}`; tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoRangeFilterOptions { tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } // Date export interface BentoDateFilterOptions { firstDayOfWeek?: BentoDatePickerFirstDayOfWeek; isDateDisabled?: BentoDatePickerIsDateDisabled; yearRange?: BentoDatePickerYearRange; variant?: `${BentoDatePickerVariant}`; tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoDateRangeFilterOptions { allowTimeInput?: boolean; firstDayOfWeek?: BentoDatePickerFirstDayOfWeek; isDateDisabled?: BentoDatePickerIsDateDisabled; yearRange?: BentoDateRangePickerYearRange; granularities?: Array<BentoDateRangePickerGranularityConfig>; maxRange?: number; min?: Date; max?: Date; numberOfMonths?: number; showEndDateOnOpen?: boolean; quickSelectRanges?: DateRangePickerCalendarRangeSelectorItems | boolean; variant?: `${BentoDateRangePickerVariant}`; tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoInpuWithDropdowntFilterValue { inputValue: BentoInputFieldValue; dropdownValue: BentoInputDropdownProps['value']; } export type BentoSelectFilterValue = BentoListboxSelectedValue; export type BentoBooleanFilterValue = string | null; export type BentoDateFilterValue = Date | null; export type BentoDateRangeFilterValue = BentoDateRangePickerValue | null; export type BentoFilterBarValue = | BentoInputFilterValue | BentoInpuWithDropdowntFilterValue | BentoSelectFilterValue | BentoBooleanFilterValue | BentoDateFilterValue | BentoDateRangeFilterValue | BentoRadioGroupFilterValue | BentoCheckboxGroupFilterValue | BentoToggleFilterValue; export type BentoFilterOptions = | BentoSelectFilterOptions | BentoSelectAvatarFilterOptions | BentoSelectCountryFilterOptions | BentoSelectCurrencyFilterOptions | BentoSelectPaymentMethodFilterOptions | BentoSelectTagFilterOptions | BentoDateFilterOptions | BentoDateRangeFilterOptions | BentoBooleanFilterOptions | BentoInputFilterOptions | BentoInputWithDropdownFilterOptions | BentoRadioGroupFilterOptions | BentoCheckboxGroupFilterOptions | BentoToggleFilterOptions | undefined; export enum BentoFilterEvent { // Fires upon the user interacting with the filter INPUT = 'input', // Fires on the (new) filter values being applied UPDATE = 'update', } export enum BentoFilterBarEvent { INPUT = 'input', UPDATE_FILTER_VALUES_OBJECT = 'update:filter-values-object', UPDATE_FILTER_VALUES_ARRAY = 'update:filter-values', } export enum BentoFilterItemType { /** * @deprecated Since v2.0.0. Use `BentoFilterItemType.RADIO_GROUP` instead. */ BOOLEAN = 'BentoBooleanFilter', RANGE = 'BentoRangeFilter', // Input INPUT = 'BentoInputFilter', INPUT_DROPDOWN = 'BentoInputWithDropdownFilter', // Select SELECT = 'BentoSelectFilter', SELECT_AVATAR = 'BentoSelectAvatarFilter', SELECT_COUNTRY = 'BentoSelectCountryFilter', SELECT_CURRENCY = 'BentoSelectCurrencyFilter', SELECT_PAYMENT_METHOD = 'BentoSelectPaymentMethodFilter', SELECT_TAG = 'BentoSelectTagFilter', // Date DATE = 'BentoDateFilter', DATE_RANGE = 'BentoDateRangeFilter', // Radio group RADIO_GROUP = 'BentoRadioGroupFilter', // Checkbox CHECKBOX_GROUP = 'BentoCheckboxGroupFilter', // Toggle TOGGLE = 'BentoToggleFilter', } export interface BentoFilterValue { field: string; value?: BentoFilterBarValue; } export interface BentoFilterModel<CustomOptions = any> extends BentoFilterValue { label: string; description?: string; options?: BentoFilterOptions | CustomOptions; disabled?: boolean; defaultValue?: BentoFilterBarValue; visible?: boolean; /** * Defines the filter to be used. * * Can be one filter defined in the {@link BentoFilterItemType} enum or a * custom filter component defined as a {@link VueNodeElement} */ type: BentoFilterItemType | VueNodeElement; /** * @deprecated Since v2.0.0. Use `config` and `filter-values` instead. */ value?: BentoFilterBarValue; } export type BentoFilterBarModel<CustomOptions = any> = Array<BentoFilterModel<CustomOptions>>; export type BentoFilterValues = Array<BentoFilterValue>; export type BentoFilterValueObject<T extends PropertyKey = string, CustomValue = BentoFilterBarValue> = Record< T, CustomValue >; export type BentoFilterBarSearchBarProps = OmitStrict<BentoSearchBarProps, 'value' | 'condensed'> & { /** * CSS width value applied to the search box element (e.g. `'200px'`, `'50%'`). */ width?: string; }; export type BentoFilterBarSearchTerm = BentoSearchBarProps['value']; export type BentoFilterBarScreenOptionsLayout = 'one-line' | 'multi-line'; export interface BentoFilterBarContainerSizeLayout { /** * @default 'multi-line' */ large?: BentoFilterBarScreenOptionsLayout; /** * @default 'one-line' */ medium?: BentoFilterBarScreenOptionsLayout; /** * @default 'multi-line' */ small?: BentoFilterBarScreenOptionsLayout; } export interface BentoFilterBarProps<CustomValue = BentoFilterBarValue | any, CustomOptions = any> { /** * Array of filters based on @see {@link BentoFilterBarModel}. * Each one of the elements in the array correspond to a filter that will * be created and rendered in the FilterBar. * * @example * [{ * field: 'company', * label: 'Company', * type: FilterType.TEXT, * }, { * field: 'name', * label: 'Name', * type: FilterType.TEXT, * }] */ config?: BentoFilterBarModel<CustomOptions>; /** * An array of the values for each filter in the filter bar. * @see {@link BentoFilterValues} * * @example * [{ * field: 'company', * value: '200', * }, { * field: 'name', * value: 'some text', * }] */ filterValues?: BentoFilterValues; /** * Defines the layout (multi-line or one-line) to be used in the filter bar for different * container sizes: * - Large (>=700px) * - Medium (>=430px & <700px) * - Small (<430px) * * @example * { * large: 'multi-line', * medium: 'multi-line', * small: 'one-line' * } */ containerSizeLayout?: BentoFilterBarContainerSizeLayout; /** * Search bar properties with which the placeholder, hint and other * "bento-search-bar" properties can be set. @see {@link BentoFilterBarSearchBarProps} */ search?: BentoFilterBarSearchBarProps; /** * Search term useful to filter all the data by text. * If the value is non-existant (null or undefined) the search bar won't be enabled. * Can be used with `.sync` */ searchTerm?: BentoFilterBarSearchTerm; /** * Will show all applied filters even if their visibility is set to false. */ showAppliedHiddenFilters?: boolean; /** * Array of filters based on @see {@link BentoFilterBarModel}. * Each one of the elements in the array correspond to a filter that will * be created and rendered in the FilterBar. * * Can be used with v-model or independently with the @input event. * * @example * [{ * field: 'company', * label: 'Company', * value: '200', * type: FilterType.TEXT, * }, { * field: 'name', * label: 'Name', * value: null, * type: FilterType.TEXT, * }] * * @deprecated Since v2.0.0. Use `config` instead. */ value?: BentoFilterBarModel; /** * An object of the values for each filter in the filter bar where key is a the field name and the value is the BentoFilterBarValue. * * @example * { * 'company': '200', * 'name': 'some text' * }, */ filterValuesObject?: BentoFilterValueObject<string, CustomValue>; }
@@ -1 +1 @@
1
- <template> <div ref="filterBarRef" class="b-filter-bar" :class="rootElementConditionalClasses"> <div v-if="!showSearchbarInsideContainer && isSearchBarEnabled" class="b-filter-bar__search-external-container"> <bento-search-bar class="b-filter-bar__search-bar" v-bind="searchBarProps" :input-field-aria-label="searchBarAriaLabel" :value="searchTerm" condensed :style="searchBarWidthStyle" @input="onSearchbarInput" @clear="onSearchbarClear" /> </div> <component :is="screenLayoutComponent" :class="containerConditionalClasses" condensed> <div :class="contentConditionalClasses"> <div v-if="showSearchbarInsideContainer && isSearchBarEnabled" class="b-filter-bar__search-bar-container" :style="searchBarWidthStyle" > <bento-search-bar class="b-filter-bar__search-bar" v-bind="searchBarProps" :input-field-aria-label="searchBarAriaLabel" :value="searchTerm" condensed @input="onSearchbarInput" @clear="onSearchbarClear" /> </div> <template v-if="persistentFilters.length"> <component :is="filter.type" v-for="filter in persistentFilters" :key="filter.field" :field="filter.field" :label="filter.label" :disabled="filter?.disabled" :options="filter.options" :value="filter.value" :default-value="filter.defaultValue" @update="updateFilterHandler" /> </template> <component :is="filter.type" v-for="filter in regularFilters" :key="filter.field" :field="filter.field" :label="filter.label" :disabled="filter?.disabled" :options="filter.options" :value="filter.value" @update="updateFilterHandler" /> <!-- All filters button --> <all-filters-modal v-if="hasHiddenFilters" class="b-filter-bar__all-filters-modal" :value="dynamicFilters" @input="updateFilterHandler" /> <!-- Clear all filters button --> <filter-bar-button v-if="isClearAllVisible" class="b-filter-bar__clear-all" @click="clearAllFilters"> {{ t('clearFilters') }} <template #iconLeft> <refresh-icon aria-hidden="true" /> </template> </filter-bar-button> <!-- Reset all filters button --> <filter-bar-button v-if="isResetAllVisible" class="b-filter-bar__reset-all" @click="resetAllFilters"> {{ t('resetFilters') }} <template #iconLeft> <refresh-icon aria-hidden="true" /> </template> </filter-bar-button> </div> </component> </div> </template> <script setup lang="ts"> import { computed, ref, toRef } from 'vue'; import { FilterBarButton } from './components/filter-bar-button'; import { BentoFilterBarEvent, type BentoFilterBarModel, type BentoFilterBarProps, type BentoFilterModel, } from './filter-bar.types'; import BentoSearchBar from '@/components/search-bar/search-bar.vue'; import { BentoSearchBarEvent } from '@/components/search-bar/search-bar.types'; import { useBentoToastController } from '@/composables'; import { useI18n } from '@/utils/ts/i18n'; import { useContainerSizeOptionsLayout } from './composables/use-container-size-options-layout/use-container-size-options-layout'; import AllFiltersModal from './components/all-filters-modal/all-filters-modal.vue'; import RefreshIcon from '@adyen/ui-assets-icons-16/vue/refresh'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; // TODO: setup Vue3 generics in Bento 2.0.0 const props = withDefaults(defineProps<BentoFilterBarProps>(), { config: () => [], filterValues: () => [], containerSizeLayout: () => ({ medium: 'one-line', small: 'multi-line', }), search: undefined, searchTerm: undefined, value: () => [], showAppliedHiddenFilters: false, filterValuesObject: undefined, }); const emit = defineEmits([ BentoFilterBarEvent.INPUT, BentoFilterBarEvent.UPDATE_FILTER_VALUES_OBJECT, BentoFilterBarEvent.UPDATE_FILTER_VALUES_ARRAY, BentoSearchBarEvent.CLEAR, BentoSearchBarEvent.INPUT, BentoSearchBarEvent.UPDATE, ]); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const filterBarRef = ref<HTMLDivElement>(null); const valueAndFilterValuesPropsMerged = computed<BentoFilterBarModel>(() => (props.config?.length > 0 ? props.config : props.value)?.map(filterModel => { let filterValue = props.filterValues.find(({ field }) => field === filterModel.field)?.value; if (props.filterValuesObject && filterModel.field in props.filterValuesObject) { filterValue = props.filterValuesObject[filterModel.field]; } if (filterValue === undefined) { // If the filter is using the deprecated `config` value prop use that // But if that is also not set then check if a default value exists set it to that if it is defined. filterValue = filterModel?.value === undefined && filterModel?.defaultValue !== undefined ? filterModel?.defaultValue : filterModel?.value; } return { ...filterModel, value: filterValue !== undefined ? filterValue : filterModel?.value }; }) ); const { addToast } = useBentoToastController(); /** * Container size layout. * Allows to change the layout from multi-line to one-line for each container size type: * - large: >=700 * - medium: >=430 & <700 * - small: <430 */ const { screenLayoutComponent, showSearchbarInsideContainer, isOneLineLayout, isLargeLayout, rootElementConditionalClasses, } = useContainerSizeOptionsLayout(filterBarRef, toRef(props, 'containerSizeLayout')); /** * Classes */ /** * Handles the classes of the swappable component ('div' or 'fixed-scroller) */ const containerConditionalClasses = computed(() => ({ 'b-filter-bar__fixed-scroller': isOneLineLayout.value, 'b-filter-bar__container': !isOneLineLayout.value, // Multi-line })); /** * Handles the classes of the container inside the swappable component. */ const contentConditionalClasses = computed(() => ({ 'b-filter-bar__scroller-content': isOneLineLayout.value, 'b-filter-bar__filters': !isOneLineLayout.value, // Multi-line })); /** * Search bar pops */ const searchBarAriaLabel = computed(() => props.search?.inputFieldAriaLabel || (t('searchBar') as string)); const searchBarProps = computed(() => { if (!props.search) { return undefined; } const { debounceTime, disabled, inputFieldAriaLabel, hint, placeholder } = props.search; return { debounceTime, disabled, inputFieldAriaLabel, hint, placeholder }; }); const searchBarWidthStyle = computed(() => props.search?.width && isLargeLayout.value ? { flexBasis: props.search.width, maxWidth: props.search.width } : undefined ); const hasHiddenFilters = computed(() => { // Has props with visible set const hasVisibleSet = valueAndFilterValuesPropsMerged.value.some( ({ visible }) => visible !== undefined && visible !== null ); return hasVisibleSet ? valueAndFilterValuesPropsMerged.value.some(({ visible }) => !visible) : false; }); // Persistent filters i.e. filters with default value const persistentFilters = computed(() => valueAndFilterValuesPropsMerged.value.filter(({ defaultValue }) => defaultValue !== undefined) ); // Dynamic filters i.e. filters without default value const dynamicFilters = computed(() => valueAndFilterValuesPropsMerged.value.filter(({ defaultValue }) => defaultValue === undefined) ); // Show non-persistent & visible filters const regularFilters = computed(() => { const hasVisibleSet = valueAndFilterValuesPropsMerged.value.some( ({ visible }) => visible !== undefined && visible !== null ); // If no filter has the `visible` prop set then all are visible, otherwise search for the "visible: true" elements return hasVisibleSet ? valueAndFilterValuesPropsMerged.value.filter( ({ defaultValue, visible, value }) => // eslint-disable-next-line eqeqeq (defaultValue === undefined && visible) || (props.showAppliedHiddenFilters && value != undefined) ) : valueAndFilterValuesPropsMerged.value.filter( ({ defaultValue, value }) => // eslint-disable-next-line eqeqeq defaultValue === undefined || (props.showAppliedHiddenFilters && value != undefined) ); }); // Enable search bar. // Soft equality to check for "null" and "undefined" // eslint-disable-next-line eqeqeq const isSearchBarEnabled = computed(() => props.searchTerm != undefined); const hasActiveFilters = computed(() => valueAndFilterValuesPropsMerged.value .filter(({ defaultValue }) => defaultValue === undefined || defaultValue === null) // Disabling strict equality to check for both undefined and null // eslint-disable-next-line eqeqeq .some(({ value }) => value != undefined) ); // Show the "Clear all" button (true) if at least one filter is active (contains a value) // and there are not persistent filters present. const isClearAllVisible = computed(() => persistentFilters.value.length === 0 && hasActiveFilters.value); const isResetAllVisible = computed(() => { if (persistentFilters.value.length === 0) { return false; } const areAllValuesEqualToDefaults = valueAndFilterValuesPropsMerged.value .filter(({ defaultValue }) => defaultValue !== undefined || defaultValue !== null) .every(({ value, defaultValue }) => JSON.stringify(value) === JSON.stringify(defaultValue)); return !areAllValuesEqualToDefaults || hasActiveFilters.value; }); const updateAllFilters = (newFilters: BentoFilterBarModel) => { emit(BentoFilterBarEvent.INPUT, newFilters); emit( BentoFilterBarEvent.UPDATE_FILTER_VALUES_ARRAY, newFilters ?.filter(({ value }) => value !== null || value !== undefined) .map(({ field, value }) => ({ field, value })) ); emit( BentoFilterBarEvent.UPDATE_FILTER_VALUES_OBJECT, newFilters ?.filter(({ value }) => value !== null || value !== undefined) .reduce((acc, filter) => { acc[filter.field] = filter.value; return acc; }, {}) ); }; const updateFilterHandler = (updatedFilters: BentoFilterModel | BentoFilterBarModel) => { const filtersArray = Array.isArray(updatedFilters) ? updatedFilters : [updatedFilters]; const updatedFiltersMap = new Map(filtersArray.map(filter => [filter.field, filter.value])); const newFilters = valueAndFilterValuesPropsMerged.value.map(filter => { if (updatedFiltersMap.has(filter.field)) { return { ...filter, value: updatedFiltersMap.get(filter.field) }; } return filter; }); updateAllFilters(newFilters); }; const clearAllFilters = () => { const previousFilters = valueAndFilterValuesPropsMerged.value.slice(); const clearedFilters = valueAndFilterValuesPropsMerged.value.map(({ value, ...otherFields }) => ({ ...otherFields, value: undefined, })); addToast({ text: t('filtersHaveBeenCleared') as string, action: { handler: () => updateAllFilters(previousFilters), text: t('undo') as string, }, }); updateAllFilters(clearedFilters); }; const resetAllFilters = () => { const previousFilters = valueAndFilterValuesPropsMerged.value.slice(); const resetFilters = valueAndFilterValuesPropsMerged.value.map(({ defaultValue, ...otherFields }) => ({ ...otherFields, defaultValue, value: defaultValue, })); addToast({ text: t('filtersHaveBeenReset') as string, action: { handler: () => updateAllFilters(previousFilters), text: t('undo') as string, }, }); updateAllFilters(resetFilters); }; const onSearchbarInput = (newSearchTerm: string) => { emit(BentoSearchBarEvent.INPUT, newSearchTerm); emit(BentoSearchBarEvent.UPDATE, newSearchTerm); }; const onSearchbarClear = () => { emit(BentoSearchBarEvent.CLEAR); }; // Expose methods for BentoDataGrid to access defineExpose({ isClearAllVisible, clearAllFilters, isResetAllVisible, resetAllFilters }); </script> <script lang="ts"> import { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoToggleFilter, } from './components'; import { BentoTeleport, FixedScroller } from '@/internal'; /** * The filter bar filters item lists and tables according to various filter criteria. * * @example * import { BentoFilterBar } from '@adyen/bento-vue2' * * export default { * components: { BentoFilterBar }, * data: () => ({ * filters: [{ * field: 'name', * label: 'Name', * value: null, * options: { ... }, * type: FilterType.TEXT, * }] * }), * template: ` * <bento-filter-bar * v-model="filter" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-filter-bar', components: { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoTeleport, BentoToggleFilter, FixedScroller, }, }; </script> <style lang="scss" scoped src="./filter-bar.scss" />
1
+ <template> <div ref="filterBarRef" class="b-filter-bar" :class="rootElementConditionalClasses"> <div v-if="!showSearchbarInsideContainer && isSearchBarEnabled" class="b-filter-bar__search-external-container"> <bento-search-bar class="b-filter-bar__search-bar" v-bind="searchBarProps" :input-field-aria-label="searchBarAriaLabel" :value="searchTerm" condensed :style="searchBarWidthStyle" @input="onSearchbarInput" @clear="onSearchbarClear" /> </div> <component :is="screenLayoutComponent" :class="containerConditionalClasses" condensed> <div :class="contentConditionalClasses"> <div v-if="showSearchbarInsideContainer && isSearchBarEnabled" class="b-filter-bar__search-bar-container" :style="searchBarWidthStyle" > <bento-search-bar class="b-filter-bar__search-bar" v-bind="searchBarProps" :input-field-aria-label="searchBarAriaLabel" :value="searchTerm" condensed @input="onSearchbarInput" @clear="onSearchbarClear" /> </div> <template v-if="persistentFilters.length"> <component :is="filter.type" v-for="filter in persistentFilters" :key="filter.field" :field="filter.field" :label="filter.label" :disabled="filter?.disabled" :options="filter.options" :value="filter.value" :default-value="filter.defaultValue" @update="updateFilterHandler" /> </template> <component :is="filter.type" v-for="filter in regularFilters" :key="filter.field" :field="filter.field" :label="filter.label" :disabled="filter?.disabled" :options="filter.options" :value="filter.value" @update="updateFilterHandler" /> <!-- All filters button --> <all-filters-modal v-if="hasHiddenFilters" class="b-filter-bar__all-filters-modal" :value="dynamicFilters" @input="updateFilterHandler" /> <!-- Clear all filters button --> <filter-bar-button v-if="isClearAllVisible" class="b-filter-bar__clear-all" @click="clearAllFilters"> {{ t('clearFilters') }} <template #iconLeft> <refresh-icon aria-hidden="true" /> </template> </filter-bar-button> <!-- Reset all filters button --> <filter-bar-button v-if="isResetAllVisible" class="b-filter-bar__reset-all" @click="resetAllFilters"> {{ t('resetFilters') }} <template #iconLeft> <refresh-icon aria-hidden="true" /> </template> </filter-bar-button> </div> </component> </div> </template> <script setup lang="ts"> import { computed, ref } from 'vue'; import { FilterBarButton } from './components/filter-bar-button'; import { type BentoFilterBarContainerSizeLayout, BentoFilterBarEvent, type BentoFilterBarModel, type BentoFilterBarProps, type BentoFilterModel, } from './filter-bar.types'; import BentoSearchBar from '@/components/search-bar/search-bar.vue'; import { BentoSearchBarEvent } from '@/components/search-bar/search-bar.types'; import { useBentoToastController } from '@/composables'; import { useI18n } from '@/utils/ts/i18n'; import { useContainerSizeOptionsLayout } from './composables/use-container-size-options-layout/use-container-size-options-layout'; import AllFiltersModal from './components/all-filters-modal/all-filters-modal.vue'; import RefreshIcon from '@adyen/ui-assets-icons-16/vue/refresh'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; // TODO: setup Vue3 generics in Bento 2.0.0 const props = withDefaults(defineProps<BentoFilterBarProps>(), { config: () => [], filterValues: () => [], containerSizeLayout: null, search: undefined, searchTerm: undefined, value: () => [], showAppliedHiddenFilters: false, filterValuesObject: undefined, }); const emit = defineEmits([ BentoFilterBarEvent.INPUT, BentoFilterBarEvent.UPDATE_FILTER_VALUES_OBJECT, BentoFilterBarEvent.UPDATE_FILTER_VALUES_ARRAY, BentoSearchBarEvent.CLEAR, BentoSearchBarEvent.INPUT, BentoSearchBarEvent.UPDATE, ]); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const filterBarRef = ref<HTMLDivElement>(null); const valueAndFilterValuesPropsMerged = computed<BentoFilterBarModel>(() => (props.config?.length > 0 ? props.config : props.value)?.map(filterModel => { let filterValue = props.filterValues.find(({ field }) => field === filterModel.field)?.value; if (props.filterValuesObject && filterModel.field in props.filterValuesObject) { filterValue = props.filterValuesObject[filterModel.field]; } if (filterValue === undefined) { // If the filter is using the deprecated `config` value prop use that // But if that is also not set then check if a default value exists set it to that if it is defined. filterValue = filterModel?.value === undefined && filterModel?.defaultValue !== undefined ? filterModel?.defaultValue : filterModel?.value; } return { ...filterModel, value: filterValue !== undefined ? filterValue : filterModel?.value }; }) ); const { addToast } = useBentoToastController(); /** * Container size layout. * Allows to change the layout from multi-line to one-line for each container size type: * - large: >=700 * - medium: >=430 & <700 * - small: <430 */ const DEFAULT_CONTAINER_SIZE_LAYOUT: Required<BentoFilterBarContainerSizeLayout> = { large: 'multi-line', medium: 'one-line', small: 'multi-line', }; const mergedContainerSizeLayout = computed<Required<BentoFilterBarContainerSizeLayout>>(() => ({ ...DEFAULT_CONTAINER_SIZE_LAYOUT, ...props.containerSizeLayout, })); const { screenLayoutComponent, showSearchbarInsideContainer, isOneLineLayout, isLargeLayout, rootElementConditionalClasses, } = useContainerSizeOptionsLayout(filterBarRef, mergedContainerSizeLayout); /** * Classes */ /** * Handles the classes of the swappable component ('div' or 'fixed-scroller) */ const containerConditionalClasses = computed(() => ({ 'b-filter-bar__fixed-scroller': isOneLineLayout.value, 'b-filter-bar__container': !isOneLineLayout.value, // Multi-line })); /** * Handles the classes of the container inside the swappable component. */ const contentConditionalClasses = computed(() => ({ 'b-filter-bar__scroller-content': isOneLineLayout.value, 'b-filter-bar__filters': !isOneLineLayout.value, // Multi-line })); /** * Search bar pops */ const searchBarAriaLabel = computed(() => props.search?.inputFieldAriaLabel || (t('searchBar') as string)); const searchBarProps = computed(() => { if (!props.search) { return undefined; } const { debounceTime, disabled, inputFieldAriaLabel, hint, placeholder } = props.search; return { debounceTime, disabled, inputFieldAriaLabel, hint, placeholder }; }); const searchBarWidthStyle = computed(() => props.search?.width && isLargeLayout.value ? { flexBasis: props.search.width, maxWidth: props.search.width } : undefined ); const hasHiddenFilters = computed(() => { // Has props with visible set const hasVisibleSet = valueAndFilterValuesPropsMerged.value.some( ({ visible }) => visible !== undefined && visible !== null ); return hasVisibleSet ? valueAndFilterValuesPropsMerged.value.some(({ visible }) => !visible) : false; }); // Persistent filters i.e. filters with default value const persistentFilters = computed(() => valueAndFilterValuesPropsMerged.value.filter(({ defaultValue }) => defaultValue !== undefined) ); // Dynamic filters i.e. filters without default value const dynamicFilters = computed(() => valueAndFilterValuesPropsMerged.value.filter(({ defaultValue }) => defaultValue === undefined) ); // Show non-persistent & visible filters const regularFilters = computed(() => { const hasVisibleSet = valueAndFilterValuesPropsMerged.value.some( ({ visible }) => visible !== undefined && visible !== null ); // If no filter has the `visible` prop set then all are visible, otherwise search for the "visible: true" elements return hasVisibleSet ? valueAndFilterValuesPropsMerged.value.filter( ({ defaultValue, visible, value }) => // eslint-disable-next-line eqeqeq (defaultValue === undefined && visible) || (props.showAppliedHiddenFilters && value != undefined) ) : valueAndFilterValuesPropsMerged.value.filter( ({ defaultValue, value }) => // eslint-disable-next-line eqeqeq defaultValue === undefined || (props.showAppliedHiddenFilters && value != undefined) ); }); // Enable search bar. // Soft equality to check for "null" and "undefined" // eslint-disable-next-line eqeqeq const isSearchBarEnabled = computed(() => props.searchTerm != undefined); const hasActiveFilters = computed(() => valueAndFilterValuesPropsMerged.value .filter(({ defaultValue }) => defaultValue === undefined || defaultValue === null) // Disabling strict equality to check for both undefined and null // eslint-disable-next-line eqeqeq .some(({ value }) => value != undefined) ); // Show the "Clear all" button (true) if at least one filter is active (contains a value) // and there are not persistent filters present. const isClearAllVisible = computed(() => persistentFilters.value.length === 0 && hasActiveFilters.value); const isResetAllVisible = computed(() => { if (persistentFilters.value.length === 0) { return false; } const areAllValuesEqualToDefaults = valueAndFilterValuesPropsMerged.value .filter(({ defaultValue }) => defaultValue !== undefined || defaultValue !== null) .every(({ value, defaultValue }) => JSON.stringify(value) === JSON.stringify(defaultValue)); return !areAllValuesEqualToDefaults || hasActiveFilters.value; }); const updateAllFilters = (newFilters: BentoFilterBarModel) => { emit(BentoFilterBarEvent.INPUT, newFilters); emit( BentoFilterBarEvent.UPDATE_FILTER_VALUES_ARRAY, newFilters ?.filter(({ value }) => value !== null || value !== undefined) .map(({ field, value }) => ({ field, value })) ); emit( BentoFilterBarEvent.UPDATE_FILTER_VALUES_OBJECT, newFilters ?.filter(({ value }) => value !== null || value !== undefined) .reduce((acc, filter) => { acc[filter.field] = filter.value; return acc; }, {}) ); }; const updateFilterHandler = (updatedFilters: BentoFilterModel | BentoFilterBarModel) => { const filtersArray = Array.isArray(updatedFilters) ? updatedFilters : [updatedFilters]; const updatedFiltersMap = new Map(filtersArray.map(filter => [filter.field, filter.value])); const newFilters = valueAndFilterValuesPropsMerged.value.map(filter => { if (updatedFiltersMap.has(filter.field)) { return { ...filter, value: updatedFiltersMap.get(filter.field) }; } return filter; }); updateAllFilters(newFilters); }; const clearAllFilters = () => { const previousFilters = valueAndFilterValuesPropsMerged.value.slice(); const clearedFilters = valueAndFilterValuesPropsMerged.value.map(({ value, ...otherFields }) => ({ ...otherFields, value: undefined, })); addToast({ text: t('filtersHaveBeenCleared') as string, action: { handler: () => updateAllFilters(previousFilters), text: t('undo') as string, }, }); updateAllFilters(clearedFilters); }; const resetAllFilters = () => { const previousFilters = valueAndFilterValuesPropsMerged.value.slice(); const resetFilters = valueAndFilterValuesPropsMerged.value.map(({ defaultValue, ...otherFields }) => ({ ...otherFields, defaultValue, value: defaultValue, })); addToast({ text: t('filtersHaveBeenReset') as string, action: { handler: () => updateAllFilters(previousFilters), text: t('undo') as string, }, }); updateAllFilters(resetFilters); }; const onSearchbarInput = (newSearchTerm: string) => { emit(BentoSearchBarEvent.INPUT, newSearchTerm); emit(BentoSearchBarEvent.UPDATE, newSearchTerm); }; const onSearchbarClear = () => { emit(BentoSearchBarEvent.CLEAR); }; // Expose methods for BentoDataGrid to access defineExpose({ isClearAllVisible, clearAllFilters, isResetAllVisible, resetAllFilters }); </script> <script lang="ts"> import { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoToggleFilter, } from './components'; import { BentoTeleport, FixedScroller } from '@/internal'; /** * The filter bar filters item lists and tables according to various filter criteria. * * @example * import { BentoFilterBar } from '@adyen/bento-vue2' * * export default { * components: { BentoFilterBar }, * data: () => ({ * filters: [{ * field: 'name', * label: 'Name', * value: null, * options: { ... }, * type: FilterType.TEXT, * }] * }), * template: ` * <bento-filter-bar * v-model="filter" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-filter-bar', components: { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoTeleport, BentoToggleFilter, FixedScroller, }, }; </script> <style lang="scss" scoped src="./filter-bar.scss" />
@@ -40,6 +40,14 @@ The only compulsory element of a header is title. Everything else can be turned
40
40
  Buttons section should contain one primary and one secondary button, and a more options menu for any potential
41
41
  additional actions. These are all passed as a `BentoButtonActionsList` to the component.
42
42
 
43
+ To listen to additional DOM events on the action buttons, such as `mouseenter` or `mouseleave`, use the action object's
44
+ `elementRef`. See
45
+ [additional DOM events on action buttons](../?path=/docs/components-button-button-actions--docs#additional-dom-events-on-action-buttons)
46
+ in the `bento-button-actions` documentation.
47
+
48
+ In `bento-header`, only the displayed action buttons can be referenced this way. Extra actions are placed in the more
49
+ options menu.
50
+
43
51
  ### Info icon content
44
52
 
45
53
  If a page or component description and link exists, it’s displayed inside of a popover next to the title. Users can
@@ -39,6 +39,18 @@ The user can provide an empty state or a "default" view.
39
39
  5. Listen to `reorder:view`.
40
40
  6. On emission, update the order of your views that you have passed into the prop.
41
41
 
42
+ ### Actions
43
+
44
+ The `actions` prop renders action buttons in the header using `bento-button-actions`.
45
+
46
+ To listen to additional DOM events on the action buttons, such as `mouseenter` or `mouseleave`, use the action object's
47
+ `elementRef`. See
48
+ [additional DOM events on action buttons](../?path=/docs/components-button-button-actions--docs#additional-dom-events-on-action-buttons)
49
+ in the `bento-button-actions` documentation.
50
+
51
+ In `bento-header-with-views`, only the displayed action buttons can be referenced this way. Extra actions are placed in
52
+ the more options menu.
53
+
42
54
  ## Modifiers
43
55
 
44
56
  ### Controlled Active View
@@ -1 +1 @@
1
- <template> <bento-typography class="b-input-field" v-bind="getRootAttributes($attrs)" :data-testid="getDataTestId($attrs)" :class="inputFieldConditionalClasses" el="div" variant="body" > <field-label v-if="hasSlot('default') || label" :for="inputFieldId" data-testid="input-field-label" :tooltip-text="tooltipText" :optional="optional" :required="required" :label="label" @click="focusInput" > <slot></slot> </field-label> <div class="b-input-field__input-box" data-testid="input-box" :aria-disabled="disabled" @click="focusInput"> <!-- Optional Icon for Default text variant --> <span v-if="isDefaultVariant && (hasSlot('defaultIconBefore') || hasSlot('iconBefore'))" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-before" > <slot v-if="hasSlot('iconBefore')" name="iconBefore" /> <slot v-if="hasSlot('defaultIconBefore') && !hasSlot('iconBefore')" name="defaultIconBefore" /> </span> <!-- Mandatory Icon for Payment Method variant --> <span v-if="isPaymentMethodVariant && hasSlot('paymentMethod')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__payment-method" > <slot name="paymentMethod" /> </span> <!-- Dropdown at start --> <div v-if="shouldDisplayDropdownAtStart" :id="dropdownId" class="b-input-field__dropdown b-input-field__dropdown--start" @click.stop > <bento-dropdown v-if="dropdown" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> <!-- Static Value at start --> <bento-typography v-if="shouldDisplayStaticValueAtStart" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--start" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <div class="b-input-field__input-container"> <input :id="inputFieldId" v-bind="$attrs" ref="inputFieldElement" class="b-input-field__input" :aria-label="computedAriaLabel" :aria-describedby="computedAriaDescribedBy" :aria-owns="computedAriaOwns" :aria-invalid="shouldShowError" :placeholder="placeholder" :required="required" :aria-required="required" :type="type" :value="modelValue ?? value" :disabled="disabled" :readonly="isReadOnly" @input="onInput" @change="emit('change')" @focus="onFocus" @blur="onBlur" @keydown="emit('keydown', $event)" @keyup="emit('keyup', $event)" @keydown.esc="emit('escape-pressed')" @click="emit('click', $event)" /> <!-- Hint at the end --> <bento-typography v-if="hint && isFocused && (modelValue || value)" el="span" class="b-input-field__hint" > {{ hint }} </bento-typography> </div> <span v-if="hasSlot('iconAfter')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-after" data-testid="input-text__icon-after" > <slot name="iconAfter" /> </span> <!-- Static Value at the end --> <bento-typography v-if="shouldDisplayStaticValueAtEnd" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--end" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <!-- Dropdown at end --> <div v-if="shouldDisplayDropdownAtEnd" :id="dropdownId" data-testid="input-field-dropdown-container-end" class="b-input-field__dropdown b-input-field__dropdown--end" @click.stop > <bento-dropdown v-if="dropdown" :id="dropdownId" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> </div> <error-message v-if="shouldShowError && !!errorMessage" :id="errorId" :error-message="errorMessage" class="b-input-field__error-message" /> <span v-if="hasSlot('description') || description" :id="descriptionId" class="b-input-field__description" @click="focusInput" > <bento-typography el="span" variant="body"> <slot id="description" name="description"> {{ description }} </slot> </bento-typography> </span> </bento-typography> </template> <script setup lang="ts"> import { computed, inject, provide, type Ref, ref, toRef, toRefs, useAttrs, useSlots } from 'vue'; // Components import { BentoDropdown } from '@/components/dropdown'; import { DROPDOWN_SMALL_SIZE_INJECTION_KEY } from '@/components/dropdown/dropdown.keys'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { generateUid } from '@/core/utils/ts'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { INPUT_FIELD_HINT_INJECTION_KEY, INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, } from '@/components/input-field/input-field.keys'; import { type BentoListboxSelectedValue } from '@/types/listbox'; // Composables import { useFormLayoutFieldLoading, useHasSlot } from '@/composables'; // Types import { type BentoInputDropdownProps, BentoInputFieldElementPosition, type BentoInputFieldProps, BentoInputFieldStateClass, BentoInputFieldType, type BentoInputFieldValue, BentoInputFieldVariant, } from './input-field.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const props = withDefaults(defineProps<BentoInputFieldProps>(), { ariaHidden: true, ariaLabel: undefined, condensed: false, description: '', disabled: false, dropdown: undefined, dropdownPosition: BentoInputFieldElementPosition.START, error: false, errorMessage: null, label: '', modelValue: undefined, optional: false, placeholder: '', required: false, readonly: false, slashedZero: false, staticValue: '', staticValuePosition: BentoInputFieldElementPosition.START, tooltipText: null, type: BentoInputFieldType.TEXT, value: undefined, variant: BentoInputFieldVariant.DEFAULT, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the input is clicked */ (e: 'click', event: MouseEvent); /** * Emitted when the user modifies the element's value. Unlike the 'input' event, the change event is not necessarily fired for each alteration to an element's value */ (e: 'change'): void; /** * Emitted when the Input Field's internal dropdown changes it's value */ (e: 'dropdown-input', selectedValue: BentoListboxSelectedValue): void; /** * Emitted when the "ESC" key is pressed */ (e: 'escape-pressed'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when a key is pressed and lifted up */ (e: 'keyup', event: KeyboardEvent): void; /** * Emitted when the component's model changes * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', inputValue: string): void; /** * Emitted when the Input Field's internal dropdown changes it's value. Contains every dropdown prop passed with the updated selected value as 'value' */ (e: 'update:dropdown', updatedDropdownProps: BentoInputDropdownProps): void; /** * Emitted when the component's model changes */ (e: 'update:model-value', inputValue: BentoInputFieldValue): void; }>(); const { emitValue } = useFormFieldEmits<BentoInputFieldValue>(emit); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const hasSlot = useHasSlot(slots); const inputFieldElement = ref(null); const inputDropdownElement = ref(null); const inputFieldId = generateUid('input'); const dropdownId = generateUid('input-dropdown'); const staticValueId = generateUid('input-static-value'); const descriptionId = generateUid('input-description'); const errorId = generateUid('input-error'); const isFocused = ref(false); /** * Gets the overriden or the default data-testid attribute * @param attributes - the live attributes object */ function getDataTestId(attributes: Record<string, unknown>) { return (attributes['data-testid'] as string) ?? 'input-text-container'; } /** * Filters out all aria attributes for the root element. * @param attributes - the live attributes object */ function getRootAttributes(attributes: Record<string, unknown>) { return Object.fromEntries( Object.entries(attributes).filter(([key]) => !key.startsWith('aria-') && key !== 'data-testid') ); } // Provide DROPDOWN_SMALL_SIZE_INJECTION_KEY as true, to set the input only dropdown size variant in the BentoDropdown comp provide(DROPDOWN_SMALL_SIZE_INJECTION_KEY, true); // Renders the input with a hint if a string is provided const hint = inject<Ref<string | undefined>>(INPUT_FIELD_HINT_INJECTION_KEY, ref(undefined)); const onFocus = () => { isFocused.value = true; emit('focus'); }; const onBlur = () => { isFocused.value = false; emit('blur'); }; const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const isDropdownReadOnly = computed<BentoInputDropdownProps['readonly']>( () => isReadOnly.value || props.dropdown?.readonly ); const shouldShowStaticValue = computed( () => props.variant === BentoInputFieldVariant.STATIC_VALUE && !!slots.staticValue ); const isDefaultVariant = computed(() => props.variant === BentoInputFieldVariant.DEFAULT); const isPaymentMethodVariant = computed(() => props.variant === BentoInputFieldVariant.PAYMENT_METHOD); const shouldDisplayStaticValueAtStart = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.START ); const shouldDisplayStaticValueAtEnd = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.END ); const shouldShowDropdown = computed(() => props.variant === BentoInputFieldVariant.DROPDOWN && !!props.dropdown); const shouldDisplayDropdownAtStart = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.START ); const shouldDisplayDropdownAtEnd = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.END ); const shouldShowError = computed( () => !props.disabled && !isReadOnly.value && (!!props.errorMessage || props.error) ); const computedLabel = computed(() => (getSlotText(slots)('default') as string) || props.label); const computedDescription = computed(() => (getSlotText(slots)('description') as string) || props.description); const computedAriaOwns = computed(() => { if (shouldShowDropdown.value === true && shouldShowStaticValue.value === true) { return `${staticValueId} ${dropdownId}`; } if (shouldShowDropdown.value === true) { return `${dropdownId}`; } if (shouldShowStaticValue.value === true) { return `${staticValueId}`; } return null; }); // 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(() => attrs?.['aria-labelledby'] ? undefined : t('ariaLabelFallback') ); const computedAriaLabelDropdowFallback = computed(() => t('ariaLabelDropdownFallback', { inputLabel: computedAriaLabel.value }) ); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel } = toRefs(props); const computedAriaLabel = useAriaLabel({ ariaLabel: ariaLabelAttribute.value as string, // TODO: take this line away when the aria-label props is removed label: ariaLabel, defaultFallback: computedLabel.value || computedAriaLabelFallbackMessage, }); const computedAriaDescribedBy = computed( () => `${computedDescription.value ? descriptionId : ''} ${ shouldShowError.value && !!props.errorMessage ? errorId : '' }` ); const inputFieldConditionalClasses = computed(() => ({ [`b-input-field--${BentoInputFieldStateClass.CONDENSED}`]: props.condensed, [`b-input-field--${BentoInputFieldStateClass.DISABLED}`]: props.disabled, [`b-input-field--${BentoInputFieldStateClass.READONLY}`]: isReadOnly.value, [`b-input-field--${BentoInputFieldStateClass.ERROR}`]: shouldShowError.value, [`b-input-field--slashed-zero`]: props.slashedZero, })); const inputFieldDropdownProps = computed(() => ({ 'aria-label': computedAriaLabelDropdowFallback.value, ...props.dropdown, })); const onInput = (event: Event) => { const inputValue = (event.target as HTMLInputElement).value; if (!props.disabled && !isReadOnly.value) { emitValue(inputValue); } }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { emit('dropdown-input', selectedValue); emit('update:dropdown', { ...props?.dropdown, value: selectedValue }); }; const focusInput = () => { inputFieldElement.value.focus(); }; const focus = () => { if (props.variant === 'dropdown') { inputDropdownElement.value.focus(); } else { focusInput(); } }; // Expose method so that other components can focus on it in Vue3 defineExpose({ focusInput, focus, }); const inputFieldParentComponent = inject(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, ''); if (!Object.values(BentoInputFieldType).includes(props.type as BentoInputFieldType) && !inputFieldParentComponent) { if (props.type === 'tel') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-phone-number" instead.` ); } else if (props.type === 'password') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-password" instead.` ); } else { printDevelopmentWarning(`"bento-input-field" dropdown does not support the type '${props.type}'`); } } /** * Deprecations */ if (props.error) { deprecate( 'BentoInputField "error" property', `Use the BentoInputField "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 (hasSlot('defaultIconBefore')) { deprecate( 'BentoInputField "defaultIconBefore" slot', `Use the BentoInputField "iconBefore" slot instead to add an icon at the left side of the input field <bento-input-field> \t<template #iconBefore> \t\t<my-icon-goes-here /> \t</template> </bento-input-field>`, '2.0.0' ); } if (props.value) { deprecate( 'BentoInputField "value" property', `The use of "value" prop in "BentoInputField" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field component is used to create interactive controls for web-based * forms in order to accept data from the user. * * @example * import { BentoInputField } from '@adyen/bento-vue2' * * const inputValue = ref(''); * * export default { * components: { BentoInputField }, * tempate: ` * <bento-input-field * :model-value="inputValue" * :@update:model-value="newValue => inputValue.value = newValue" * > * } */ export default { name: 'bento-input-field', inheritAttrs: false, model: { prop: 'modelValue' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./input-field.scss" />
1
+ <template> <bento-typography class="b-input-field" v-bind="getRootAttributes($attrs)" :data-testid="getDataTestId($attrs)" :class="inputFieldConditionalClasses" el="div" variant="body" > <field-label v-if="hasSlot('default') || label" :for="inputFieldId" data-testid="input-field-label" :tooltip-text="tooltipText" :optional="optional" :required="required" :label="label" @click="focusInput" > <slot></slot> </field-label> <div class="b-input-field__input-box" data-testid="input-box" :aria-disabled="disabled" @click="focusInput"> <!-- Optional Icon for Default text variant --> <span v-if="isDefaultVariant && (hasSlot('defaultIconBefore') || hasSlot('iconBefore'))" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-before" > <slot v-if="hasSlot('iconBefore')" name="iconBefore" /> <slot v-if="hasSlot('defaultIconBefore') && !hasSlot('iconBefore')" name="defaultIconBefore" /> </span> <!-- Mandatory Icon for Payment Method variant --> <span v-if="isPaymentMethodVariant && hasSlot('paymentMethod')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__payment-method" > <slot name="paymentMethod" /> </span> <!-- Dropdown at start --> <div v-if="shouldDisplayDropdownAtStart" :id="dropdownId" class="b-input-field__dropdown b-input-field__dropdown--start" @click.stop > <bento-dropdown v-if="dropdown" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> <!-- Static Value at start --> <bento-typography v-if="shouldDisplayStaticValueAtStart" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--start" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <div class="b-input-field__input-container"> <input :id="inputFieldId" v-bind="$attrs" ref="inputFieldElement" class="b-input-field__input" :aria-label="computedAriaLabel" :aria-describedby="computedAriaDescribedBy" :aria-owns="computedAriaOwns" :aria-invalid="shouldShowError" :placeholder="placeholder" :required="required" :aria-required="required" :type="type" :value="modelValue ?? value" :disabled="disabled" :readonly="isReadOnly" @input="onInput" @change="emit('change')" @focus="onFocus" @blur="onBlur" @keydown="emit('keydown', $event)" @keyup="emit('keyup', $event)" @keydown.esc="emit('escape-pressed')" @click="emit('click', $event)" /> <!-- Hint at the end --> <bento-typography v-if="hint && isFocused && (modelValue || value)" el="span" class="b-input-field__hint" > {{ hint }} </bento-typography> </div> <span v-if="hasSlot('iconAfter')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-after" data-testid="input-text__icon-after" > <slot name="iconAfter" /> </span> <!-- Static Value at the end --> <bento-typography v-if="shouldDisplayStaticValueAtEnd" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--end" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <!-- Dropdown at end --> <div v-if="shouldDisplayDropdownAtEnd" :id="dropdownId" data-testid="input-field-dropdown-container-end" class="b-input-field__dropdown b-input-field__dropdown--end" @click.stop > <bento-dropdown v-if="dropdown" :id="dropdownId" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> </div> <error-message v-if="shouldShowError && !!errorMessage" :id="errorId" :error-message="errorMessage" class="b-input-field__error-message" /> <span v-if="hasSlot('description') || description" :id="descriptionId" class="b-input-field__description" @click="focusInput" > <bento-typography el="span" variant="body"> <slot id="description" name="description"> {{ description }} </slot> </bento-typography> </span> </bento-typography> </template> <script setup lang="ts"> import { computed, inject, provide, type Ref, ref, toRef, toRefs, useAttrs, useSlots } from 'vue'; // Components import { BentoDropdown } from '@/components/dropdown'; import { DROPDOWN_SMALL_SIZE_INJECTION_KEY } from '@/components/dropdown/dropdown.keys'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { generateUid } from '@/core/utils/ts'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { INPUT_FIELD_HINT_INJECTION_KEY, INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, } from '@/components/input-field/input-field.keys'; import { type BentoListboxSelectedValue } from '@/types/listbox'; // Composables import { useFormLayoutFieldLoading, useHasSlot } from '@/composables'; // Types import { type BentoInputDropdownProps, BentoInputFieldElementPosition, type BentoInputFieldProps, BentoInputFieldStateClass, BentoInputFieldType, type BentoInputFieldValue, BentoInputFieldVariant, } from './input-field.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const props = withDefaults(defineProps<BentoInputFieldProps>(), { ariaHidden: true, ariaLabel: undefined, condensed: false, description: '', disabled: false, dropdown: undefined, dropdownPosition: BentoInputFieldElementPosition.START, error: false, errorMessage: null, label: '', modelValue: undefined, optional: false, placeholder: '', required: false, readonly: false, slashedZero: false, staticValue: '', staticValuePosition: BentoInputFieldElementPosition.START, tooltipText: null, type: BentoInputFieldType.TEXT, value: undefined, variant: BentoInputFieldVariant.DEFAULT, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the input is clicked */ (e: 'click', event: MouseEvent); /** * Emitted when the user modifies the element's value. Unlike the 'input' event, the change event is not necessarily fired for each alteration to an element's value */ (e: 'change'): void; /** * Emitted when the Input Field's internal dropdown changes it's value */ (e: 'dropdown-input', selectedValue: BentoListboxSelectedValue): void; /** * Emitted when the "ESC" key is pressed */ (e: 'escape-pressed'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when a key is pressed and lifted up */ (e: 'keyup', event: KeyboardEvent): void; /** * Emitted when the component's model changes * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', inputValue: string): void; /** * Emitted when the Input Field's internal dropdown changes it's value. Contains every dropdown prop passed with the updated selected value as 'value' */ (e: 'update:dropdown', updatedDropdownProps: BentoInputDropdownProps): void; /** * Emitted when the component's model changes */ (e: 'update:model-value', inputValue: BentoInputFieldValue): void; }>(); const { emitValue } = useFormFieldEmits<BentoInputFieldValue>(emit); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const hasSlot = useHasSlot(slots); const inputFieldElement = ref<HTMLInputElement>(null); const inputDropdownElement = ref(null); const inputFieldId = generateUid('input'); const dropdownId = generateUid('input-dropdown'); const staticValueId = generateUid('input-static-value'); const descriptionId = generateUid('input-description'); const errorId = generateUid('input-error'); const isFocused = ref(false); /** * Gets the overriden or the default data-testid attribute * @param attributes - the live attributes object */ function getDataTestId(attributes: Record<string, unknown>) { return (attributes['data-testid'] as string) ?? 'input-text-container'; } /** * Filters out all aria attributes for the root element. * @param attributes - the live attributes object */ function getRootAttributes(attributes: Record<string, unknown>) { return Object.fromEntries( Object.entries(attributes).filter(([key]) => !key.startsWith('aria-') && key !== 'data-testid') ); } // Provide DROPDOWN_SMALL_SIZE_INJECTION_KEY as true, to set the input only dropdown size variant in the BentoDropdown comp provide(DROPDOWN_SMALL_SIZE_INJECTION_KEY, true); // Renders the input with a hint if a string is provided const hint = inject<Ref<string | undefined>>(INPUT_FIELD_HINT_INJECTION_KEY, ref(undefined)); const onFocus = () => { isFocused.value = true; emit('focus'); }; const onBlur = () => { isFocused.value = false; emit('blur'); }; const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const isDropdownReadOnly = computed<BentoInputDropdownProps['readonly']>( () => isReadOnly.value || props.dropdown?.readonly ); const shouldShowStaticValue = computed( () => props.variant === BentoInputFieldVariant.STATIC_VALUE && !!slots.staticValue ); const isDefaultVariant = computed(() => props.variant === BentoInputFieldVariant.DEFAULT); const isPaymentMethodVariant = computed(() => props.variant === BentoInputFieldVariant.PAYMENT_METHOD); const shouldDisplayStaticValueAtStart = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.START ); const shouldDisplayStaticValueAtEnd = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.END ); const shouldShowDropdown = computed(() => props.variant === BentoInputFieldVariant.DROPDOWN && !!props.dropdown); const shouldDisplayDropdownAtStart = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.START ); const shouldDisplayDropdownAtEnd = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.END ); const shouldShowError = computed( () => !props.disabled && !isReadOnly.value && (!!props.errorMessage || props.error) ); const computedLabel = computed(() => (getSlotText(slots)('default') as string) || props.label); const computedDescription = computed(() => (getSlotText(slots)('description') as string) || props.description); const computedAriaOwns = computed(() => { if (shouldShowDropdown.value === true && shouldShowStaticValue.value === true) { return `${staticValueId} ${dropdownId}`; } if (shouldShowDropdown.value === true) { return `${dropdownId}`; } if (shouldShowStaticValue.value === true) { return `${staticValueId}`; } return null; }); // 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(() => attrs?.['aria-labelledby'] ? undefined : t('ariaLabelFallback') ); const computedAriaLabelDropdowFallback = computed(() => t('ariaLabelDropdownFallback', { inputLabel: computedAriaLabel.value }) ); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel } = toRefs(props); const computedAriaLabel = useAriaLabel({ ariaLabel: ariaLabelAttribute.value as string, // TODO: take this line away when the aria-label props is removed label: ariaLabel, defaultFallback: computedLabel.value || computedAriaLabelFallbackMessage, }); const computedAriaDescribedBy = computed( () => `${computedDescription.value ? descriptionId : ''} ${ shouldShowError.value && !!props.errorMessage ? errorId : '' }` ); const inputFieldConditionalClasses = computed(() => ({ [`b-input-field--${BentoInputFieldStateClass.CONDENSED}`]: props.condensed, [`b-input-field--${BentoInputFieldStateClass.DISABLED}`]: props.disabled, [`b-input-field--${BentoInputFieldStateClass.READONLY}`]: isReadOnly.value, [`b-input-field--${BentoInputFieldStateClass.ERROR}`]: shouldShowError.value, [`b-input-field--slashed-zero`]: props.slashedZero, })); const inputFieldDropdownProps = computed(() => ({ 'aria-label': computedAriaLabelDropdowFallback.value, ...props.dropdown, })); const onInput = (event: Event) => { const inputValue = (event.target as HTMLInputElement).value; if (!props.disabled && !isReadOnly.value) { emitValue(inputValue); } }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { emit('dropdown-input', selectedValue); emit('update:dropdown', { ...props?.dropdown, value: selectedValue }); }; const focusInput = () => { inputFieldElement.value.focus(); }; const focus = () => { if (props.variant === 'dropdown') { inputDropdownElement.value.focus(); } else { focusInput(); } }; // Expose focus methods for parent components in Vue 3. defineExpose({ /** * Focuses the input field. */ focusInput, /** * Focuses the dropdown when using the dropdown variant; otherwise focuses the input field. */ focus, /** * Reference to the native input element. */ inputFieldElement, }); const inputFieldParentComponent = inject(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, ''); if (!Object.values(BentoInputFieldType).includes(props.type as BentoInputFieldType) && !inputFieldParentComponent) { if (props.type === 'tel') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-phone-number" instead.` ); } else if (props.type === 'password') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-password" instead.` ); } else { printDevelopmentWarning(`"bento-input-field" dropdown does not support the type '${props.type}'`); } } /** * Deprecations */ if (props.error) { deprecate( 'BentoInputField "error" property', `Use the BentoInputField "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 (hasSlot('defaultIconBefore')) { deprecate( 'BentoInputField "defaultIconBefore" slot', `Use the BentoInputField "iconBefore" slot instead to add an icon at the left side of the input field <bento-input-field> \t<template #iconBefore> \t\t<my-icon-goes-here /> \t</template> </bento-input-field>`, '2.0.0' ); } if (props.value) { deprecate( 'BentoInputField "value" property', `The use of "value" prop in "BentoInputField" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field component is used to create interactive controls for web-based * forms in order to accept data from the user. * * @example * import { BentoInputField } from '@adyen/bento-vue2' * * const inputValue = ref(''); * * export default { * components: { BentoInputField }, * tempate: ` * <bento-input-field * :model-value="inputValue" * :@update:model-value="newValue => inputValue.value = newValue" * > * } */ export default { name: 'bento-input-field', inheritAttrs: false, model: { prop: 'modelValue' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./input-field.scss" />