@adyen/bento-mcp 0.6.0 → 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.
- package/CHANGELOG.md +11 -0
- package/README.md +7 -6
- package/bin/add-skill.js +12 -1
- package/dist/assets/components/anchor-scroller/anchor-scroller.vue +1 -1
- package/dist/assets/components/anchor-scroller/components/anchor-scroller-list/anchor-scroller-list.types.ts +1 -1
- package/dist/assets/components/anchor-scroller/components/anchor-scroller-list/anchor-scroller-list.vue +1 -1
- package/dist/assets/components/button/button.docs.mdx +24 -0
- package/dist/assets/components/button/button.vue +1 -1
- package/dist/assets/components/button/components/button-actions/button-actions.docs.mdx +14 -1
- package/dist/assets/components/date-range-picker/date-range-picker.vue +1 -1
- package/dist/assets/components/draggable/components/draggable-handle/draggable-handle.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-options-container/dropdown-options-container.vue +1 -1
- package/dist/assets/components/dropdown/dropdown.docs.mdx +58 -0
- package/dist/assets/components/dropdown/dropdown.stories.ts +1 -1
- package/dist/assets/components/dropdown/dropdown.vue +1 -1
- package/dist/assets/components/filter-bar/components/range-filter/range-filter.types.ts +1 -0
- package/dist/assets/components/filter-bar/components/range-filter/range-filter.vue +1 -1
- package/dist/assets/components/filter-bar/filter-bar.docs.mdx +74 -1
- package/dist/assets/components/filter-bar/filter-bar.stories.ts +1 -1
- package/dist/assets/components/filter-bar/filter-bar.types.ts +1 -1
- package/dist/assets/components/filter-bar/filter-bar.vue +1 -1
- package/dist/assets/components/header/header.docs.mdx +8 -0
- package/dist/assets/components/header-with-views/header-with-views.docs.mdx +12 -0
- package/dist/assets/components/input-field/input-field.vue +1 -1
- package/dist/assets/components/internal/listbox/components/listbox-lazy-load/listbox-lazy-load.types.ts +1 -0
- package/dist/assets/components/internal/listbox/components/listbox-lazy-load/listbox-lazy-load.vue +1 -0
- 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
- 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
- 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
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/listbox-multi-select-options.vue +1 -1
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/listbox-multi-select.vue +1 -1
- package/dist/assets/components/internal/listbox/components/listbox-single-select/listbox-single-select.vue +1 -1
- package/dist/assets/components/internal/listbox/listbox.vue +1 -1
- package/dist/assets/components/table-of-contents/components/table-of-contents-list/table-of-contents-list.types.ts +1 -0
- package/dist/assets/components/table-of-contents/components/table-of-contents-list/table-of-contents-list.vue +1 -0
- package/dist/assets/components/table-of-contents/table-of-contents.docs.mdx +34 -0
- package/dist/assets/components/table-of-contents/table-of-contents.stories.ts +1 -0
- package/dist/assets/components/table-of-contents/table-of-contents.types.ts +1 -0
- package/dist/assets/components/table-of-contents/table-of-contents.vue +1 -0
- package/dist/assets/components.json +1 -0
- package/dist/assets/composables/use-numeric-input/use-numeric-input.types.ts +1 -0
- package/dist/assets/composables/use-section-navigation-keyboard/use-section-navigation-keyboard.types.ts +1 -0
- package/dist/assets/deprecations.md +136 -136
- package/dist/assets/index.ts +1 -1
- package/dist/assets/usage.json +80 -79
- package/dist/assets/utils/ts/scroll-to-section/scroll-to-section.types.ts +1 -0
- package/dist/main.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import { action } from '@storybook/addon-actions'; import { computed, onMounted, ref, watch } from 'vue'; import { isVue2 } from 'vue-demi'; import { type BentoListboxOptionItem, type BentoListboxOptions, type BentoListboxSelectedValue } from '@/types/listbox'; import { BentoDropdownEvent, BentoDropdownLazyLoadType, type BentoDropdownSearchOptions, } from '@/components/dropdown/dropdown.types'; import { BentoTag, BentoTagVariant } from '@/components/tag'; import { useCountryName, useCountryPhoneCode } from '@/components/country'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import type { Meta, StoryObj } from '@storybook/vue'; import BentoDropdown from './dropdown.vue'; import DropdownWithDescriptionExample from './__tests__/dropdown-with-description-example.vue?raw'; import DropdownWithCustomSlotsExample from './__tests__/dropdown-with-custom-slots-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 })); const DEFAULT_DESCRIPTION = 'Choose an option'; const DEFAULT_LABEL = 'Label'; const DEFAULT_PLACEHOLDER = 'Select an option'; const DEFAULT_PROPS = { label: DEFAULT_LABEL, description: DEFAULT_DESCRIPTION, placeholder: DEFAULT_PLACEHOLDER, lazyLoadType: 'none', enableValueLabelPair: false, }; const COUNTRY_OPTIONS = ['NL', 'BE', 'DE'].map(code => ({ label: `${useCountryName(code, 'en-US').value} ${useCountryPhoneCode(code).value}`, value: code, displayValue: `${code} (${useCountryPhoneCode(code).value})`, })); const showMoreOptions = () => action(BentoDropdownEvent.SHOW_MORE)(); const meta: Meta = { title: 'Dropdown', component: BentoDropdown, argTypes: { 'show-more': { name: 'show-more', table: { type: { summary: '() => void' }, }, control: { disable: true }, }, size: { table: { disable: true } }, // Slots default: { description: 'Custom slots for the items and display value', table: { category: 'Slots', type: { summary: 'VNode[]' }, }, control: { type: 'text' }, }, description: { name: 'description', description: 'If set, displays a description text for the input.', table: { category: 'props', type: { summary: 'string' }, }, control: { type: 'text' }, }, 'slot:description': { name: 'description', defaultValue: '', description: 'If set, displays a description text for the input. Overrides the `description` prop when provided.', table: { category: 'Slots', type: { summary: 'VNode[]' }, }, control: { type: 'text' }, }, 'display-value': { description: 'Custom slot for the display value when the display value is different than the items', table: { category: 'Slots', type: { summary: 'VNode[]' }, }, control: { type: 'text' }, }, innerSlot: { // Hide non-existent slot inaccurately created by storybook table: { disable: true }, }, // Values lazyLoadType: { options: Object.values(BentoDropdownLazyLoadType), mapping: BentoDropdownLazyLoadType, control: { type: 'select', }, }, error: { control: { type: 'boolean' }, table: { category: 'Deprecated' }, }, input: { description: 'Deprecated since version 2.0. Use `v-model` or `update:model-value` instead', table: { category: 'Deprecated' }, control: { disable: true }, }, value: { control: { type: 'text' }, table: { category: 'Deprecated' }, description: 'Deprecated for usage with properties since version 2.0. Use `v-model` or `model-value` property instead', }, }, }; export default meta; type Story = StoryObj<typeof BentoDropdown>; const defaultDocs = ` <template> <bento-dropdown v-model="modelValue" aria-label="Descriptive definition of what the dropdown is for" description="Description of the field" :condensed="false" :disabled="false" :empty-state="emptyState" dynamic-filtering error-message="This is an error message" :multiple="false" :items="items" :is-option-disabled="isOptionDisabled" label="Dropdown input label" :optional="!isRequired" tooltip-text="Label tooltip text" placeholder="Dropdown placeholder text" :required="isRequired" enable-value-label-pair /> </template> <script setup lang="ts"> import { BentoDropdown, BentoDropdownEmptyStateProps, BentoListboxOptionItem, BentoListboxOptions, BentoListboxSelectedValue, } from '@adyen/bento-vue2'; import { computed, ref } from 'vue'; const modelValue = ref<BentoListboxSelectedValue>({ label: 'Option 1', value: 1 }); const items: BentoListboxOptions = new Array(10) .fill(0) .map((_, index) => ({ label: \`Option \${index + 1}\`, value: index + 1 })); // "required" and "optional" are mutually exclusive const isRequired = ref(false); const emptyState = computed<BentoDropdownEmptyStateProps>(() => ({ title: 'Unable to retrieve items', action: { title: 'Refresh the page to try again', event: () => { // Refresh page logic }, }, })); // Disable individual options const isOptionDisabled = (option: BentoListboxOptionItem) => option.value === 3; </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const isOptionDisabled = (option: BentoListboxOptionItem) => option.value === 3; const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { selectedValue.value = newSelectedValue; action('update:model-value')(newSelectedValue); }; return { // Values args, selectedValue, // Methods isOptionDisabled, onUpdateModelValue, showMoreOptions, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown v-bind="args" :model-value="selectedValue" @update:model-value="onUpdateModelValue" :isOptionDisabled="isOptionDisabled" @show-more="showMoreOptions" > <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-dropdown> </div> `, }), args: { ...DEFAULT_PROPS, modelValue: DEFAULT_OPTIONS[0].value, items: DEFAULT_OPTIONS, }, parameters: storybookDocsParameter(defaultDocs), }; export const MultiSelect: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const isOptionDisabled = (option: BentoListboxOptionItem) => [3, 5, 7, 9].includes(option.value as number); const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { selectedValue.value = newSelectedValue; action('update:model-value')(newSelectedValue); }; return { // Values selectedValue, args, // Methods isOptionDisabled, onUpdateModelValue, showMoreOptions, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown v-bind="args" :model-value="selectedValue" @update:model-value="onUpdateModelValue" :isOptionDisabled="isOptionDisabled" @show-more="showMoreOptions" > <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-dropdown> </div> `, }), args: { ...DEFAULT_PROPS, description: 'Choose one or more options', modelValue: [1, 3, 100], multiple: true, items: DEFAULT_OPTIONS, }, parameters: storybookDocsParameter(` <template> <bento-dropdown v-model="modelValue" aria-label="Descriptive definition of what the dropdown is for" description="Description of the field" :condensed="false" :disabled="false" :empty-state="emptyState" dynamic-filtering error-message="This is an error message" :multiple="true" :items="items" :is-option-disabled="isOptionDisabled" label="Dropdown input label" :optional="!isRequired" tooltip-text="Label tooltip text" placeholder="Dropdown placeholder text" :required="isRequired" enable-value-label-pair @update:model-value="onUpdateModelValue" /> </template> <script setup lang="ts"> import { BentoDropdown, BentoDropdownEmptyStateProps, BentoListboxOptionItem, BentoListboxOptions, BentoListboxSelectedValue, } from '@adyen/bento-vue2'; import { computed, ref } from 'vue'; const modelValue = ref<BentoListboxSelectedValue>([ { label: 'Option 1', value: 1 } ]); const items: BentoListboxOptions = new Array(10) .fill(0) .map((_, index) => ({ label: \`Option \${index + 1}\`, value: index + 1 })); // "required" and "optional" are mutually exclusive const isRequired = ref(false); const emptyState = computed<BentoDropdownEmptyStateProps>(() => ({ title: 'Unable to retrieve items', action: { title: 'Refresh the page to try again', event: () => { // Refresh page logic }, }, })); // Disable individual options const isOptionDisabled = (option: BentoListboxOptionItem) => option.value === 3; const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { modelValue.value = newSelectedValue; }; </script> `), }; const TAG_LIST = Object.values(BentoTagVariant); const TAG_OPTIONS: BentoListboxOptions = new Array(TAG_LIST.length) .fill(0) .map((_, index) => ({ label: `Option ${index + 1}`, value: TAG_LIST[index] })); export const Slots: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown, BentoTag }, props: Object.keys(argTypes), setup(props) { const selectedValue = ref(props.value); const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { selectedValue.value = newSelectedValue; action('update:model-value')(newSelectedValue); }; return { // Values args: isVue2 ? props : _args, selectedValue, // Methods onUpdateModelValue, showMoreOptions, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown v-bind="args" :model-value="selectedValue" @update:model-value="onUpdateModelValue" @show-more="showMoreOptions" > <template #default="{ label, value }"> <bento-tag :variant="value" :label="label" /> </template> <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-dropdown> </div> `, }), args: { ...DEFAULT_PROPS, modelValue: TAG_OPTIONS[1].value, items: TAG_OPTIONS, }, parameters: storybookDocsParameter(DropdownWithCustomSlotsExample), }; export const WithDescription: Story = { ...Default, args: { ...DEFAULT_PROPS, modelValue: [1, 2, 3], multiple: true, items: DEFAULT_OPTIONS.map(item => ({ ...item, description: `Description ${item.value}`, })), }, parameters: storybookDocsParameter(DropdownWithDescriptionExample), }; export const NestedMultiSelect: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const isOptionDisabled = (option: BentoListboxOptionItem) => [4, 5].includes(option.value as number); const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { selectedValue.value = newSelectedValue; action('update:model-value')(newSelectedValue); }; return { // Values args, selectedValue, // Methods isOptionDisabled, onUpdateModelValue, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown v-bind="args" :model-value="selectedValue" @update:model-value="onUpdateModelValue" :isOptionDisabled="isOptionDisabled" > <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-dropdown> </div> `, }), args: { ...DEFAULT_PROPS, description: 'Choose one or more options', modelValue: [2, 3, 100], multiple: true, items: [ { label: 'Option 1', description: 'Uncategorized', value: 1, }, { label: 'Category 1', description: 'C1', value: 'c1', items: [ { label: 'Option 2', description: 'C1-O2', value: 2, }, { label: 'Option 3', description: 'C1-O3', value: 3, }, ], }, { label: 'Category 2', description: 'C2', value: 'c2', items: [ { label: 'Option 4', description: 'C2-O4', value: 4, }, { label: 'Option 5', description: 'C2-O5', value: 5, }, ], }, ] as BentoListboxOptions, }, }; const externalFilteringTemplate = storybookDocsParameter(` <template> <bento-dropdown v-model="selectedValue" :items="internalList" :search="search" :component-loading="isSearching" dynamic-filtering multiple has-more-items lazy-load-type="automatic" label="Dropdown with external filtering" enable-value-label-pair /> </template> <script setup lang="ts"> import { ref, computed } from 'vue'; import { BentoDropdown, BentoListboxOptions, BentoListboxSelectedValueLabelPair, BentoDropdownSearchOptions, } from '@adyen/bento-vue2'; const initialItems: BentoListboxOptions = [ { label: 'Option 1', value: 1 }, { label: 'Option 2', value: 2 }, { label: 'Option 3', value: 3 }, { label: 'Option 4', value: 4 }, { label: 'Option 5', value: 5 }, ]; const selectedValue = ref<Array<BentoListboxSelectedValueLabelPair>>([]); const internalList = ref<BentoListboxOptions>(initialItems); const isSearching = ref(false); const searchForValue = (searchString: string) => { // This is where you would typically make an API call // to fetch data based on the searchString. // For this example, we'll simulate it. if (!searchString) { internalList.value = initialItems; return; } const newItems = new Array(5).fill(0).map((_, index) => ({ label: \`\${searchString} \${index + 1}\`, value: \`\${searchString}-\${index + 1}\`, })); internalList.value = newItems; }; const search = computed<BentoDropdownSearchOptions>(() => ({ debounceTime: 500, searchEvent: async (value: string) => { isSearching.value = true; // Simulate API call delay await new Promise(resolve => setTimeout(resolve, 1000)); searchForValue(value); isSearching.value = false; }, })); </script> `); export const externalFiltering: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const internalList = ref<BentoListboxOptions>(args.items); const isSearching = ref(false); const searchForValue = (searchString: string) => { // This is where you would typically make an API call // to fetch data based on the searchString. // For this example, we'll simulate it. if (!searchString) { internalList.value = args.items; return; } const newItems = new Array(5).fill(0).map((_, index) => ({ label: `${searchString} ${index + 1}`, value: `${searchString}-${index + 1}`, })); internalList.value = newItems; }; const search = computed<BentoDropdownSearchOptions>(() => ({ debounceTime: args.search.debounceTime, searchEvent: async value => { isSearching.value = true; await new Promise(resolve => setTimeout(resolve, 1000)); searchForValue(value); action('searchEvent')(value); isSearching.value = false; }, })); const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { selectedValue.value = newSelectedValue; action('update:model-value')(newSelectedValue); }; return { args, selectedValue, internalList, isSearching, search, onUpdateModelValue, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown v-bind="args" :model-value="selectedValue" :items="internalList" :search="search" :component-loading="isSearching" @update:model-value="onUpdateModelValue" > <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-dropdown> </div> `, }), args: { ...DEFAULT_PROPS, items: DEFAULT_OPTIONS, label: 'Dropdown with external filtering', description: 'Type to simulate fetching new items.', dynamicFiltering: true, multiple: true, modelValue: [], search: { debounceTime: 500, }, }, parameters: externalFilteringTemplate, }; const lazyLoadingTemplate = storybookDocsParameter(` <template> <bento-dropdown v-model="selectedValue" :items="listboxItems" :loading="isLoading" :has-more-items="hasMoreItems" :lazy-load-type="lazyLoadType" label="Lazy Loading Dropdown" multiple enable-value-label-pair @show-more="onLazyLoad" /> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue'; import { BentoDropdown, BentoListboxOptions, BentoListboxSelectedValueLabelPair, BentoDropdownLazyLoadType, } from '@adyen/bento-vue2'; // --- Mock API for demonstration --- const ALL_ITEMS = Array.from({ length: 100 }, (_, i) => ({ label: \`Item \${i + 1}\`, value: i + 1, })); const PAGE_SIZE = 20; const fetchItems = (page: number) => { console.log(\`Fetching page: \${page}\`); return new Promise<{ items: BentoListboxOptions; hasMore: boolean }>(resolve => { setTimeout(() => { const start = (page - 1) * PAGE_SIZE; const end = start + PAGE_SIZE; const pageItems = ALL_ITEMS.slice(start, end); resolve({ items: pageItems, hasMore: end < ALL_ITEMS.length, }); }, 500); }); }; // --- End Mock API --- const selectedValue = ref<Array<BentoListboxSelectedValueLabelPair>>([]); const listboxItems = ref<BentoListboxOptions>([]); const isLoading = ref(false); const hasMoreItems = ref(true); const currentPage = ref(1); const lazyLoadType = ref<BentoDropdownLazyLoadType>('automatic'); // or 'button' const onLazyLoad = async () => { if (isLoading.value || !hasMoreItems.value) { return; } currentPage.value++; isLoading.value = true; const { items, hasMore } = await fetchItems(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(async () => { isLoading.value = true; const { items, hasMore } = await fetchItems(currentPage.value); listboxItems.value = items; hasMoreItems.value = hasMore; isLoading.value = false; }); </script> `); export const LazyLoading: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref<BentoListboxSelectedValue>([]); const listboxItems = ref<BentoListboxOptions>([]); const isLoading = ref(false); const hasMoreItems = ref(true); const currentPage = ref(1); const allItems = Array.from({ length: 100 }, (_, i) => ({ label: `Item ${i + 1}`, value: i + 1, })); const pageSize = 20; const fetchItems = (page: number) => { return new Promise<{ items: BentoListboxOptions; hasMore: boolean }>(resolve => { setTimeout(() => { const start = (page - 1) * pageSize; const end = start + pageSize; const pageItems = allItems.slice(start, end); resolve({ items: pageItems, hasMore: end < allItems.length, }); }, 500); }); }; const onLazyLoad = async () => { if (isLoading.value || !hasMoreItems.value) { return; } currentPage.value++; isLoading.value = true; const { items, hasMore } = await fetchItems(currentPage.value); listboxItems.value = [...listboxItems.value, ...items]; hasMoreItems.value = hasMore; isLoading.value = false; action('show-more')(); }; const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { selectedValue.value = newSelectedValue; action('update:model-value')(newSelectedValue); }; const loadInitial = async () => { currentPage.value = 1; isLoading.value = true; const { items, hasMore } = await fetchItems(currentPage.value); listboxItems.value = items; hasMoreItems.value = hasMore; isLoading.value = false; }; watch(() => args.lazyLoadType, loadInitial, { immediate: true }); return { args, selectedValue, listboxItems, isLoading, hasMoreItems, onLazyLoad, onUpdateModelValue, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown v-bind="args" :items="listboxItems" :model-value="selectedValue" :loading="isLoading" :has-more-items="hasMoreItems" @update:model-value="onUpdateModelValue" @show-more="onLazyLoad" > <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-dropdown> </div> `, }), args: { ...DEFAULT_PROPS, label: 'Lazy Loading', description: 'Scroll down or click the button to load more items.', multiple: true, lazyLoadType: 'automatic', }, parameters: lazyLoadingTemplate, }; const externalFilteringAndLazyLoadingTemplate = storybookDocsParameter(` <template> <bento-dropdown v-model="selectedValue" :items="listboxItems" :component-loading="isComponentLoading" :loading="isLazyLoading" :has-more-items="hasMoreItems" :search="search" :lazy-load-type="lazyLoadType" label="External Filtering & Lazy Loading" dynamic-filtering multiple enable-value-label-pair @show-more="onLazyLoad" /> </template> <script setup lang="ts"> import { ref, computed, onMounted } from 'vue'; import { BentoDropdown, BentoListboxOptions, BentoListboxSelectedValueLabelPair, BentoDropdownSearchOptions, BentoDropdownLazyLoadType, } from '@adyen/bento-vue2'; // --- Mock API for demonstration --- const ALL_ITEMS = Array.from({ length: 1000 }, (_, i) => ({ label: \`Item \${i + 1}\`, value: i + 1, })); const PAGE_SIZE = 20; const fetchItems = (searchTerm: string, page: number) => { 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) * PAGE_SIZE; const end = start + PAGE_SIZE; const pageItems = filtered.slice(start, end); resolve({ items: pageItems, hasMore: end < filtered.length, }); }, 500); }); }; // --- End Mock API --- const selectedValue = ref<Array<BentoListboxSelectedValueLabelPair>>([]); const listboxItems = ref<BentoListboxOptions>([]); const isComponentLoading = ref(false); const isLazyLoading = ref(false); const hasMoreItems = ref(true); const currentPage = ref(1); const currentSearchTerm = ref(''); const lazyLoadType = ref<BentoDropdownLazyLoadType>('automatic'); const onSearch = async (searchTerm: string) => { currentSearchTerm.value = searchTerm; currentPage.value = 1; isComponentLoading.value = true; const { items, hasMore } = await fetchItems(searchTerm, currentPage.value); listboxItems.value = items; hasMoreItems.value = hasMore; isComponentLoading.value = false; }; const onLazyLoad = async () => { if (isLazyLoading.value || !hasMoreItems.value) { return; } currentPage.value++; isLazyLoading.value = true; const { items, hasMore } = await fetchItems(currentSearchTerm.value, currentPage.value); listboxItems.value = [...listboxItems.value, ...items]; hasMoreItems.value = hasMore; isLazyLoading.value = false; }; const search = computed<BentoDropdownSearchOptions>(() => ({ searchEvent: onSearch, })); onMounted(() => { onSearch(''); }); </script> `); export const ExternalFilteringAndLazyLoading: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref<BentoListboxSelectedValue>([{ label: 'Item 2000', value: 2000 }]); const listboxItems = ref<BentoListboxOptions>([]); const isComponentLoading = ref(false); const isLazyLoading = ref(false); const hasMoreItems = ref(true); const currentPage = ref(1); const currentSearchTerm = ref(''); const allItems = Array.from({ length: 1000 }, (_, i) => ({ label: `Item ${i + 1}`, value: i + 1, })); const pageSize = 20; const fetchItems = (searchTerm: string, page: number) => { return new Promise<{ items: BentoListboxOptions; hasMore: boolean }>(resolve => { setTimeout(() => { const filtered = allItems.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); }); }; const onSearch = async (searchTerm: string) => { action('searchEvent')(searchTerm); currentSearchTerm.value = searchTerm; currentPage.value = 1; isComponentLoading.value = true; const { items, hasMore } = await fetchItems(searchTerm, currentPage.value); listboxItems.value = items; hasMoreItems.value = hasMore; isComponentLoading.value = false; }; const onLazyLoad = async () => { if (isLazyLoading.value || !hasMoreItems.value) { return; } currentPage.value++; isLazyLoading.value = true; const { items, hasMore } = await fetchItems(currentSearchTerm.value, currentPage.value); listboxItems.value = [...listboxItems.value, ...items]; hasMoreItems.value = hasMore; isLazyLoading.value = false; action('show-more')(); }; const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { selectedValue.value = newSelectedValue; action('update:model-value')(newSelectedValue); }; const search = computed<BentoDropdownSearchOptions>(() => ({ searchEvent: onSearch, debounceTime: args.search?.debounceTime, })); onMounted(() => { onSearch(''); }); return { args, selectedValue, listboxItems, isComponentLoading, isLazyLoading, hasMoreItems, search, onLazyLoad, onUpdateModelValue, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown v-bind="args" :items="listboxItems" :model-value="selectedValue" :component-loading="isComponentLoading" :loading="isLazyLoading" :has-more-items="hasMoreItems" :search="search" @update:model-value="onUpdateModelValue" @show-more="onLazyLoad" > <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-dropdown> </div> `, }), args: { ...DEFAULT_PROPS, label: 'External Filtering & Lazy Loading', description: 'Search for items and scroll to load more results.', dynamicFiltering: true, multiple: true, lazyLoadType: 'automatic', search: { debounceTime: 500, }, }, parameters: externalFilteringAndLazyLoadingTemplate, }; export const StaticCategories: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown }, props: Object.keys(argTypes), setup(props) { const selectedValue = ref(props.value); const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { selectedValue.value = newSelectedValue; action('update:model-value')(newSelectedValue); }; return { // Values selectedValue, args: isVue2 ? props : _args, // Methods onUpdateModelValue, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown v-bind="args" :model-value="selectedValue" @update:model-value="onUpdateModelValue" > <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-dropdown> </div> `, }), args: { ...DEFAULT_PROPS, description: 'Select an option from a category', modelValue: '', multiple: false, staticCategories: true, items: [ { label: 'All animals', value: 'all', }, { label: 'Carnivores', value: 'carnivores', items: [ { label: 'T-Rex', value: 'trex', }, { label: 'Velociraptor', value: 'velociraptor', }, ], }, { label: 'Herbivores', value: 'herbivores', items: [ { label: 'Iguanodon', value: 'iguanodon', }, { label: 'Barney', value: 'barney', }, ], }, { label: 'Aquatic', value: 'aquatic', items: [ { label: 'Mosasaurus', value: 'mosasaurus', }, { label: 'Nessie', value: 'nessie', }, ], }, ] as BentoListboxOptions, }, }; export const VirtualScrolling: Story = { ...Default, args: { ...Default.args, items: new Array(1000).fill(0).map((_, index) => ({ label: `Option ${index + 1}`, value: index + 1 })), virtualScroll: true, }, }; export const CustomDisplayValue: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { selectedValue.value = newSelectedValue; action('update:model-value')(newSelectedValue); }; return { // Values args, selectedValue, // Methods onUpdateModelValue, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown v-bind="args" :model-value="selectedValue" @update:model-value="onUpdateModelValue" > <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-dropdown> </div> `, }), args: { ...DEFAULT_PROPS, modelValue: COUNTRY_OPTIONS[0].value, items: COUNTRY_OPTIONS, }, parameters: storybookDocsParameter(` <bento-dropdown :items="[ {label: "Netherlands +31", displayValue: "NL +31", value: "NL"}, {label: "Belgium +32", displayValue: "BE +32", value: "BE"}, {label: "Germany +49", displayValue: "DE +49", value: "DE"}, ]" :model-value="selectedValue" @update:model-value="onUpdateModelValue" /> `), };
|
|
1
|
+
import { action } from '@storybook/addon-actions'; import { computed, onMounted, ref, watch } from 'vue'; import { isVue2 } from 'vue-demi'; import { type BentoListboxOptionItem, type BentoListboxOptions, type BentoListboxSelectedValue } from '@/types/listbox'; import { BentoDropdownEvent, BentoDropdownLazyLoadType, type BentoDropdownSearchOptions, } from '@/components/dropdown/dropdown.types'; import { BentoTag, BentoTagVariant } from '@/components/tag'; import { useCountryName, useCountryPhoneCode } from '@/components/country'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import type { Meta, StoryObj } from '@storybook/vue'; import BentoDropdown from './dropdown.vue'; import DropdownWithDescriptionExample from './__tests__/dropdown-with-description-example.vue?raw'; import DropdownWithCustomSlotsExample from './__tests__/dropdown-with-custom-slots-example.vue?raw'; import DropdownNestedMultiSelectWithLazyLoadExample from './__tests__/dropdown-nested-multi-select-with-lazy-load-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 })); const DEFAULT_DESCRIPTION = 'Choose an option'; const DEFAULT_LABEL = 'Label'; const DEFAULT_PLACEHOLDER = 'Select an option'; const DEFAULT_PROPS = { label: DEFAULT_LABEL, description: DEFAULT_DESCRIPTION, placeholder: DEFAULT_PLACEHOLDER, lazyLoadType: 'none', enableValueLabelPair: false, }; const COUNTRY_OPTIONS = ['NL', 'BE', 'DE'].map(code => ({ label: `${useCountryName(code, 'en-US').value} ${useCountryPhoneCode(code).value}`, value: code, displayValue: `${code} (${useCountryPhoneCode(code).value})`, })); const showMoreOptions = () => action(BentoDropdownEvent.SHOW_MORE)(); const meta: Meta = { title: 'Dropdown', component: BentoDropdown, argTypes: { 'show-more': { name: 'show-more', table: { type: { summary: '() => void' }, }, control: { disable: true }, }, size: { table: { disable: true } }, // Slots default: { description: 'Custom slots for the items and display value', table: { category: 'Slots', type: { summary: 'VNode[]' }, }, control: { type: 'text' }, }, description: { name: 'description', description: 'If set, displays a description text for the input.', table: { category: 'props', type: { summary: 'string' }, }, control: { type: 'text' }, }, 'slot:description': { name: 'description', defaultValue: '', description: 'If set, displays a description text for the input. Overrides the `description` prop when provided.', table: { category: 'Slots', type: { summary: 'VNode[]' }, }, control: { type: 'text' }, }, 'display-value': { description: 'Custom slot for the display value when the display value is different than the items', table: { category: 'Slots', type: { summary: 'VNode[]' }, }, control: { type: 'text' }, }, innerSlot: { // Hide non-existent slot inaccurately created by storybook table: { disable: true }, }, // Values lazyLoadType: { options: Object.values(BentoDropdownLazyLoadType), mapping: BentoDropdownLazyLoadType, control: { type: 'select', }, }, error: { control: { type: 'boolean' }, table: { category: 'Deprecated' }, }, input: { description: 'Deprecated since version 2.0. Use `v-model` or `update:model-value` instead', table: { category: 'Deprecated' }, control: { disable: true }, }, value: { control: { type: 'text' }, table: { category: 'Deprecated' }, description: 'Deprecated for usage with properties since version 2.0. Use `v-model` or `model-value` property instead', }, }, }; export default meta; type Story = StoryObj<typeof BentoDropdown>; const defaultDocs = ` <template> <bento-dropdown v-model="modelValue" aria-label="Descriptive definition of what the dropdown is for" description="Description of the field" :condensed="false" :disabled="false" :empty-state="emptyState" dynamic-filtering error-message="This is an error message" :multiple="false" :items="items" :is-option-disabled="isOptionDisabled" label="Dropdown input label" :optional="!isRequired" tooltip-text="Label tooltip text" placeholder="Dropdown placeholder text" :required="isRequired" enable-value-label-pair /> </template> <script setup lang="ts"> import { BentoDropdown, BentoDropdownEmptyStateProps, BentoListboxOptionItem, BentoListboxOptions, BentoListboxSelectedValue, } from '@adyen/bento-vue2'; import { computed, ref } from 'vue'; const modelValue = ref<BentoListboxSelectedValue>({ label: 'Option 1', value: 1 }); const items: BentoListboxOptions = new Array(10) .fill(0) .map((_, index) => ({ label: \`Option \${index + 1}\`, value: index + 1 })); // "required" and "optional" are mutually exclusive const isRequired = ref(false); const emptyState = computed<BentoDropdownEmptyStateProps>(() => ({ title: 'Unable to retrieve items', action: { title: 'Refresh the page to try again', event: () => { // Refresh page logic }, }, })); // Disable individual options const isOptionDisabled = (option: BentoListboxOptionItem) => option.value === 3; </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const isOptionDisabled = (option: BentoListboxOptionItem) => option.value === 3; const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { selectedValue.value = newSelectedValue; action('update:model-value')(newSelectedValue); }; return { // Values args, selectedValue, // Methods isOptionDisabled, onUpdateModelValue, showMoreOptions, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown v-bind="args" :model-value="selectedValue" @update:model-value="onUpdateModelValue" :isOptionDisabled="isOptionDisabled" @show-more="showMoreOptions" > <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-dropdown> </div> `, }), args: { ...DEFAULT_PROPS, modelValue: DEFAULT_OPTIONS[0].value, items: DEFAULT_OPTIONS, }, parameters: storybookDocsParameter(defaultDocs), }; export const MultiSelect: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const isOptionDisabled = (option: BentoListboxOptionItem) => [3, 5, 7, 9].includes(option.value as number); const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { selectedValue.value = newSelectedValue; action('update:model-value')(newSelectedValue); }; return { // Values selectedValue, args, // Methods isOptionDisabled, onUpdateModelValue, showMoreOptions, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown v-bind="args" :model-value="selectedValue" @update:model-value="onUpdateModelValue" :isOptionDisabled="isOptionDisabled" @show-more="showMoreOptions" > <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-dropdown> </div> `, }), args: { ...DEFAULT_PROPS, description: 'Choose one or more options', modelValue: [1, 3, 100], multiple: true, items: DEFAULT_OPTIONS, }, parameters: storybookDocsParameter(` <template> <bento-dropdown v-model="modelValue" aria-label="Descriptive definition of what the dropdown is for" description="Description of the field" :condensed="false" :disabled="false" :empty-state="emptyState" dynamic-filtering error-message="This is an error message" :multiple="true" :items="items" :is-option-disabled="isOptionDisabled" label="Dropdown input label" :optional="!isRequired" tooltip-text="Label tooltip text" placeholder="Dropdown placeholder text" :required="isRequired" enable-value-label-pair @update:model-value="onUpdateModelValue" /> </template> <script setup lang="ts"> import { BentoDropdown, BentoDropdownEmptyStateProps, BentoListboxOptionItem, BentoListboxOptions, BentoListboxSelectedValue, } from '@adyen/bento-vue2'; import { computed, ref } from 'vue'; const modelValue = ref<BentoListboxSelectedValue>([ { label: 'Option 1', value: 1 } ]); const items: BentoListboxOptions = new Array(10) .fill(0) .map((_, index) => ({ label: \`Option \${index + 1}\`, value: index + 1 })); // "required" and "optional" are mutually exclusive const isRequired = ref(false); const emptyState = computed<BentoDropdownEmptyStateProps>(() => ({ title: 'Unable to retrieve items', action: { title: 'Refresh the page to try again', event: () => { // Refresh page logic }, }, })); // Disable individual options const isOptionDisabled = (option: BentoListboxOptionItem) => option.value === 3; const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { modelValue.value = newSelectedValue; }; </script> `), }; const TAG_LIST = Object.values(BentoTagVariant); const TAG_OPTIONS: BentoListboxOptions = new Array(TAG_LIST.length) .fill(0) .map((_, index) => ({ label: `Option ${index + 1}`, value: TAG_LIST[index] })); export const Slots: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown, BentoTag }, props: Object.keys(argTypes), setup(props) { const selectedValue = ref(props.value); const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { selectedValue.value = newSelectedValue; action('update:model-value')(newSelectedValue); }; return { // Values args: isVue2 ? props : _args, selectedValue, // Methods onUpdateModelValue, showMoreOptions, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown v-bind="args" :model-value="selectedValue" @update:model-value="onUpdateModelValue" @show-more="showMoreOptions" > <template #default="{ label, value }"> <bento-tag :variant="value" :label="label" /> </template> <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-dropdown> </div> `, }), args: { ...DEFAULT_PROPS, modelValue: TAG_OPTIONS[1].value, items: TAG_OPTIONS, }, parameters: storybookDocsParameter(DropdownWithCustomSlotsExample), }; export const WithDescription: Story = { ...Default, args: { ...DEFAULT_PROPS, modelValue: [1, 2, 3], multiple: true, items: DEFAULT_OPTIONS.map(item => ({ ...item, description: `Description ${item.value}`, })), }, parameters: storybookDocsParameter(DropdownWithDescriptionExample), }; export const NestedMultiSelect: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const isOptionDisabled = (option: BentoListboxOptionItem) => [4, 5].includes(option.value as number); const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { selectedValue.value = newSelectedValue; action('update:model-value')(newSelectedValue); }; return { // Values args, selectedValue, // Methods isOptionDisabled, onUpdateModelValue, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown v-bind="args" :model-value="selectedValue" @update:model-value="onUpdateModelValue" :isOptionDisabled="isOptionDisabled" > <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-dropdown> </div> `, }), args: { ...DEFAULT_PROPS, description: 'Choose one or more options', modelValue: [2, 3, 100], multiple: true, items: [ { label: 'Option 1', description: 'Uncategorized', value: 1, }, { label: 'Category 1', description: 'C1', value: 'c1', items: [ { label: 'Option 2', description: 'C1-O2', value: 2, }, { label: 'Option 3', description: 'C1-O3', value: 3, }, ], }, { label: 'Category 2', description: 'C2', value: 'c2', items: [ { label: 'Option 4', description: 'C2-O4', value: 4, }, { label: 'Option 5', description: 'C2-O5', value: 5, }, ], }, ] as BentoListboxOptions, }, }; export const NestedMultiSelectWithLazyLoad: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const lazyLoadEvent = ({ value, index }) => { action('Fetching data for')(`Option ${value}. Index: ${index}`); return new Promise<void>((resolve, reject) => { setTimeout(() => { const shouldFetchFail = () => Math.random() < 0.5; if (shouldFetchFail()) { action('Failed fetching data')(`Option ${value}. Index: ${index}`); return reject(new Error('Failed to fetch data')); } const fetchedData = [ { label: 'Option 2', description: 'C1-O2', value: 2, }, { label: 'Option 3', description: 'C1-O3', value: 3, }, ] satisfies BentoListboxOptions; items.value[index].lazyLoad.hasMoreItems = false; items.value[index].items = [...items.value[index].items, ...fetchedData]; return resolve(); }, 3000); }); }; const items = ref<BentoListboxOptions>(); watch( () => args.items, () => { items.value = args.items; items.value[1].lazyLoad.lazyLoadEvent = lazyLoadEvent; }, { immediate: true } ); const isOptionDisabled = (option: BentoListboxOptionItem) => [4, 5].includes(option.value as number); const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { selectedValue.value = newSelectedValue; action('update:model-value')(newSelectedValue); }; return { // Values args, items, selectedValue, // Methods isOptionDisabled, onUpdateModelValue, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown v-bind="args" :items="items" :model-value="selectedValue" @update:model-value="onUpdateModelValue" :isOptionDisabled="isOptionDisabled" > <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-dropdown> </div> `, }), args: { ...DEFAULT_PROPS, description: 'Choose one or more options', modelValue: [ { value: 'c1', label: 'Category 1' }, { value: 100, label: 'Option 100' }, ], enableValueLabelPair: true, multiple: true, items: [ { label: 'Option 1', description: 'Uncategorized', value: 1, }, { label: 'Category 1', description: 'C1', value: 'c1', lazyLoad: { hasMoreItems: true, lazyLoadEvent: () => { // Lazy load your nested elements here }, }, items: [ // Lazy Loaded options ], }, { label: 'Category 2', description: 'C2', value: 'c2', items: [ { label: 'Option 4', description: 'C2-O4', value: 4, }, { label: 'Option 5', description: 'C2-O5', value: 5, }, ], }, ] as BentoListboxOptions, }, parameters: storybookDocsParameter(DropdownNestedMultiSelectWithLazyLoadExample), }; const externalFilteringTemplate = storybookDocsParameter(` <template> <bento-dropdown v-model="selectedValue" :items="internalList" :search="search" :component-loading="isSearching" dynamic-filtering multiple has-more-items lazy-load-type="automatic" label="Dropdown with external filtering" enable-value-label-pair /> </template> <script setup lang="ts"> import { ref, computed } from 'vue'; import { BentoDropdown, BentoListboxOptions, BentoListboxSelectedValueLabelPair, BentoDropdownSearchOptions, } from '@adyen/bento-vue2'; const initialItems: BentoListboxOptions = [ { label: 'Option 1', value: 1 }, { label: 'Option 2', value: 2 }, { label: 'Option 3', value: 3 }, { label: 'Option 4', value: 4 }, { label: 'Option 5', value: 5 }, ]; const selectedValue = ref<Array<BentoListboxSelectedValueLabelPair>>([]); const internalList = ref<BentoListboxOptions>(initialItems); const isSearching = ref(false); const searchForValue = (searchString: string) => { // This is where you would typically make an API call // to fetch data based on the searchString. // For this example, we'll simulate it. if (!searchString) { internalList.value = initialItems; return; } const newItems = new Array(5).fill(0).map((_, index) => ({ label: \`\${searchString} \${index + 1}\`, value: \`\${searchString}-\${index + 1}\`, })); internalList.value = newItems; }; const search = computed<BentoDropdownSearchOptions>(() => ({ debounceTime: 500, searchEvent: async (value: string) => { isSearching.value = true; // Simulate API call delay await new Promise(resolve => setTimeout(resolve, 1000)); searchForValue(value); isSearching.value = false; }, })); </script> `); export const externalFiltering: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const internalList = ref<BentoListboxOptions>(args.items); const isSearching = ref(false); const searchForValue = (searchString: string) => { // This is where you would typically make an API call // to fetch data based on the searchString. // For this example, we'll simulate it. if (!searchString) { internalList.value = args.items; return; } const newItems = new Array(5).fill(0).map((_, index) => ({ label: `${searchString} ${index + 1}`, value: `${searchString}-${index + 1}`, })); internalList.value = newItems; }; const search = computed<BentoDropdownSearchOptions>(() => ({ debounceTime: args.search.debounceTime, searchEvent: async value => { isSearching.value = true; await new Promise(resolve => setTimeout(resolve, 1000)); searchForValue(value); action('searchEvent')(value); isSearching.value = false; }, })); const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { selectedValue.value = newSelectedValue; action('update:model-value')(newSelectedValue); }; return { args, selectedValue, internalList, isSearching, search, onUpdateModelValue, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown v-bind="args" :model-value="selectedValue" :items="internalList" :search="search" :component-loading="isSearching" @update:model-value="onUpdateModelValue" > <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-dropdown> </div> `, }), args: { ...DEFAULT_PROPS, items: DEFAULT_OPTIONS, label: 'Dropdown with external filtering', description: 'Type to simulate fetching new items.', dynamicFiltering: true, multiple: true, modelValue: [], search: { debounceTime: 500, }, }, parameters: externalFilteringTemplate, }; const lazyLoadingTemplate = storybookDocsParameter(` <template> <bento-dropdown v-model="selectedValue" :items="listboxItems" :loading="isLoading" :has-more-items="hasMoreItems" :lazy-load-type="lazyLoadType" label="Lazy Loading Dropdown" multiple enable-value-label-pair @show-more="onLazyLoad" /> </template> <script setup lang="ts"> import { ref, onMounted } from 'vue'; import { BentoDropdown, BentoListboxOptions, BentoListboxSelectedValueLabelPair, BentoDropdownLazyLoadType, } from '@adyen/bento-vue2'; // --- Mock API for demonstration --- const ALL_ITEMS = Array.from({ length: 100 }, (_, i) => ({ label: \`Item \${i + 1}\`, value: i + 1, })); const PAGE_SIZE = 20; const fetchItems = (page: number) => { console.log(\`Fetching page: \${page}\`); return new Promise<{ items: BentoListboxOptions; hasMore: boolean }>(resolve => { setTimeout(() => { const start = (page - 1) * PAGE_SIZE; const end = start + PAGE_SIZE; const pageItems = ALL_ITEMS.slice(start, end); resolve({ items: pageItems, hasMore: end < ALL_ITEMS.length, }); }, 500); }); }; // --- End Mock API --- const selectedValue = ref<Array<BentoListboxSelectedValueLabelPair>>([]); const listboxItems = ref<BentoListboxOptions>([]); const isLoading = ref(false); const hasMoreItems = ref(true); const currentPage = ref(1); const lazyLoadType = ref<BentoDropdownLazyLoadType>('automatic'); // or 'button' const onLazyLoad = async () => { if (isLoading.value || !hasMoreItems.value) { return; } currentPage.value++; isLoading.value = true; const { items, hasMore } = await fetchItems(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(async () => { isLoading.value = true; const { items, hasMore } = await fetchItems(currentPage.value); listboxItems.value = items; hasMoreItems.value = hasMore; isLoading.value = false; }); </script> `); export const LazyLoading: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref<BentoListboxSelectedValue>([]); const listboxItems = ref<BentoListboxOptions>([]); const isLoading = ref(false); const hasMoreItems = ref(true); const currentPage = ref(1); const allItems = Array.from({ length: 100 }, (_, i) => ({ label: `Item ${i + 1}`, value: i + 1, })); const pageSize = 20; const fetchItems = (page: number) => { return new Promise<{ items: BentoListboxOptions; hasMore: boolean }>(resolve => { setTimeout(() => { const start = (page - 1) * pageSize; const end = start + pageSize; const pageItems = allItems.slice(start, end); resolve({ items: pageItems, hasMore: end < allItems.length, }); }, 500); }); }; const onLazyLoad = async () => { if (isLoading.value || !hasMoreItems.value) { return; } currentPage.value++; isLoading.value = true; const { items, hasMore } = await fetchItems(currentPage.value); listboxItems.value = [...listboxItems.value, ...items]; hasMoreItems.value = hasMore; isLoading.value = false; action('show-more')(); }; const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { selectedValue.value = newSelectedValue; action('update:model-value')(newSelectedValue); }; const loadInitial = async () => { currentPage.value = 1; isLoading.value = true; const { items, hasMore } = await fetchItems(currentPage.value); listboxItems.value = items; hasMoreItems.value = hasMore; isLoading.value = false; }; watch(() => args.lazyLoadType, loadInitial, { immediate: true }); return { args, selectedValue, listboxItems, isLoading, hasMoreItems, onLazyLoad, onUpdateModelValue, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown v-bind="args" :items="listboxItems" :model-value="selectedValue" :loading="isLoading" :has-more-items="hasMoreItems" @update:model-value="onUpdateModelValue" @show-more="onLazyLoad" > <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-dropdown> </div> `, }), args: { ...DEFAULT_PROPS, label: 'Lazy Loading', description: 'Scroll down or click the button to load more items.', multiple: true, lazyLoadType: 'automatic', }, parameters: lazyLoadingTemplate, }; const externalFilteringAndLazyLoadingTemplate = storybookDocsParameter(` <template> <bento-dropdown v-model="selectedValue" :items="listboxItems" :component-loading="isComponentLoading" :loading="isLazyLoading" :has-more-items="hasMoreItems" :search="search" :lazy-load-type="lazyLoadType" label="External Filtering & Lazy Loading" dynamic-filtering multiple enable-value-label-pair @show-more="onLazyLoad" /> </template> <script setup lang="ts"> import { ref, computed, onMounted } from 'vue'; import { BentoDropdown, BentoListboxOptions, BentoListboxSelectedValueLabelPair, BentoDropdownSearchOptions, BentoDropdownLazyLoadType, } from '@adyen/bento-vue2'; // --- Mock API for demonstration --- const ALL_ITEMS = Array.from({ length: 1000 }, (_, i) => ({ label: \`Item \${i + 1}\`, value: i + 1, })); const PAGE_SIZE = 20; const fetchItems = (searchTerm: string, page: number) => { 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) * PAGE_SIZE; const end = start + PAGE_SIZE; const pageItems = filtered.slice(start, end); resolve({ items: pageItems, hasMore: end < filtered.length, }); }, 500); }); }; // --- End Mock API --- const selectedValue = ref<Array<BentoListboxSelectedValueLabelPair>>([]); const listboxItems = ref<BentoListboxOptions>([]); const isComponentLoading = ref(false); const isLazyLoading = ref(false); const hasMoreItems = ref(true); const currentPage = ref(1); const currentSearchTerm = ref(''); const lazyLoadType = ref<BentoDropdownLazyLoadType>('automatic'); const onSearch = async (searchTerm: string) => { currentSearchTerm.value = searchTerm; currentPage.value = 1; isComponentLoading.value = true; const { items, hasMore } = await fetchItems(searchTerm, currentPage.value); listboxItems.value = items; hasMoreItems.value = hasMore; isComponentLoading.value = false; }; const onLazyLoad = async () => { if (isLazyLoading.value || !hasMoreItems.value) { return; } currentPage.value++; isLazyLoading.value = true; const { items, hasMore } = await fetchItems(currentSearchTerm.value, currentPage.value); listboxItems.value = [...listboxItems.value, ...items]; hasMoreItems.value = hasMore; isLazyLoading.value = false; }; const search = computed<BentoDropdownSearchOptions>(() => ({ searchEvent: onSearch, })); onMounted(() => { onSearch(''); }); </script> `); export const ExternalFilteringAndLazyLoading: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref<BentoListboxSelectedValue>([{ label: 'Item 2000', value: 2000 }]); const listboxItems = ref<BentoListboxOptions>([]); const isComponentLoading = ref(false); const isLazyLoading = ref(false); const hasMoreItems = ref(true); const currentPage = ref(1); const currentSearchTerm = ref(''); const allItems = Array.from({ length: 1000 }, (_, i) => ({ label: `Item ${i + 1}`, value: i + 1, })); const pageSize = 20; const fetchItems = (searchTerm: string, page: number) => { return new Promise<{ items: BentoListboxOptions; hasMore: boolean }>(resolve => { setTimeout(() => { const filtered = allItems.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); }); }; const onSearch = async (searchTerm: string) => { action('searchEvent')(searchTerm); currentSearchTerm.value = searchTerm; currentPage.value = 1; isComponentLoading.value = true; const { items, hasMore } = await fetchItems(searchTerm, currentPage.value); listboxItems.value = items; hasMoreItems.value = hasMore; isComponentLoading.value = false; }; const onLazyLoad = async () => { if (isLazyLoading.value || !hasMoreItems.value) { return; } currentPage.value++; isLazyLoading.value = true; const { items, hasMore } = await fetchItems(currentSearchTerm.value, currentPage.value); listboxItems.value = [...listboxItems.value, ...items]; hasMoreItems.value = hasMore; isLazyLoading.value = false; action('show-more')(); }; const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { selectedValue.value = newSelectedValue; action('update:model-value')(newSelectedValue); }; const search = computed<BentoDropdownSearchOptions>(() => ({ searchEvent: onSearch, debounceTime: args.search?.debounceTime, })); onMounted(() => { onSearch(''); }); return { args, selectedValue, listboxItems, isComponentLoading, isLazyLoading, hasMoreItems, search, onLazyLoad, onUpdateModelValue, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown v-bind="args" :items="listboxItems" :model-value="selectedValue" :component-loading="isComponentLoading" :loading="isLazyLoading" :has-more-items="hasMoreItems" :search="search" @update:model-value="onUpdateModelValue" @show-more="onLazyLoad" > <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-dropdown> </div> `, }), args: { ...DEFAULT_PROPS, label: 'External Filtering & Lazy Loading', description: 'Search for items and scroll to load more results.', dynamicFiltering: true, multiple: true, lazyLoadType: 'automatic', search: { debounceTime: 500, }, }, parameters: externalFilteringAndLazyLoadingTemplate, }; export const StaticCategories: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown }, props: Object.keys(argTypes), setup(props) { const selectedValue = ref(props.value); const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { selectedValue.value = newSelectedValue; action('update:model-value')(newSelectedValue); }; return { // Values selectedValue, args: isVue2 ? props : _args, // Methods onUpdateModelValue, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown v-bind="args" :model-value="selectedValue" @update:model-value="onUpdateModelValue" > <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-dropdown> </div> `, }), args: { ...DEFAULT_PROPS, description: 'Select an option from a category', modelValue: '', multiple: false, staticCategories: true, items: [ { label: 'All animals', value: 'all', }, { label: 'Carnivores', value: 'carnivores', items: [ { label: 'T-Rex', value: 'trex', }, { label: 'Velociraptor', value: 'velociraptor', }, ], }, { label: 'Herbivores', value: 'herbivores', items: [ { label: 'Iguanodon', value: 'iguanodon', }, { label: 'Barney', value: 'barney', }, ], }, { label: 'Aquatic', value: 'aquatic', items: [ { label: 'Mosasaurus', value: 'mosasaurus', }, { label: 'Nessie', value: 'nessie', }, ], }, ] as BentoListboxOptions, }, }; export const VirtualScrolling: Story = { ...Default, args: { ...Default.args, items: new Array(1000).fill(0).map((_, index) => ({ label: `Option ${index + 1}`, value: index + 1 })), virtualScroll: true, }, }; export const CustomDisplayValue: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const onUpdateModelValue = (newSelectedValue: BentoListboxSelectedValue) => { selectedValue.value = newSelectedValue; action('update:model-value')(newSelectedValue); }; return { // Values args, selectedValue, // Methods onUpdateModelValue, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown v-bind="args" :model-value="selectedValue" @update:model-value="onUpdateModelValue" > <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-dropdown> </div> `, }), args: { ...DEFAULT_PROPS, modelValue: COUNTRY_OPTIONS[0].value, items: COUNTRY_OPTIONS, }, parameters: storybookDocsParameter(` <bento-dropdown :items="[ {label: "Netherlands +31", displayValue: "NL +31", value: "NL"}, {label: "Belgium +32", displayValue: "BE +32", value: "BE"}, {label: "Germany +49", displayValue: "DE +49", value: "DE"}, ]" :model-value="selectedValue" @update:model-value="onUpdateModelValue" /> `), };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<!-- TODO: Remove aria props as these are part of JSX elements /vue/types/jsx.d.ts --> <!-- eslint-disable vue/attribute-hyphenation --> <template> <div v-bento-click-outside-directive="clickOutsideDropdownOptions" class="b-dropdown" :class="conditionalClasses"> <field-label v-if="label" :id="labelId" :condensed="condensed" :label="label" :tooltip-text="tooltipText" :optional="optional" :required="required" /> <div ref="inputContainerRefWrapper" v-bento-keyboard-navigation-directive v-on="textboxKeyboardNavigationListeners" > <component :is="computedSize" ref="inputContainerRef" :condensed="condensed" :value="searchTerm" :additional-items-selected="additionalItemsSelected" :aria-controls="dropdownOptionsContainerId" :ariaExpanded="isDropdownOpen" :aria-labelledby="label ? labelId : null" :aria-describedby="ariaDescribedBy" :aria-label="computedAriaLabel" :aria-readonly="isReadOnly" :aria-required="required" :readonly="isReadOnly" :disabled="disabled" :display-value="displayValue" :dynamic-filtering="dynamicFiltering" :is-invalid="!!errorMessage || error" :multiple="multiple" :open="isDropdownOpen" :selected-value-item="!isInputComponent ? selectedValueItem : undefined" :show-slot-content-in-multiple="showSlotContentInMultiple" @input="searchTerm = $event" @open="toggleDropdown" @close="toggleDropdown" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </component> </div> <bento-teleport v-if="inputContainerRef" :disabled="teleport?.disabled" :to="teleport?.to"> <bento-dropdown-options-container :id="dropdownOptionsContainerId" ref="optionsContainerRef" :aria-label="computedAriaLabel" :component-loading="componentLoading" :empty-state="emptyState" :disabled="disabled" :is-option-disabled="isOptionDisabled" :items="filteredItems" :multiple="multiple" :static-categories="staticCategories" :open="isDropdownOpen" :selected-value="cachedSelectedListboxOptions" :searching="isSearching" :target-element="inputContainerRefWrapper" :loading="loading" :has-more-items="hasMoreItems" :lazy-load-type="lazyLoadType" :virtual-scroll="virtualScroll" v-on="listboxListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-dropdown-options-container> </bento-teleport> <error-message v-if="!!errorMessage" :id="errorId" :error-message="errorMessage" class="b-dropdown__error-message" /> <bento-typography v-if="description || hasSlot('description')" :id="descriptionId" class="b-dropdown__description" :class="descriptionConditionalClasses" el="span" > <slot name="description"> {{ description }} </slot> </bento-typography> </div> </template> <script setup lang="ts"> import { computed, defineComponent, inject, nextTick, type Ref, ref, toRef, toRefs, useAttrs, useSlots, watch, } from 'vue'; // Components import { BentoDropdownOptionsContainer } from './components/dropdown-options-container'; import { BentoTypography } from '@/components/typography'; import { DropdownInputDefault, DropdownInputSmall } from './components'; import { BentoListbox, useCachedSelectedValues, useMultiLevelItems } from '@/internal/listbox'; import { ErrorMessage } from '@/internal/error-message'; import { FieldLabel } from '@/internal/field-label'; import { BentoTeleport } from '@/internal/teleport'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { generateUid } from '@/core/utils/ts'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; // Composables import { useDisplayValue, useListboxKeyboardNavigation, useTextboxKeyboardNavigation } from './composables'; import { useVisibleDomOptions } from '@/internal/listbox/composables/use-visible-dom-options/use-visible-dom-options'; import { useFormLayoutFieldLoading } from '@/composables/use-form-layout-loading/use-form-layout-loading'; import { useSearchBarFilter } from '@/components/search-bar/useSearchBarDataFilter'; import { useHasSlot } from '@/composables'; // Directives import { BentoClickOutsideDirective as vBentoClickOutsideDirective } from '@/directives/click-outside'; import { BentoKeyboardNavigationDirective as vBentoKeyboardNavigationDirective } from '@/directives'; // Types import { BentoDropdownEvent, type BentoDropdownProps, BentoDropdownSize } from './dropdown.types'; import { BentoListboxEvent, type BentoListboxItemRole, type BentoListboxMultiSelectValue, type BentoListboxOptions, type BentoListboxSelectedValue, type BentoListboxSelectedValueLabelPair, } from '@/types/listbox'; import { DROPDOWN_SMALL_SIZE_INJECTION_KEY } from './dropdown.keys'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = withDefaults(defineProps<BentoDropdownProps>(), { ariaLabel: null, condensed: false, emptyState: undefined, disabled: false, dynamicFiltering: false, error: false, errorMessage: null, description: null, hasMoreItems: false, isOptionDisabled: undefined, items: () => [], label: null, loading: false, componentLoading: false, lazyLoadType: BentoListbox.props.lazyLoadType.default, multiple: false, optional: false, placeholder: null, readonly: false, required: false, search: undefined, showSlotContentInMultiple: false, size: null, staticCategories: false, teleport: () => ({ disabled: true }), tooltipText: null, value: null, modelValue: null, virtualScroll: false, enableValueLabelPair: false, }); const emit = defineEmits<{ /** * Emitted when an option is clicked. Updates the selected option from the "value" property linked to the v-model. * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', selectedValue: BentoListboxSelectedValue | any); // eslint-disable-line @typescript-eslint/no-explicit-any /** * Emitted when an option is clicked. Updates the selected option from the "modelValue" property linked to the v-model. */ (e: 'update:model-value', selectedValue: BentoListboxSelectedValue | any); // eslint-disable-line @typescript-eslint/no-explicit-any /** * Emitted when: * - `lazyLoad` is `automatic` and the scroll meets the end of the list * - `lazyload` is `button` and the button is clicked */ (e: 'show-more'); /** * Emitted when the dropdown is opened. */ (e: 'open'); }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const { emitValue } = useFormFieldEmits<BentoListboxSelectedValue>(emit); const inputContainerRef = ref(null); const inputContainerRefWrapper = ref(null); const optionsContainerRef: Ref<InstanceType<typeof BentoDropdownOptionsContainer>> = ref(null); const isDropdownOpen = ref(false); const dropdownOptionsContainerId = generateUid('bento-dropdown-options-container'); const labelId = generateUid('dropdown-label'); const descriptionId = generateUid('dropdown-description'); const errorId = generateUid('dropdown-error'); const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const dropdownValue = computed(() => props.modelValue ?? props.value); const { cachedSelectedListboxOptions, cachedListboxSelectedValues, setCachedValues } = useCachedSelectedValues( toRef(props, 'items'), dropdownValue, toRef(props, 'multiple'), toRef(props, 'enableValueLabelPair') ); const ariaLabelAttribute = ref(attrs['aria-label']); const ariaDescribedByAttribute = ref(attrs['aria-describedby']); const { ariaLabel, label } = toRefs(props); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => t('ariaLabelFallback')); const computedAriaLabel = useAriaLabel({ // TODO: take ariaLabel ref away when aria-label prop is removed ariaLabel: ariaLabel.value || (ariaLabelAttribute as Ref<string>), label, defaultFallback: computedAriaLabelFallbackMessage, }); // Inject input field component key to change the dropdown size const isInputComponent = props.size === BentoDropdownSize.SMALL || inject(DROPDOWN_SMALL_SIZE_INJECTION_KEY, false); const computedSize = computed(() => (isInputComponent ? BentoDropdownSize.SMALL : BentoDropdownSize.DEFAULT)); const conditionalClasses = computed(() => ({ 'b-dropdown--condensed': props.condensed, })); // Description styles const descriptionConditionalClasses = computed(() => ({ 'b-dropdown__description--error': !!props.errorMessage, })); // Filtering const searchTerm = ref(''); const items = toRef(props, 'items'); const staticCategories = toRef(props, 'staticCategories'); const filteredItems = useSearchBarFilter( searchTerm, items, props?.search?.searchEvent, staticCategories, props?.search?.debounceTime ); const isSearching = computed(() => props.dynamicFiltering && !!searchTerm.value); const displayValue = useDisplayValue({ items, placeholder: toRef(props, 'placeholder'), isMultiSelect: toRef(props, 'multiple'), selectedValues: cachedSelectedListboxOptions, }); const selectedValueItem = computed(() => { if (props?.multiple && Array.isArray(dropdownValue.value)) { return props.items.filter( ({ value }) => props?.value && (props?.value as BentoListboxMultiSelectValue).includes(value) ); } if (!props.multiple && props.staticCategories) { // Find the category that holds the selected value const category = props.items.find(categoryItem => categoryItem.items?.some(item => item.value === dropdownValue.value) ); // Find the specific item within the category const selectedCategoryItem = category ? category.items.find(item => item.value === dropdownValue.value) : props.items.find(({ value }) => value === dropdownValue.value); return selectedCategoryItem ?? null; } return props.items.find(({ value }) => value === dropdownValue.value); }); const selectedValueIndex = computed(() => { const searchList = isMultiLevelSelect.value ? flattenedItemsList.value : filteredItems.value; const index = searchList.findIndex(({ value }) => { return props.enableValueLabelPair ? (dropdownValue.value as BentoListboxSelectedValueLabelPair)?.value === value : dropdownValue.value === value; }); return index > -1 ? index : 0; }); const { isMultiLevelSelect, flattenedItemsList } = useMultiLevelItems(items); const isTreePattern = computed(() => isMultiLevelSelect.value && !props.staticCategories); const computedOptionRole = computed<BentoListboxItemRole>(() => (isTreePattern.value ? 'treeitem' : 'option')); const additionalItemsSelected = computed(() => { if (props.multiple) { const selectedItemsCount = Array.isArray(dropdownValue.value) ? dropdownValue.value.length : 0; const total = isMultiLevelSelect.value ? flattenedItemsList.value.length : items.value.length; if (selectedItemsCount === 0) { return null; } // Display count always - show "all" only when is not external filtering (hasMoreItems = true) if (!props.hasMoreItems && total > 0 && total === selectedItemsCount) { return t('all'); } return selectedItemsCount; } return null; }); // ARIA const ariaDescribedBy = computed( () => [ props.description ? descriptionId : null, ariaDescribedByAttribute.value, props.errorMessage ? errorId : null, ] .filter(Boolean) .join(' ') || null ); const listboxRef = computed(() => optionsContainerRef.value?.$el as HTMLDivElement); const { visibleDomOptions, updateVisibleDomOptions } = useVisibleDomOptions( listboxRef, filteredItems, computedOptionRole ); const openDropdown = () => { if (isReadOnly.value) { return; } isDropdownOpen.value = true; nextTick(() => { updateVisibleDomOptions(); }); }; const closeDropdown = () => { isDropdownOpen.value = false; }; const toggleDropdown = () => { if (isReadOnly.value) { return; } isDropdownOpen.value = !isDropdownOpen.value; nextTick(() => { updateVisibleDomOptions(); }); // Clear search if (isSearching.value) { searchTerm.value = ''; } }; const clickOutsideDropdown = () => { if (isSearching.value) { searchTerm.value = ''; } if (isDropdownOpen.value) { isDropdownOpen.value = false; // Reset the internal dropdown options state optionsContainerRef.value.onOutsideDropdownClick(); } }; const clickOutsideDropdownOptions = computed(() => [ clickOutsideDropdown, { ignore: [listboxRef], }, ]); const onOptionSelected = async (selectedValue: BentoListboxOptions) => { // Clear and close the dropdown on single selection if (!props.multiple) { searchTerm.value = ''; isDropdownOpen.value = false; } setCachedValues(selectedValue); /** * Emit the value/label pair objects if enableValueLabelPair is enabled, * otherwise just emit the list of selected values (number or string) */ emitValue(cachedListboxSelectedValues.value); // Focus the input after selection and dropdown has closed if (!props.multiple) { await nextTick(); focus(); } }; const emitShowMoreEvent = () => { emit(BentoDropdownEvent.SHOW_MORE); }; const textboxKeyboardNavigationListeners = useTextboxKeyboardNavigation( { inputContainerRef, optionsContainerRef, selectedValueIndex, isDropdownOpen, isDynamicFltering: toRef(props, 'dynamicFiltering'), items: visibleDomOptions, isMultiple: props.multiple, totalItemCount: computed(() => filteredItems.value.length), isVirtualScroll: !props?.multiple && props?.virtualScroll !== false && props?.lazyLoadType === 'none', }, toggleDropdown, openDropdown, closeDropdown ); const listboxKeyboardNavigationListeners = useListboxKeyboardNavigation( { inputContainerRef, isDropdownOpen, }, closeDropdown ); const listboxListeners = { [BentoListboxEvent.CLOSE_DROPDOWN]: closeDropdown, [BentoListboxEvent.SELECT]: onOptionSelected, [BentoListboxEvent.SHOW_MORE]: emitShowMoreEvent, ...listboxKeyboardNavigationListeners, }; if (props.error) { deprecate( 'BentoDropdown "error" property', `Use the BentoDropdown "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-dropdown errorMessage="Error message text" />`, '2.0.0' ); } if (props.size) { deprecate('BentoDropdown "size" property', `Do not use. Only 'default' size should be used.`, '2.0.0'); } if (props.value) { deprecate( 'BentoDropdown "value" property', `The use of "value" prop in "BentoDropdown" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } const focus = () => { inputContainerRef.value.focus(); }; const scrollToSelectedOption = async () => { await nextTick(); if (!isDropdownOpen.value || props.multiple || !optionsContainerRef.value) { return; } const index = selectedValueIndex.value; if (index <= 0) { return; } // eslint-disable-next-line @typescript-eslint/no-explicit-any const listboxSingleSelect = (optionsContainerRef.value as any)?.$refs?.dropdownOptionsListboxRef?.$refs ?.listboxItemRef; listboxSingleSelect?.scrollToItem?.(index); }; watch( () => isDropdownOpen.value, (isOpen: boolean) => { if (isOpen) { emit('open'); scrollToSelectedOption(); } } ); defineExpose({ focus, }); </script> <script lang="ts"> /** * A Dropdown shows a selected option's. * Use a dropdown when you want users to select options * from a list of pre-defined options. * * @example * import { BentoDropdown } from '@adyen/bento-vue2'; * import type { BentoDropdownOptions } from '@adyen/bento-vue2' * * export default { * components: { BentoDropdown }, * template: ` * <bento-dropdown * v-model="selectedValue" * :disabled="false" * :multiple="true" * :isOptionsDisabled="option => option.value === 2" * :items="options" * /> * `, * setup() { * const selectedValue = ref(2) // Default value * const options: BentoDropdownOptions = [ * { label: 'Option 1', value: 1 }, * { label: 'Option 2', value: 2 }, * ]; * return { options, selectedValue }; * } * } */ export default defineComponent({ i18n: { messages }, name: 'b-dropdown', components: { DropdownInputDefault, DropdownInputSmall, }, model: { prop: 'modelValue' }, }); </script> <style lang="scss" scoped src="./dropdown.scss" />
|
|
1
|
+
<!-- TODO: Remove aria props as these are part of JSX elements /vue/types/jsx.d.ts --> <!-- eslint-disable vue/attribute-hyphenation --> <template> <div v-bento-click-outside-directive="clickOutsideDropdownOptions" class="b-dropdown" :class="conditionalClasses"> <field-label v-if="label" :id="labelId" :condensed="condensed" :label="label" :tooltip-text="tooltipText" :optional="optional" :required="required" /> <div ref="inputContainerRefWrapper" v-bento-keyboard-navigation-directive v-on="textboxKeyboardNavigationListeners" > <component :is="computedSize" ref="inputContainerRef" :condensed="condensed" :value="searchTerm" :additional-items-selected="additionalItemsSelected" :aria-controls="dropdownOptionsContainerId" :ariaExpanded="isDropdownOpen" :aria-labelledby="label ? labelId : null" :aria-describedby="ariaDescribedBy" :aria-label="computedAriaLabel" :aria-readonly="isReadOnly" :aria-required="required" :readonly="isReadOnly" :disabled="disabled" :display-value="displayValue" :dynamic-filtering="dynamicFiltering" :is-invalid="!!errorMessage || error" :multiple="multiple" :open="isDropdownOpen" :selected-value-item="!isInputComponent ? selectedValueItem : undefined" :show-slot-content-in-multiple="showSlotContentInMultiple" @input="searchTerm = $event" @open="toggleDropdown" @close="toggleDropdown" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </component> </div> <bento-teleport v-if="inputContainerRef" :disabled="teleport?.disabled" :to="teleport?.to"> <bento-dropdown-options-container :id="dropdownOptionsContainerId" ref="optionsContainerRef" :aria-label="computedAriaLabel" :component-loading="componentLoading" :empty-state="emptyState" :disabled="disabled" :is-option-disabled="isOptionDisabled" :items="filteredItems" :multiple="multiple" :static-categories="staticCategories" :open="isDropdownOpen" :selected-value="cachedSelectedListboxOptions" :searching="isSearching" :target-element="inputContainerRefWrapper" :loading="loading" :has-more-items="hasMoreItems" :lazy-load-type="lazyLoadType" :virtual-scroll="virtualScroll" v-on="listboxListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-dropdown-options-container> </bento-teleport> <error-message v-if="!!errorMessage" :id="errorId" :error-message="errorMessage" class="b-dropdown__error-message" /> <bento-typography v-if="description || hasSlot('description')" :id="descriptionId" class="b-dropdown__description" :class="descriptionConditionalClasses" el="span" > <slot name="description"> {{ description }} </slot> </bento-typography> </div> </template> <script setup lang="ts"> import { computed, defineComponent, inject, nextTick, type Ref, ref, toRef, toRefs, useAttrs, useSlots, watch, } from 'vue'; // Components import { BentoDropdownOptionsContainer } from './components/dropdown-options-container'; import { BentoTypography } from '@/components/typography'; import { DropdownInputDefault, DropdownInputSmall } from './components'; import { BentoListbox, useCachedSelectedValues, useMultiLevelItems } from '@/internal/listbox'; import { ErrorMessage } from '@/internal/error-message'; import { FieldLabel } from '@/internal/field-label'; import { BentoTeleport } from '@/internal/teleport'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { generateUid } from '@/core/utils/ts'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; // Composables import { useDisplayValue, useListboxKeyboardNavigation, useTextboxKeyboardNavigation } from './composables'; import { useVisibleDomOptions } from '@/internal/listbox/composables/use-visible-dom-options/use-visible-dom-options'; import { useFormLayoutFieldLoading } from '@/composables/use-form-layout-loading/use-form-layout-loading'; import { useSearchBarFilter } from '@/components/search-bar/useSearchBarDataFilter'; import { useHasSlot } from '@/composables'; // Directives import { BentoClickOutsideDirective as vBentoClickOutsideDirective } from '@/directives/click-outside'; import { BentoKeyboardNavigationDirective as vBentoKeyboardNavigationDirective } from '@/directives'; // Types import { BentoDropdownEvent, type BentoDropdownProps, BentoDropdownSize } from './dropdown.types'; import { BentoListboxEvent, type BentoListboxItemRole, type BentoListboxMultiSelectValue, type BentoListboxOptions, type BentoListboxSelectedValue, type BentoListboxSelectedValueLabelPair, } from '@/types/listbox'; import { DROPDOWN_SMALL_SIZE_INJECTION_KEY } from './dropdown.keys'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = withDefaults(defineProps<BentoDropdownProps>(), { ariaLabel: null, condensed: false, emptyState: undefined, disabled: false, dynamicFiltering: false, error: false, errorMessage: null, description: null, hasMoreItems: false, isOptionDisabled: undefined, items: () => [], label: null, loading: false, componentLoading: false, lazyLoadType: BentoListbox.props.lazyLoadType.default, multiple: false, optional: false, placeholder: null, readonly: false, required: false, search: undefined, showSlotContentInMultiple: false, size: null, staticCategories: false, teleport: () => ({ disabled: true }), tooltipText: null, value: null, modelValue: null, virtualScroll: false, enableValueLabelPair: false, }); const emit = defineEmits<{ /** * Emitted when an option is clicked. Updates the selected option from the "value" property linked to the v-model. * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', selectedValue: BentoListboxSelectedValue | any); // eslint-disable-line @typescript-eslint/no-explicit-any /** * Emitted when an option is clicked. Updates the selected option from the "modelValue" property linked to the v-model. */ (e: 'update:model-value', selectedValue: BentoListboxSelectedValue | any); // eslint-disable-line @typescript-eslint/no-explicit-any /** * Emitted when: * - `lazyLoad` is `automatic` and the scroll meets the end of the list * - `lazyload` is `button` and the button is clicked */ (e: 'show-more'); /** * Emitted when the dropdown is opened. */ (e: 'open'); }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const { emitValue } = useFormFieldEmits<BentoListboxSelectedValue>(emit); const inputContainerRef = ref(null); const inputContainerRefWrapper = ref(null); const optionsContainerRef: Ref<InstanceType<typeof BentoDropdownOptionsContainer>> = ref(null); const isDropdownOpen = ref(false); const dropdownOptionsContainerId = generateUid('bento-dropdown-options-container'); const labelId = generateUid('dropdown-label'); const descriptionId = generateUid('dropdown-description'); const errorId = generateUid('dropdown-error'); const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const dropdownValue = computed(() => props.modelValue ?? props.value); const { cachedSelectedListboxOptions, cachedListboxSelectedValues, setCachedValues } = useCachedSelectedValues( toRef(props, 'items'), dropdownValue, toRef(props, 'multiple'), toRef(props, 'enableValueLabelPair') ); const ariaLabelAttribute = ref(attrs['aria-label']); const ariaDescribedByAttribute = ref(attrs['aria-describedby']); const { ariaLabel, label } = toRefs(props); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => t('ariaLabelFallback')); const computedAriaLabel = useAriaLabel({ // TODO: take ariaLabel ref away when aria-label prop is removed ariaLabel: ariaLabel.value || (ariaLabelAttribute as Ref<string>), label, defaultFallback: computedAriaLabelFallbackMessage, }); // Inject input field component key to change the dropdown size const isInputComponent = props.size === BentoDropdownSize.SMALL || inject(DROPDOWN_SMALL_SIZE_INJECTION_KEY, false); const computedSize = computed(() => (isInputComponent ? BentoDropdownSize.SMALL : BentoDropdownSize.DEFAULT)); const conditionalClasses = computed(() => ({ 'b-dropdown--condensed': props.condensed, })); // Description styles const descriptionConditionalClasses = computed(() => ({ 'b-dropdown__description--error': !!props.errorMessage, })); // Filtering const searchTerm = ref(''); const items = toRef(props, 'items'); const staticCategories = toRef(props, 'staticCategories'); const filteredItems = useSearchBarFilter( searchTerm, items, props?.search?.searchEvent, staticCategories, props?.search?.debounceTime ); const isSearching = computed(() => props.dynamicFiltering && !!searchTerm.value); const displayValue = useDisplayValue({ items, placeholder: toRef(props, 'placeholder'), isMultiSelect: toRef(props, 'multiple'), selectedValues: cachedSelectedListboxOptions, }); const selectedValueItem = computed(() => { if (props?.multiple && Array.isArray(dropdownValue.value)) { return props.items.filter( ({ value }) => props?.value && (props?.value as BentoListboxMultiSelectValue).includes(value) ); } if (!props.multiple && props.staticCategories) { // Find the category that holds the selected value const category = props.items.find(categoryItem => categoryItem.items?.some(item => item.value === dropdownValue.value) ); // Find the specific item within the category const selectedCategoryItem = category ? category.items.find(item => item.value === dropdownValue.value) : props.items.find(({ value }) => value === dropdownValue.value); return selectedCategoryItem ?? null; } return props.items.find(({ value }) => value === dropdownValue.value); }); const selectedValueIndex = computed(() => { const searchList = isMultiLevelSelect.value ? flattenedItemsList.value : filteredItems.value; const index = searchList.findIndex(({ value }) => { return props.enableValueLabelPair ? (dropdownValue.value as BentoListboxSelectedValueLabelPair)?.value === value : dropdownValue.value === value; }); return index > -1 ? index : 0; }); const { isMultiLevelSelect, flattenedItemsList } = useMultiLevelItems(items); const isTreePattern = computed(() => isMultiLevelSelect.value && !props.staticCategories); const computedOptionRole = computed<BentoListboxItemRole>(() => (isTreePattern.value ? 'treeitem' : 'option')); const additionalItemsSelected = computed(() => { if (props.multiple) { const selectedItemsCount = Array.isArray(dropdownValue.value) ? dropdownValue.value.length : 0; const total = isMultiLevelSelect.value ? flattenedItemsList.value.length : items.value.length; if (selectedItemsCount === 0) { return null; } // Display count always - show "all" only when is not external filtering (hasMoreItems = true) if (!props.hasMoreItems && total > 0 && total === selectedItemsCount) { return t('all'); } return selectedItemsCount; } return null; }); // ARIA const ariaDescribedBy = computed( () => [ props.description ? descriptionId : null, ariaDescribedByAttribute.value, props.errorMessage ? errorId : null, ] .filter(Boolean) .join(' ') || null ); const listboxRef = computed(() => optionsContainerRef.value?.$el as HTMLDivElement); const { visibleDomOptions, updateVisibleDomOptions } = useVisibleDomOptions( listboxRef, filteredItems, computedOptionRole ); const openDropdown = () => { if (isReadOnly.value) { return; } isDropdownOpen.value = true; nextTick(() => { updateVisibleDomOptions(); }); }; const closeDropdown = () => { isDropdownOpen.value = false; }; const toggleDropdown = () => { if (isReadOnly.value) { return; } isDropdownOpen.value = !isDropdownOpen.value; nextTick(() => { updateVisibleDomOptions(); }); // Clear search if (isSearching.value) { searchTerm.value = ''; } }; const clickOutsideDropdown = () => { if (isSearching.value) { searchTerm.value = ''; } if (isDropdownOpen.value) { isDropdownOpen.value = false; // Reset the internal dropdown options state optionsContainerRef.value.onOutsideDropdownClick(); } }; const clickOutsideDropdownOptions = computed(() => [ clickOutsideDropdown, { ignore: [listboxRef], }, ]); const onOptionSelected = async (selectedValue: BentoListboxOptions) => { // Clear and close the dropdown on single selection if (!props.multiple) { searchTerm.value = ''; isDropdownOpen.value = false; } setCachedValues(selectedValue); /** * Emit the value/label pair objects if enableValueLabelPair is enabled, * otherwise just emit the list of selected values (number or string) */ emitValue(cachedListboxSelectedValues.value); // Focus the input after selection and dropdown has closed if (!props.multiple) { await nextTick(); focus(); } }; const emitShowMoreEvent = () => { emit(BentoDropdownEvent.SHOW_MORE); }; const textboxKeyboardNavigationListeners = useTextboxKeyboardNavigation( { inputContainerRef, optionsContainerRef, selectedValueIndex, isDropdownOpen, isDynamicFltering: toRef(props, 'dynamicFiltering'), items: visibleDomOptions, isMultiple: props.multiple, totalItemCount: computed(() => filteredItems.value.length), isVirtualScroll: !props?.multiple && props?.virtualScroll !== false && props?.lazyLoadType === 'none', }, toggleDropdown, openDropdown, closeDropdown ); const listboxKeyboardNavigationListeners = useListboxKeyboardNavigation( { inputContainerRef, isDropdownOpen, }, closeDropdown ); const listboxListeners = { [BentoListboxEvent.CLOSE_DROPDOWN]: closeDropdown, [BentoListboxEvent.SELECT]: onOptionSelected, [BentoListboxEvent.SHOW_MORE]: emitShowMoreEvent, ...listboxKeyboardNavigationListeners, }; if (props.error) { deprecate( 'BentoDropdown "error" property', `Use the BentoDropdown "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-dropdown errorMessage="Error message text" />`, '2.0.0' ); } if (props.size) { deprecate('BentoDropdown "size" property', `Do not use. Only 'default' size should be used.`, '2.0.0'); } if (props.value) { deprecate( 'BentoDropdown "value" property', `The use of "value" prop in "BentoDropdown" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } const focus = () => { inputContainerRef.value.focus(); }; const scrollToSelectedOption = async () => { await nextTick(); if (!isDropdownOpen.value || props.multiple || !optionsContainerRef.value) { return; } const index = selectedValueIndex.value; if (index <= 0) { return; } const dropdownOptionsListboxRef = optionsContainerRef.value.dropdownOptionsListboxRef; const listbox = dropdownOptionsListboxRef?.listboxRef; const listboxSingleSelect = dropdownOptionsListboxRef?.listboxItemRef; listboxSingleSelect?.scrollToItem?.(index, listbox); }; watch( () => isDropdownOpen.value, (isOpen: boolean) => { if (isOpen) { emit('open'); scrollToSelectedOption(); } } ); defineExpose({ focus, }); </script> <script lang="ts"> /** * A Dropdown shows a selected option's. * Use a dropdown when you want users to select options * from a list of pre-defined options. * * @example * import { BentoDropdown } from '@adyen/bento-vue2'; * import type { BentoDropdownOptions } from '@adyen/bento-vue2' * * export default { * components: { BentoDropdown }, * template: ` * <bento-dropdown * v-model="selectedValue" * :disabled="false" * :multiple="true" * :isOptionsDisabled="option => option.value === 2" * :items="options" * /> * `, * setup() { * const selectedValue = ref(2) // Default value * const options: BentoDropdownOptions = [ * { label: 'Option 1', value: 1 }, * { label: 'Option 2', value: 2 }, * ]; * return { options, selectedValue }; * } * } */ export default defineComponent({ i18n: { messages }, name: 'b-dropdown', components: { DropdownInputDefault, DropdownInputSmall, }, model: { prop: 'modelValue' }, }); </script> <style lang="scss" scoped src="./dropdown.scss" />
|
|
@@ -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="
|
|
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
|
-
|
|
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), };
|