@adyen/bento-mcp 0.12.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.vue +1 -1
- package/dist/assets/components/data-grid/data-grid.docs.mdx +11 -1
- package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.internal.types.ts +1 -1
- package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-default-textbox/dropdown-default-textbox.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-small-textbox/dropdown-small-textbox.vue +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.stories.ts +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-country/dropdown-country.stories.ts +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-currency/dropdown-currency.stories.ts +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.stories.ts +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-tag/dropdown-tag.stories.ts +1 -1
- package/dist/assets/components/dropdown/dropdown.docs.mdx +11 -0
- package/dist/assets/components/dropdown/dropdown.stories.ts +1 -1
- package/dist/assets/components/dropdown/dropdown.types.ts +1 -1
- package/dist/assets/components/dropdown/dropdown.vue +1 -1
- package/dist/assets/components/filter-bar/components/all-filters-modal/components/all-filters-modal-selected-filter/all-filters-modal-selected-filter.vue +1 -1
- package/dist/assets/components/filter-bar/filter-bar-filter-components.types.ts +1 -0
- package/dist/assets/components/filter-bar/filter-bar.vue +1 -1
- package/dist/assets/components/menu/menu.types.ts +1 -1
- package/dist/assets/components/pagination/components/pagination-context/pagination-context.vue +1 -1
- package/dist/assets/components/pagination/components/pagination-controls/pagination-controls.vue +1 -1
- package/dist/assets/components/promo-banner/promo-banner.types.ts +1 -1
- package/dist/assets/components/sidepanel/sidepanel.vue +1 -1
- package/dist/assets/components/summary-grid/summary-grid.docs.mdx +60 -13
- package/dist/assets/components/summary-grid/summary-grid.stories.ts +1 -1
- package/dist/assets/components/timeline/components/timeline-item/timeline-item.stories.ts +1 -1
- package/dist/assets/components/timeline/components/timeline-item/timeline-item.vue +1 -1
- package/dist/assets/components/timeline/timeline.docs.mdx +20 -4
- package/dist/assets/components/timeline/timeline.types.ts +1 -1
- package/dist/assets/components/typography/typography.types.ts +1 -1
- package/dist/assets/deprecations.md +8 -0
- package/dist/assets/templates/dashboard-template/dashboard-template.vue +1 -1
- package/dist/assets/usage.json +7 -7
- package/dist/main.js +1 -1
- package/package.json +2 -2
|
@@ -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'; const DropdownWithDescriptionExample = "<template>\n <bento-dropdown\n v-model=\"modelValue\"\n aria-label=\"Descriptive definition of what the dropdown is for\"\n description=\"Description of the field\"\n dynamic-filtering\n :multiple=\"false\"\n :items=\"items\"\n :is-option-disabled=\"isOptionDisabled\"\n label=\"Dropdown input label\"\n placeholder=\"Dropdown placeholder text\"\n enable-value-label-pair\n />\n</template>\n<script setup lang=\"ts\">\n import {\n BentoDropdown,\n type BentoListboxOptionItem,\n type BentoListboxOptions,\n type BentoListboxSelectedValueLabelPair,\n } from '@adyen/bento-vue2';\n import { ref } from 'vue';\n\n const items: BentoListboxOptions = [\n { label: 'Option 1', value: 1, description: 'Description 1' },\n { label: 'Option 2', value: 2, description: 'Description 2' },\n { label: 'Option 3', value: 3, description: 'Description 3' },\n { label: 'Option 4', value: 4, description: 'Description 4' },\n ];\n\n // Select the first option as Value / Label pair\n const modelValue = ref<BentoListboxSelectedValueLabelPair>(items[0]);\n\n // Disable individual options\n const isOptionDisabled = (option: BentoListboxOptionItem) => option.value === 3;\n</script>\n"; const DropdownWithCustomSlotsExample = "<template>\n <bento-dropdown\n v-model=\"modelValue\"\n aria-label=\"Descriptive definition of what the dropdown is for\"\n description=\"Description of the field\"\n :multiple=\"false\"\n :items=\"items\"\n label=\"Dropdown input label\"\n placeholder=\"Dropdown placeholder text\"\n enable-value-label-pair\n >\n <!-- Define how the display value should be rendered -->\n <template #display-value=\"{ label, value }\">\n <bento-typography>{{ label }} : {{ value }}</bento-typography>\n </template>\n\n <!-- Define how the items should be rendered -->\n <template #default=\"{ label, description }\">\n <bento-typography>{{ label }} : {{ description }}</bento-typography>\n </template>\n </bento-dropdown>\n</template>\n<script setup lang=\"ts\">\n import {\n BentoDropdown,\n type BentoListboxOptions,\n type BentoListboxSelectedValueLabelPair,\n } from '@adyen/bento-vue2';\n import { ref } from 'vue';\n\n const items: BentoListboxOptions = [\n { label: 'Option 1', value: 1, description: 'Description 1' },\n { label: 'Option 2', value: 2, description: 'Description 2' },\n { label: 'Option 3', value: 3, description: 'Description 3' },\n { label: 'Option 4', value: 4, description: 'Description 4' },\n ];\n\n // Select the first option as Value / Label pair\n const modelValue = ref<BentoListboxSelectedValueLabelPair>(items[0]);\n</script>\n"; const DropdownNestedMultiSelectWithLazyLoadExample = "<template>\n <bento-dropdown\n v-model=\"selectedValue\"\n :items=\"items\"\n :is-option-disabled=\"isOptionDisabled\"\n :has-more-items=\"hasMoreItems\"\n description=\"Choose one or more options\"\n label=\"Dropdown input label\"\n multiple\n lazy-load-type=\"button\"\n enable-value-label-pair\n :loading=\"isLoadingData\"\n @show-more=\"onShowMore\"\n />\n</template>\n\n<script setup lang=\"ts\">\n import { ref } from 'vue';\n import {\n BentoDropdown,\n type BentoListboxLazyLoadEvent,\n type BentoListboxOptionItem,\n type BentoListboxOptions,\n type BentoListboxSelectedValue,\n } from '@adyen/bento-vue2';\n\n // Control main list lazy load\n const hasMoreItems = ref(true);\n const isLoadingData = ref(false);\n // All values must be on the list (lazy loaded or not)\n\n const selectedValue = ref<BentoListboxSelectedValue>([\n // Initially loaded files\n { value: 1, label: 'Option 1' },\n // Lazy loaded category and all its options\n { value: 'c1', label: 'Category 1' },\n // Global lazy loaded values\n { value: 100, label: 'Option 100' },\n ]);\n\n // Fetch nested item lazy loaded items\n const nestedLazyLoadEvent: BentoListboxLazyLoadEvent = ({ value, index }) => {\n // eslint-disable-next-line no-console\n console.log(`Fetching data for: Option ${value}. Index: ${index}`);\n\n // Fetching data for option with value and index\n return new Promise(resolve => {\n const fetchedData = [\n {\n label: 'Option 6',\n description: 'C1-O6',\n value: 6,\n },\n {\n label: 'Option 7',\n description: 'C1-O7',\n value: 7,\n },\n ] satisfies BentoListboxOptions;\n\n // Append the newly fetched items to the nested list of items\n items.value[1].lazyLoad.hasMoreItems = false;\n items.value[1].items = [...items.value[1].items, ...fetchedData];\n\n return resolve();\n });\n };\n\n const items = ref<BentoListboxOptions>([\n {\n label: 'Option 1',\n description: 'Uncategorized',\n value: 1,\n },\n {\n label: 'Category 1',\n description: 'C1',\n value: 'c1',\n items: [\n {\n label: 'Option 2',\n description: 'C1-O2',\n value: 2,\n },\n {\n label: 'Option 3',\n description: 'C1-O3',\n value: 3,\n },\n ],\n lazyLoad: {\n hasMoreItems: true,\n lazyLoadEvent: nestedLazyLoadEvent,\n },\n },\n {\n label: 'Category 2',\n description: 'C2',\n value: 'c2',\n items: [\n {\n label: 'Option 4',\n description: 'C2-O4',\n value: 4,\n },\n {\n label: 'Option 5',\n description: 'C2-O5',\n value: 5,\n },\n ],\n },\n ]);\n\n // Load the items of the main list\n const onShowMore = () => {\n isLoadingData.value = true;\n // Append the newly fetched items to the nested list of items\n items.value.push({\n label: 'Option 100',\n description: 'Lazy loaded',\n value: 100,\n });\n\n isLoadingData.value = false;\n hasMoreItems.value = false;\n };\n\n const isOptionDisabled = (option: BentoListboxOptionItem) => [4, 5].includes(option.value as number);\n</script>\n"; const DropdownCustomDisplayValue = "<template>\n <bento-dropdown\n v-model=\"selectedValue\"\n description=\"Choose an option\"\n :items=\"items\"\n label=\"Label\"\n placeholder=\"Select an option\"\n />\n</template>\n\n<script setup lang=\"ts\">\n import { BentoDropdown, type BentoListboxOptions, type BentoListboxSelectedValue } from '@adyen/bento-vue2';\n import { ref } from 'vue';\n\n const items: BentoListboxOptions = [\n { label: 'Netherlands +31', displayValue: 'NL (+31)', value: 'NL' },\n { label: 'Belgium +32', displayValue: 'BE (+32)', value: 'BE' },\n { label: 'Germany +49', displayValue: 'DE (+49)', value: 'DE' },\n ];\n const selectedValue = ref<BentoListboxSelectedValue>('NL');\n</script>\n"; const DropdownDefault = "<template>\n <bento-dropdown\n v-model=\"selectedValue\"\n description=\"Choose an option\"\n :is-option-disabled=\"isOptionDisabled\"\n :items=\"items\"\n label=\"Label\"\n placeholder=\"Select an option\"\n enable-value-label-pair\n />\n</template>\n\n<script setup lang=\"ts\">\n import {\n BentoDropdown,\n type BentoListboxOptionItem,\n type BentoListboxOptions,\n type BentoListboxSelectedValue,\n } from '@adyen/bento-vue2';\n import { ref } from 'vue';\n\n const items: BentoListboxOptions = Array.from({ length: 10 }, (_, index) => ({\n label: `Option ${index + 1}`,\n value: index + 1,\n }));\n const selectedValue = ref<BentoListboxSelectedValue>({ label: 'Option 1', value: 1 });\n const isOptionDisabled = (option: BentoListboxOptionItem) => option.value === 3;\n</script>\n"; const DropdownExternalFiltering = "<template>\n <bento-dropdown\n v-model=\"selectedValue\"\n :component-loading=\"isSearching\"\n description=\"Type to simulate fetching new items.\"\n dynamic-filtering\n :items=\"items\"\n label=\"Dropdown with external filtering\"\n multiple\n placeholder=\"Select an option\"\n :search=\"search\"\n enable-value-label-pair\n />\n</template>\n\n<script setup lang=\"ts\">\n import {\n BentoDropdown,\n type BentoDropdownSearchOptions,\n type BentoListboxOptions,\n type BentoListboxSelectedValue,\n } from '@adyen/bento-vue2';\n import { computed, ref } from 'vue';\n\n const initialItems: BentoListboxOptions = Array.from({ length: 10 }, (_, index) => ({\n label: `Option ${index + 1}`,\n value: index + 1,\n }));\n const isSearching = ref(false);\n const items = ref(initialItems);\n const selectedValue = ref<BentoListboxSelectedValue>([]);\n\n const search = computed<BentoDropdownSearchOptions>(() => ({\n debounceTime: 500,\n searchEvent: async value => {\n isSearching.value = true;\n await new Promise(resolve => setTimeout(resolve, 1000));\n items.value = value\n ? Array.from({ length: 5 }, (_, index) => ({\n label: `${value} ${index + 1}`,\n value: `${value}-${index + 1}`,\n }))\n : initialItems;\n isSearching.value = false;\n },\n }));\n</script>\n"; const DropdownExternalFilteringAndLazyLoading = "<template>\n <bento-dropdown\n v-model=\"selectedValue\"\n :component-loading=\"isComponentLoading\"\n description=\"Search for items and scroll to load more results.\"\n dynamic-filtering\n :has-more-items=\"hasMoreItems\"\n :items=\"items\"\n label=\"External Filtering & Lazy Loading\"\n lazy-load-type=\"automatic\"\n :loading=\"isLazyLoading\"\n multiple\n placeholder=\"Select an option\"\n :search=\"search\"\n enable-value-label-pair\n @show-more=\"loadMore\"\n />\n</template>\n\n<script setup lang=\"ts\">\n import {\n BentoDropdown,\n type BentoDropdownSearchOptions,\n type BentoListboxOptions,\n type BentoListboxSelectedValue,\n } from '@adyen/bento-vue2';\n import { computed, onMounted, ref } from 'vue';\n\n const allItems: BentoListboxOptions = Array.from({ length: 1000 }, (_, index) => ({\n label: `Item ${index + 1}`,\n value: index + 1,\n }));\n const currentPage = ref(1);\n const currentSearchTerm = ref('');\n const hasMoreItems = ref(true);\n const isComponentLoading = ref(false);\n const isLazyLoading = ref(false);\n const items = ref<BentoListboxOptions>([]);\n const selectedValue = ref<BentoListboxSelectedValue>([{ label: 'Item 2000', value: 2000 }]);\n const pageSize = 20;\n\n const fetchItems = async (searchTerm: string, page: number) => {\n await new Promise(resolve => setTimeout(resolve, 500));\n const filtered = allItems.filter(item => item.label.toLowerCase().includes(searchTerm.toLowerCase()));\n const end = page * pageSize;\n return { hasMore: end < filtered.length, items: filtered.slice(end - pageSize, end) };\n };\n const runSearch = async (searchTerm: string) => {\n currentPage.value = 1;\n currentSearchTerm.value = searchTerm;\n isComponentLoading.value = true;\n const result = await fetchItems(searchTerm, currentPage.value);\n items.value = result.items;\n hasMoreItems.value = result.hasMore;\n isComponentLoading.value = false;\n };\n const loadMore = async () => {\n if (isLazyLoading.value || !hasMoreItems.value) {\n return;\n }\n isLazyLoading.value = true;\n const result = await fetchItems(currentSearchTerm.value, ++currentPage.value);\n items.value = [...items.value, ...result.items];\n hasMoreItems.value = result.hasMore;\n isLazyLoading.value = false;\n };\n const search = computed<BentoDropdownSearchOptions>(() => ({\n debounceTime: 500,\n searchEvent: runSearch,\n }));\n\n onMounted(() => runSearch(''));\n</script>\n"; const DropdownLazyLoading = "<template>\n <bento-dropdown\n v-model=\"selectedValue\"\n description=\"Scroll down or click the button to load more items.\"\n :has-more-items=\"hasMoreItems\"\n :items=\"items\"\n label=\"Lazy Loading\"\n lazy-load-type=\"automatic\"\n :loading=\"isLoading\"\n multiple\n placeholder=\"Select an option\"\n enable-value-label-pair\n @show-more=\"loadMore\"\n />\n</template>\n\n<script setup lang=\"ts\">\n import { BentoDropdown, type BentoListboxOptions, type BentoListboxSelectedValue } from '@adyen/bento-vue2';\n import { onMounted, ref } from 'vue';\n\n const allItems: BentoListboxOptions = Array.from({ length: 100 }, (_, index) => ({\n label: `Item ${index + 1}`,\n value: index + 1,\n }));\n const currentPage = ref(1);\n const hasMoreItems = ref(true);\n const isLoading = ref(false);\n const items = ref<BentoListboxOptions>([]);\n const selectedValue = ref<BentoListboxSelectedValue>([]);\n const pageSize = 20;\n\n const fetchItems = async (page: number) => {\n await new Promise(resolve => setTimeout(resolve, 500));\n const end = page * pageSize;\n return { hasMore: end < allItems.length, items: allItems.slice(end - pageSize, end) };\n };\n const loadMore = async () => {\n if (isLoading.value || !hasMoreItems.value) {\n return;\n }\n isLoading.value = true;\n const result = await fetchItems(++currentPage.value);\n items.value = [...items.value, ...result.items];\n hasMoreItems.value = result.hasMore;\n isLoading.value = false;\n };\n onMounted(async () => {\n isLoading.value = true;\n const result = await fetchItems(currentPage.value);\n items.value = result.items;\n hasMoreItems.value = result.hasMore;\n isLoading.value = false;\n });\n</script>\n"; const DropdownMultiSelect = "<template>\n <bento-dropdown\n v-model=\"selectedValue\"\n description=\"Choose one or more options\"\n :is-option-disabled=\"isOptionDisabled\"\n :items=\"items\"\n label=\"Label\"\n multiple\n placeholder=\"Select an option\"\n enable-value-label-pair\n />\n</template>\n\n<script setup lang=\"ts\">\n import {\n BentoDropdown,\n type BentoListboxOptionItem,\n type BentoListboxOptions,\n type BentoListboxSelectedValue,\n } from '@adyen/bento-vue2';\n import { ref } from 'vue';\n\n const items: BentoListboxOptions = Array.from({ length: 10 }, (_, index) => ({\n label: `Option ${index + 1}`,\n value: index + 1,\n }));\n const selectedValue = ref<BentoListboxSelectedValue>([\n { label: 'Option 1', value: 1 },\n { label: 'Option 3', value: 3 },\n { label: 'Option 100', value: 100 },\n ]);\n const isOptionDisabled = (option: BentoListboxOptionItem) => [3, 5, 7, 9].includes(option.value as number);\n</script>\n"; // 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 }, }, // 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', }, }, }, }; export default meta; type Story = StoryObj<typeof BentoDropdown>; 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(DropdownDefault), }; 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(DropdownMultiSelect), }; 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.modelValue); 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), }; 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: storybookDocsParameter(DropdownExternalFiltering), }; 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: storybookDocsParameter(DropdownLazyLoading), }; 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: storybookDocsParameter(DropdownExternalFilteringAndLazyLoading), }; export const StaticCategories: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown }, props: Object.keys(argTypes), setup(props) { const selectedValue = ref(props.modelValue); 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(DropdownCustomDisplayValue), };
|
|
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'; const DropdownWithDescriptionExample = "<template>\n <bento-dropdown\n v-model=\"modelValue\"\n aria-label=\"Descriptive definition of what the dropdown is for\"\n description=\"Description of the field\"\n dynamic-filtering\n :multiple=\"false\"\n :items=\"items\"\n :is-option-disabled=\"isOptionDisabled\"\n label=\"Dropdown input label\"\n placeholder=\"Dropdown placeholder text\"\n enable-value-label-pair\n />\n</template>\n<script setup lang=\"ts\">\n import {\n BentoDropdown,\n type BentoListboxOptionItem,\n type BentoListboxOptions,\n type BentoListboxSelectedValueLabelPair,\n } from '@adyen/bento-vue2';\n import { ref } from 'vue';\n\n const items: BentoListboxOptions = [\n { label: 'Option 1', value: 1, description: 'Description 1' },\n { label: 'Option 2', value: 2, description: 'Description 2' },\n { label: 'Option 3', value: 3, description: 'Description 3' },\n { label: 'Option 4', value: 4, description: 'Description 4' },\n ];\n\n // Select the first option as Value / Label pair\n const modelValue = ref<BentoListboxSelectedValueLabelPair>(items[0]);\n\n // Disable individual options\n const isOptionDisabled = (option: BentoListboxOptionItem) => option.value === 3;\n</script>\n"; const DropdownWithCustomSlotsExample = "<template>\n <bento-dropdown\n v-model=\"modelValue\"\n aria-label=\"Descriptive definition of what the dropdown is for\"\n description=\"Description of the field\"\n :multiple=\"false\"\n :items=\"items\"\n label=\"Dropdown input label\"\n placeholder=\"Dropdown placeholder text\"\n enable-value-label-pair\n >\n <!-- Define how the display value should be rendered -->\n <template #display-value=\"{ label, value }\">\n <bento-typography>{{ label }} : {{ value }}</bento-typography>\n </template>\n\n <!-- Define how the items should be rendered -->\n <template #default=\"{ label, description }\">\n <bento-typography>{{ label }} : {{ description }}</bento-typography>\n </template>\n </bento-dropdown>\n</template>\n<script setup lang=\"ts\">\n import {\n BentoDropdown,\n type BentoListboxOptions,\n type BentoListboxSelectedValueLabelPair,\n } from '@adyen/bento-vue2';\n import { ref } from 'vue';\n\n const items: BentoListboxOptions = [\n { label: 'Option 1', value: 1, description: 'Description 1' },\n { label: 'Option 2', value: 2, description: 'Description 2' },\n { label: 'Option 3', value: 3, description: 'Description 3' },\n { label: 'Option 4', value: 4, description: 'Description 4' },\n ];\n\n // Select the first option as Value / Label pair\n const modelValue = ref<BentoListboxSelectedValueLabelPair>(items[0]);\n</script>\n"; const DropdownNestedMultiSelectWithLazyLoadExample = "<template>\n <bento-dropdown\n v-model=\"selectedValue\"\n :items=\"items\"\n :is-option-disabled=\"isOptionDisabled\"\n :has-more-items=\"hasMoreItems\"\n description=\"Choose one or more options\"\n label=\"Dropdown input label\"\n multiple\n lazy-load-type=\"button\"\n enable-value-label-pair\n :loading=\"isLoadingData\"\n @show-more=\"onShowMore\"\n />\n</template>\n\n<script setup lang=\"ts\">\n import { ref } from 'vue';\n import {\n BentoDropdown,\n type BentoListboxLazyLoadEvent,\n type BentoListboxOptionItem,\n type BentoListboxOptions,\n type BentoListboxSelectedValue,\n } from '@adyen/bento-vue2';\n\n // Control main list lazy load\n const hasMoreItems = ref(true);\n const isLoadingData = ref(false);\n // All values must be on the list (lazy loaded or not)\n\n const selectedValue = ref<BentoListboxSelectedValue>([\n // Initially loaded files\n { value: 1, label: 'Option 1' },\n // Lazy loaded category and all its options\n { value: 'c1', label: 'Category 1' },\n // Global lazy loaded values\n { value: 100, label: 'Option 100' },\n ]);\n\n // Fetch nested item lazy loaded items\n const nestedLazyLoadEvent: BentoListboxLazyLoadEvent = ({ value, index }) => {\n // eslint-disable-next-line no-console\n console.log(`Fetching data for: Option ${value}. Index: ${index}`);\n\n // Fetching data for option with value and index\n return new Promise(resolve => {\n const fetchedData = [\n {\n label: 'Option 6',\n description: 'C1-O6',\n value: 6,\n },\n {\n label: 'Option 7',\n description: 'C1-O7',\n value: 7,\n },\n ] satisfies BentoListboxOptions;\n\n // Append the newly fetched items to the nested list of items\n items.value[1].lazyLoad.hasMoreItems = false;\n items.value[1].items = [...items.value[1].items, ...fetchedData];\n\n return resolve();\n });\n };\n\n const items = ref<BentoListboxOptions>([\n {\n label: 'Option 1',\n description: 'Uncategorized',\n value: 1,\n },\n {\n label: 'Category 1',\n description: 'C1',\n value: 'c1',\n items: [\n {\n label: 'Option 2',\n description: 'C1-O2',\n value: 2,\n },\n {\n label: 'Option 3',\n description: 'C1-O3',\n value: 3,\n },\n ],\n lazyLoad: {\n hasMoreItems: true,\n lazyLoadEvent: nestedLazyLoadEvent,\n },\n },\n {\n label: 'Category 2',\n description: 'C2',\n value: 'c2',\n items: [\n {\n label: 'Option 4',\n description: 'C2-O4',\n value: 4,\n },\n {\n label: 'Option 5',\n description: 'C2-O5',\n value: 5,\n },\n ],\n },\n ]);\n\n // Load the items of the main list\n const onShowMore = () => {\n isLoadingData.value = true;\n // Append the newly fetched items to the nested list of items\n items.value.push({\n label: 'Option 100',\n description: 'Lazy loaded',\n value: 100,\n });\n\n isLoadingData.value = false;\n hasMoreItems.value = false;\n };\n\n const isOptionDisabled = (option: BentoListboxOptionItem) => [4, 5].includes(option.value as number);\n</script>\n"; const DropdownCustomDisplayValue = "<template>\n <bento-dropdown\n v-model=\"selectedValue\"\n description=\"Choose an option\"\n :items=\"items\"\n label=\"Label\"\n placeholder=\"Select an option\"\n />\n</template>\n\n<script setup lang=\"ts\">\n import { BentoDropdown, type BentoListboxOptions, type BentoListboxSelectedValue } from '@adyen/bento-vue2';\n import { ref } from 'vue';\n\n const items: BentoListboxOptions = [\n { label: 'Netherlands +31', displayValue: 'NL (+31)', value: 'NL' },\n { label: 'Belgium +32', displayValue: 'BE (+32)', value: 'BE' },\n { label: 'Germany +49', displayValue: 'DE (+49)', value: 'DE' },\n ];\n const selectedValue = ref<BentoListboxSelectedValue>('NL');\n</script>\n"; const DropdownDefault = "<template>\n <bento-dropdown\n v-model=\"selectedValue\"\n description=\"Choose an option\"\n :is-option-disabled=\"isOptionDisabled\"\n :items=\"items\"\n label=\"Label\"\n placeholder=\"Select an option\"\n enable-value-label-pair\n />\n</template>\n\n<script setup lang=\"ts\">\n import {\n BentoDropdown,\n type BentoListboxOptionItem,\n type BentoListboxOptions,\n type BentoListboxSelectedValue,\n } from '@adyen/bento-vue2';\n import { ref } from 'vue';\n\n const items: BentoListboxOptions = Array.from({ length: 10 }, (_, index) => ({\n label: `Option ${index + 1}`,\n value: index + 1,\n }));\n const selectedValue = ref<BentoListboxSelectedValue>({ label: 'Option 1', value: 1 });\n const isOptionDisabled = (option: BentoListboxOptionItem) => option.value === 3;\n</script>\n"; const DropdownExternalFiltering = "<template>\n <bento-dropdown\n v-model=\"selectedValue\"\n :component-loading=\"isSearching\"\n description=\"Type to simulate fetching new items.\"\n dynamic-filtering\n :items=\"items\"\n label=\"Dropdown with external filtering\"\n multiple\n placeholder=\"Select an option\"\n :search=\"search\"\n enable-value-label-pair\n />\n</template>\n\n<script setup lang=\"ts\">\n import {\n BentoDropdown,\n type BentoDropdownSearchOptions,\n type BentoListboxOptions,\n type BentoListboxSelectedValue,\n } from '@adyen/bento-vue2';\n import { computed, ref } from 'vue';\n\n const initialItems: BentoListboxOptions = Array.from({ length: 10 }, (_, index) => ({\n label: `Option ${index + 1}`,\n value: index + 1,\n }));\n const isSearching = ref(false);\n const items = ref(initialItems);\n const selectedValue = ref<BentoListboxSelectedValue>([]);\n\n const search = computed<BentoDropdownSearchOptions>(() => ({\n debounceTime: 500,\n searchEvent: async value => {\n isSearching.value = true;\n await new Promise(resolve => setTimeout(resolve, 1000));\n items.value = value\n ? Array.from({ length: 5 }, (_, index) => ({\n label: `${value} ${index + 1}`,\n value: `${value}-${index + 1}`,\n }))\n : initialItems;\n isSearching.value = false;\n },\n }));\n</script>\n"; const DropdownExternalFilteringAndLazyLoading = "<template>\n <bento-dropdown\n v-model=\"selectedValue\"\n :component-loading=\"isComponentLoading\"\n description=\"Search for items and scroll to load more results.\"\n dynamic-filtering\n :has-more-items=\"hasMoreItems\"\n :items=\"items\"\n label=\"External Filtering & Lazy Loading\"\n lazy-load-type=\"automatic\"\n :loading=\"isLazyLoading\"\n multiple\n placeholder=\"Select an option\"\n :search=\"search\"\n enable-value-label-pair\n @show-more=\"loadMore\"\n />\n</template>\n\n<script setup lang=\"ts\">\n import {\n BentoDropdown,\n type BentoDropdownSearchOptions,\n type BentoListboxOptions,\n type BentoListboxSelectedValue,\n } from '@adyen/bento-vue2';\n import { computed, onMounted, ref } from 'vue';\n\n const allItems: BentoListboxOptions = Array.from({ length: 1000 }, (_, index) => ({\n label: `Item ${index + 1}`,\n value: index + 1,\n }));\n const currentPage = ref(1);\n const currentSearchTerm = ref('');\n const hasMoreItems = ref(true);\n const isComponentLoading = ref(false);\n const isLazyLoading = ref(false);\n const items = ref<BentoListboxOptions>([]);\n const selectedValue = ref<BentoListboxSelectedValue>([{ label: 'Item 2000', value: 2000 }]);\n const pageSize = 20;\n\n const fetchItems = async (searchTerm: string, page: number) => {\n await new Promise(resolve => setTimeout(resolve, 500));\n const filtered = allItems.filter(item => item.label.toLowerCase().includes(searchTerm.toLowerCase()));\n const end = page * pageSize;\n return { hasMore: end < filtered.length, items: filtered.slice(end - pageSize, end) };\n };\n const runSearch = async (searchTerm: string) => {\n currentPage.value = 1;\n currentSearchTerm.value = searchTerm;\n isComponentLoading.value = true;\n const result = await fetchItems(searchTerm, currentPage.value);\n items.value = result.items;\n hasMoreItems.value = result.hasMore;\n isComponentLoading.value = false;\n };\n const loadMore = async () => {\n if (isLazyLoading.value || !hasMoreItems.value) {\n return;\n }\n isLazyLoading.value = true;\n const result = await fetchItems(currentSearchTerm.value, ++currentPage.value);\n items.value = [...items.value, ...result.items];\n hasMoreItems.value = result.hasMore;\n isLazyLoading.value = false;\n };\n const search = computed<BentoDropdownSearchOptions>(() => ({\n debounceTime: 500,\n searchEvent: runSearch,\n }));\n\n onMounted(() => runSearch(''));\n</script>\n"; const DropdownLazyLoading = "<template>\n <bento-dropdown\n v-model=\"selectedValue\"\n description=\"Scroll down or click the button to load more items.\"\n :has-more-items=\"hasMoreItems\"\n :items=\"items\"\n label=\"Lazy Loading\"\n lazy-load-type=\"automatic\"\n :loading=\"isLoading\"\n multiple\n placeholder=\"Select an option\"\n enable-value-label-pair\n @show-more=\"loadMore\"\n />\n</template>\n\n<script setup lang=\"ts\">\n import { BentoDropdown, type BentoListboxOptions, type BentoListboxSelectedValue } from '@adyen/bento-vue2';\n import { onMounted, ref } from 'vue';\n\n const allItems: BentoListboxOptions = Array.from({ length: 100 }, (_, index) => ({\n label: `Item ${index + 1}`,\n value: index + 1,\n }));\n const currentPage = ref(1);\n const hasMoreItems = ref(true);\n const isLoading = ref(false);\n const items = ref<BentoListboxOptions>([]);\n const selectedValue = ref<BentoListboxSelectedValue>([]);\n const pageSize = 20;\n\n const fetchItems = async (page: number) => {\n await new Promise(resolve => setTimeout(resolve, 500));\n const end = page * pageSize;\n return { hasMore: end < allItems.length, items: allItems.slice(end - pageSize, end) };\n };\n const loadMore = async () => {\n if (isLoading.value || !hasMoreItems.value) {\n return;\n }\n isLoading.value = true;\n const result = await fetchItems(++currentPage.value);\n items.value = [...items.value, ...result.items];\n hasMoreItems.value = result.hasMore;\n isLoading.value = false;\n };\n onMounted(async () => {\n isLoading.value = true;\n const result = await fetchItems(currentPage.value);\n items.value = result.items;\n hasMoreItems.value = result.hasMore;\n isLoading.value = false;\n });\n</script>\n"; const DropdownMultiSelect = "<template>\n <bento-dropdown\n v-model=\"selectedValue\"\n description=\"Choose one or more options\"\n :is-option-disabled=\"isOptionDisabled\"\n :items=\"items\"\n label=\"Label\"\n multiple\n placeholder=\"Select an option\"\n enable-value-label-pair\n />\n</template>\n\n<script setup lang=\"ts\">\n import {\n BentoDropdown,\n type BentoListboxOptionItem,\n type BentoListboxOptions,\n type BentoListboxSelectedValue,\n } from '@adyen/bento-vue2';\n import { ref } from 'vue';\n\n const items: BentoListboxOptions = Array.from({ length: 10 }, (_, index) => ({\n label: `Option ${index + 1}`,\n value: index + 1,\n }));\n const selectedValue = ref<BentoListboxSelectedValue>([\n { label: 'Option 1', value: 1 },\n { label: 'Option 3', value: 3 },\n { label: 'Option 100', value: 100 },\n ]);\n const isOptionDisabled = (option: BentoListboxOptionItem) => [3, 5, 7, 9].includes(option.value as number);\n</script>\n"; // 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 = { clearable: false, 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 }, }, // 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', }, }, }, }; export default meta; type Story = StoryObj<typeof BentoDropdown>; 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(DropdownDefault), }; 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(DropdownMultiSelect), }; 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.modelValue); 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), }; 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: storybookDocsParameter(DropdownExternalFiltering), }; 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: storybookDocsParameter(DropdownLazyLoading), }; 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: storybookDocsParameter(DropdownExternalFilteringAndLazyLoading), }; export const StaticCategories: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown }, props: Object.keys(argTypes), setup(props) { const selectedValue = ref(props.modelValue); 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(DropdownCustomDisplayValue), };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import type { BentoListboxEmptyStateProps as BentoDropdownEmptyStateProps, BentoListboxSearchOptions as BentoDropdownSearchOptions, BentoListboxVirtulisationOptions as BentoDropdownVirtulisationOptions, BentoListboxIsOptionDisabled, BentoListboxLazyLoadType, BentoListboxOptions, BentoListboxSelectedValue, } from '../../types/listbox'; import { type FormField } from '../../types/form-field'; import type { BentoTeleportProps } from '../internal/teleport/teleport.types'; export type { BentoDropdownSearchOptions, BentoDropdownEmptyStateProps, BentoDropdownVirtulisationOptions }; export interface BentoDropdownProps extends FormField { /** * Renders the dropdown with a condensed style. */ condensed?: boolean; /** * Empty state props used with the inner empty state component which is displayed when no search results are found. */ emptyState?: BentoDropdownEmptyStateProps; /** * Sets the possibility to type into the textbox. */ dynamicFiltering?: boolean; /** * Indicates whether there are more items to load */ hasMoreItems?: boolean; /** * Function that dertermines which options are disabled from interaction. * @type {BentoListboxIsOptionDisabled} * @param {BentoListboxOptionItem} option - Dropdown option object * @param {string} option.label - Option's label text * @param {string|number} option.value - Option's value * @param {unknown} option.data - Option's extra data to be passed to the default slot */ isOptionDisabled?: BentoListboxIsOptionDisabled; /** * The option elements to populate the dropdown with. * It must be an array of {@see BentoListboxOptionItem } * @property value.label - Text to be displayed in the option * @property value.value - Value of the option */ items?: BentoListboxOptions; /** * Indicates if new options are lazy loading. */ loading?: boolean; /** * Indicates that dropdown items are loading */ componentLoading?: boolean; /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button */ lazyLoadType?: BentoListboxLazyLoadType | `${BentoListboxLazyLoadType}`; /** * If enabled, the user will be able to select multiple items. */ multiple?: boolean; /** * Dropdown search options and events. * @see BentoDropdownSearchOptions */ search?: BentoDropdownSearchOptions; /** * Toggles whether to use the slot content select items shown in the textbox in multiple mode. */ showSlotContentInMultiple?: boolean; /** * If enabled, dropdown will display non-selectable, static categories */ staticCategories?: boolean; /** * Renders the dropdown options container in the root `body` DOM node * (or `dialog` if the dropdown is inside a dialog). */ teleport?: BentoTeleportProps; /** * The `input` value. * Providing an empty string will select no options. * Set to an empty string `''` if you don't want any of the available options to be selected. */ modelValue?: BentoListboxSelectedValue; /** * Enables virtual scrolling if set to true or by providing an object with itemHeight function. * The itemHeight function is used to calculate the height of rendered item given it's index. */ virtualScroll?: BentoDropdownVirtulisationOptions; /** * Enables value/label value handling. When `true`, the component expects and emits value/label * pair objects instead of primitive values. * * This affects both the `modelValue` prop and the `update:model-value` event payload. * * - **Input (`modelValue` prop):** Expects a `{ value: string | number, label: string }` object (or an array of them for multi-select). * - **Output (`update:model-value` event):** Emits a `{ value: string | number, label: string }` object (or an array of them) upon selection. * * This is particularly useful when a selected item's label is not available in the `items` prop, * allowing the component to display the correct label for values loaded from external sources. * @default false */ enableValueLabelPair?: boolean; } export { /** * @deprecated Since v3.0.0. Use string literal instead. */ BentoListboxLazyLoadType as BentoDropdownLazyLoadType, BentoListboxEvent as BentoDropdownEvent, } from '@/types/listbox'; export { /** * @deprecated Since v3.0.0. Do not use. Only the `default` size is supported. */ BentoDropdownSize, } from './dropdown.internal.types';
|
|
1
|
+
import type { BentoListboxEmptyStateProps as BentoDropdownEmptyStateProps, BentoListboxSearchOptions as BentoDropdownSearchOptions, BentoListboxVirtulisationOptions as BentoDropdownVirtulisationOptions, BentoListboxIsOptionDisabled, BentoListboxLazyLoadType, BentoListboxOptions, BentoListboxSelectedValue, } from '../../types/listbox'; import { type FormField } from '../../types/form-field'; import type { BentoTeleportProps } from '../internal/teleport/teleport.types'; export type { BentoDropdownSearchOptions, BentoDropdownEmptyStateProps, BentoDropdownVirtulisationOptions }; export interface BentoDropdownProps extends FormField { /** * Allows the selected value to be cleared in a single-select dropdown. */ clearable?: boolean; /** * Renders the dropdown with a condensed style. */ condensed?: boolean; /** * Empty state props used with the inner empty state component which is displayed when no search results are found. */ emptyState?: BentoDropdownEmptyStateProps; /** * Sets the possibility to type into the textbox. */ dynamicFiltering?: boolean; /** * Indicates whether there are more items to load */ hasMoreItems?: boolean; /** * Function that dertermines which options are disabled from interaction. * @type {BentoListboxIsOptionDisabled} * @param {BentoListboxOptionItem} option - Dropdown option object * @param {string} option.label - Option's label text * @param {string|number} option.value - Option's value * @param {unknown} option.data - Option's extra data to be passed to the default slot */ isOptionDisabled?: BentoListboxIsOptionDisabled; /** * The option elements to populate the dropdown with. * It must be an array of {@see BentoListboxOptionItem } * @property value.label - Text to be displayed in the option * @property value.value - Value of the option */ items?: BentoListboxOptions; /** * Indicates if new options are lazy loading. */ loading?: boolean; /** * Indicates that dropdown items are loading */ componentLoading?: boolean; /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button */ lazyLoadType?: BentoListboxLazyLoadType | `${BentoListboxLazyLoadType}`; /** * If enabled, the user will be able to select multiple items. */ multiple?: boolean; /** * Dropdown search options and events. * @see BentoDropdownSearchOptions */ search?: BentoDropdownSearchOptions; /** * Toggles whether to use the slot content select items shown in the textbox in multiple mode. */ showSlotContentInMultiple?: boolean; /** * If enabled, dropdown will display non-selectable, static categories */ staticCategories?: boolean; /** * Renders the dropdown options container in the root `body` DOM node * (or `dialog` if the dropdown is inside a dialog). */ teleport?: BentoTeleportProps; /** * The `input` value. * Providing an empty string will select no options. * Set to an empty string `''` if you don't want any of the available options to be selected. */ modelValue?: BentoListboxSelectedValue; /** * Enables virtual scrolling if set to true or by providing an object with itemHeight function. * The itemHeight function is used to calculate the height of rendered item given it's index. */ virtualScroll?: BentoDropdownVirtulisationOptions; /** * Enables value/label value handling. When `true`, the component expects and emits value/label * pair objects instead of primitive values. * * This affects both the `modelValue` prop and the `update:model-value` event payload. * * - **Input (`modelValue` prop):** Expects a `{ value: string | number, label: string }` object (or an array of them for multi-select). * - **Output (`update:model-value` event):** Emits a `{ value: string | number, label: string }` object (or an array of them) upon selection. * * This is particularly useful when a selected item's label is not available in the `items` prop, * allowing the component to display the correct label for values loaded from external sources. * @default false */ enableValueLabelPair?: boolean; } export { /** * @deprecated Since v3.0.0. Use string literal instead. */ BentoListboxLazyLoadType as BentoDropdownLazyLoadType, BentoListboxEvent as BentoDropdownEvent, } from '@/types/listbox'; export { /** * @deprecated Since v3.0.0. Do not use. Only the `default` size is supported. */ BentoDropdownSize, } from './dropdown.internal.types';
|
|
@@ -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" v-bind="getRootAttributes($attrs)" 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" :aria-active-descendant="activeDescendantId" :ariaExpanded="isDropdownOpen" :aria-labelledby="label ? labelId : null" :aria-describedby="ariaDescribedBy" :aria-label="$attrs['aria-label'] ?? label ?? computedAriaLabelFallbackMessage" :aria-readonly="isReadOnly" :aria-required="required" :readonly="isReadOnly" :disabled="disabled" :display-value="displayValue" :dynamic-filtering="dynamicFiltering" :has-selected-value="cachedSelectedListboxOptions.length > 0" :is-invalid="!!errorMessage" :multiple="multiple" :open="isDropdownOpen" :selected-value-item="!isInputComponent ? selectedValueItem : undefined" :show-slot-content-in-multiple="showSlotContentInMultiple" @input="onSearchTermInput" @open="toggleDropdown" @close="toggleDropdown" > <template #autocomplete-suggestion> <template v-if="autocompleteSuggestionSuffix"> <span class="b-dropdown__autocomplete-hidden-prefix">{{ searchTerm }}</span >{{ autocompleteSuggestionSuffix }} </template> </template> <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="$attrs['aria-label'] ?? label ?? computedAriaLabelFallbackMessage" :active-descendant-id="activeDescendantId" :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, 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 { generateUid } from '@/utils/ts/generate-uid'; 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'; import { getListboxOptionId } from '@/utils/ts/get-listbox-option-id'; // Directives import { BentoClickOutsideDirective as vBentoClickOutsideDirective } from '@/directives/click-outside'; import { BentoKeyboardNavigationDirective as vBentoKeyboardNavigationDirective } from '@/directives'; // Types import { BentoDropdownEvent, type BentoDropdownProps } from './dropdown.types'; import { BentoDropdownSize } from './dropdown.internal.types'; import { BentoListboxEvent, type BentoListboxItemRole, type BentoListboxMultiSelectValue, type BentoListboxOptionItem, 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 getRootAttributes = (attributes: Record<string, unknown>) => Object.fromEntries(Object.entries(attributes).filter(([key]) => !key.startsWith('aria-'))); const hasSlot = useHasSlot(slots); const props = withDefaults(defineProps<BentoDropdownProps>(), { condensed: false, emptyState: undefined, disabled: false, dynamicFiltering: 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, staticCategories: false, teleport: () => ({ disabled: true }), tooltipText: null, modelValue: null, virtualScroll: false, enableValueLabelPair: false, }); const emit = defineEmits<{ /** * 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 shouldShowActiveDescendant = 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); const { cachedSelectedListboxOptions, cachedListboxSelectedValues, setCachedValues } = useCachedSelectedValues( toRef(props, 'items'), dropdownValue, toRef(props, 'multiple'), toRef(props, 'enableValueLabelPair') ); const ariaDescribedByAttribute = ref(attrs['aria-describedby']); // 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')); // Inject input field component key to change the dropdown size const isInputComponent = 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 }) => (dropdownValue.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; }); const firstEnabledFilteredItem = computed(() => filteredItems.value.find((item: BentoListboxOptionItem) => !props.isOptionDisabled?.(item)) ); const autocompleteSuggestion = computed(() => { if (props.multiple || !isSearching.value || !shouldShowActiveDescendant.value) { return ''; } return firstEnabledFilteredItem.value?.label ?? ''; }); const autocompleteSuggestionSuffix = computed(() => { if (!autocompleteSuggestion.value.toLocaleLowerCase().startsWith(searchTerm.value.toLocaleLowerCase())) { return ''; } return autocompleteSuggestion.value.slice(searchTerm.value.length); }); const activeDescendantId = computed(() => !props.multiple && isDropdownOpen.value && isSearching.value && shouldShowActiveDescendant.value && firstEnabledFilteredItem.value ? getListboxOptionId(dropdownOptionsContainerId, firstEnabledFilteredItem.value.value) : null ); const activeDescendantIndex = computed(() => activeDescendantId.value ? filteredItems.value.findIndex(({ value }) => value === firstEnabledFilteredItem.value?.value) : -1 ); const onSearchTermInput = (newSearchTerm: string) => { searchTerm.value = newSearchTerm; shouldShowActiveDescendant.value = true; }; // ARIA const ariaDescribedBy = computed( () => [ props.description ? descriptionId : null, ariaDescribedByAttribute.value, props.errorMessage ? errorId : null, ] .filter(Boolean) .join(' ') || null ); const listboxRef = computed(() => { const dropdownListboxRef = optionsContainerRef.value?.dropdownOptionsListboxRef; // Vue 3 exposes the ref object, while Vue 2 exposes the unwrapped DOM element. return (dropdownListboxRef?.listboxRef?.value ?? dropdownListboxRef?.listboxRef) 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 selectFirstMatchedOption = async () => { if (!firstEnabledFilteredItem.value) { return false; } await onOptionSelected([firstEnabledFilteredItem.value]); return true; }; const emitShowMoreEvent = () => { emit(BentoDropdownEvent.SHOW_MORE); }; const textboxKeyboardNavigationListeners = useTextboxKeyboardNavigation( { inputContainerRef, optionsContainerRef, selectedValueIndex, isDropdownOpen, isDynamicFltering: toRef(props, 'dynamicFiltering'), items: visibleDomOptions, isMultiple: toRef(props, 'multiple'), isSearching, activeDescendantIndex, selectFirstMatchedOption, totalItemCount: computed(() => filteredItems.value.length), isVirtualScroll: !props?.multiple && props?.virtualScroll !== false && props?.lazyLoadType === 'none', removeActiveDescendant: () => { // TODO: this solution is temporary. when we target dynamicFiltering accessibility, the component should use aria-activedescendant to navigate the options instead of focus shouldShowActiveDescendant.value = false; }, }, toggleDropdown, openDropdown, closeDropdown ); const listboxKeyboardNavigationListeners = useListboxKeyboardNavigation( { inputContainerRef, isDropdownOpen, }, closeDropdown ); const listboxListeners = { [BentoListboxEvent.CLOSE_DROPDOWN]: closeDropdown, [BentoListboxEvent.SELECT]: onOptionSelected, [BentoListboxEvent.SHOW_MORE]: emitShowMoreEvent, ...listboxKeyboardNavigationListeners, }; 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: 'bento-dropdown', components: { DropdownInputDefault, DropdownInputSmall, }, inheritAttrs: false, model: { prop: 'modelValue', event: 'update:model-value' }, }); </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" v-bind="getRootAttributes($attrs)" 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" :aria-active-descendant="activeDescendantId" :ariaExpanded="isDropdownOpen" :aria-labelledby="label ? labelId : null" :aria-describedby="ariaDescribedBy" :aria-label="$attrs['aria-label'] ?? label ?? computedAriaLabelFallbackMessage" :aria-readonly="isReadOnly" :aria-required="required" :clearable="clearable && !required" :readonly="isReadOnly" :disabled="disabled" :display-value="displayValue" :dynamic-filtering="dynamicFiltering" :has-selected-value="cachedSelectedListboxOptions.length > 0" :is-invalid="!!errorMessage" :multiple="multiple" :open="isDropdownOpen" :selected-value-item="!isInputComponent ? selectedValueItem : undefined" :show-slot-content-in-multiple="showSlotContentInMultiple" @input="onSearchTermInput" @open="toggleDropdown" @close="toggleDropdown" @clear-selection="onClearSelection" > <template #autocomplete-suggestion> <template v-if="autocompleteSuggestionSuffix"> <span class="b-dropdown__autocomplete-hidden-prefix">{{ searchTerm }}</span >{{ autocompleteSuggestionSuffix }} </template> </template> <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="$attrs['aria-label'] ?? label ?? computedAriaLabelFallbackMessage" :active-descendant-id="activeDescendantId" :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, 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 { generateUid } from '@/utils/ts/generate-uid'; 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'; import { getListboxOptionId } from '@/utils/ts/get-listbox-option-id'; // Directives import { BentoClickOutsideDirective as vBentoClickOutsideDirective } from '@/directives/click-outside'; import { BentoKeyboardNavigationDirective as vBentoKeyboardNavigationDirective } from '@/directives'; // Types import { BentoDropdownEvent, type BentoDropdownProps } from './dropdown.types'; import { BentoDropdownSize } from './dropdown.internal.types'; import { BentoListboxEvent, type BentoListboxItemRole, type BentoListboxMultiSelectValue, type BentoListboxOptionItem, 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 getRootAttributes = (attributes: Record<string, unknown>) => Object.fromEntries(Object.entries(attributes).filter(([key]) => !key.startsWith('aria-'))); const hasSlot = useHasSlot(slots); const props = withDefaults(defineProps<BentoDropdownProps>(), { clearable: false, condensed: false, emptyState: undefined, disabled: false, dynamicFiltering: 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, staticCategories: false, teleport: () => ({ disabled: true }), tooltipText: null, modelValue: null, virtualScroll: false, enableValueLabelPair: false, }); const emit = defineEmits<{ /** * 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 shouldShowActiveDescendant = 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); const { cachedSelectedListboxOptions, cachedListboxSelectedValues, setCachedValues } = useCachedSelectedValues( toRef(props, 'items'), dropdownValue, toRef(props, 'multiple'), toRef(props, 'enableValueLabelPair') ); const ariaDescribedByAttribute = ref(attrs['aria-describedby']); // 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')); // Inject input field component key to change the dropdown size const isInputComponent = 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 }) => (dropdownValue.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; }); const firstEnabledFilteredItem = computed(() => filteredItems.value.find((item: BentoListboxOptionItem) => !props.isOptionDisabled?.(item)) ); const autocompleteSuggestion = computed(() => { if (props.multiple || !isSearching.value || !shouldShowActiveDescendant.value) { return ''; } return firstEnabledFilteredItem.value?.label ?? ''; }); const autocompleteSuggestionSuffix = computed(() => { if (!autocompleteSuggestion.value.toLocaleLowerCase().startsWith(searchTerm.value.toLocaleLowerCase())) { return ''; } return autocompleteSuggestion.value.slice(searchTerm.value.length); }); const activeDescendantId = computed(() => !props.multiple && isDropdownOpen.value && isSearching.value && shouldShowActiveDescendant.value && firstEnabledFilteredItem.value ? getListboxOptionId(dropdownOptionsContainerId, firstEnabledFilteredItem.value.value) : null ); const activeDescendantIndex = computed(() => activeDescendantId.value ? filteredItems.value.findIndex(({ value }) => value === firstEnabledFilteredItem.value?.value) : -1 ); const onSearchTermInput = (newSearchTerm: string) => { searchTerm.value = newSearchTerm; shouldShowActiveDescendant.value = true; }; const onClearSelection = async () => { setCachedValues([]); emitValue(null); await nextTick(); focus(); }; // ARIA const ariaDescribedBy = computed( () => [ props.description ? descriptionId : null, ariaDescribedByAttribute.value, props.errorMessage ? errorId : null, ] .filter(Boolean) .join(' ') || null ); const listboxRef = computed(() => { const dropdownListboxRef = optionsContainerRef.value?.dropdownOptionsListboxRef; // Vue 3 exposes the ref object, while Vue 2 exposes the unwrapped DOM element. return (dropdownListboxRef?.listboxRef?.value ?? dropdownListboxRef?.listboxRef) 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 selectFirstMatchedOption = async () => { if (!firstEnabledFilteredItem.value) { return false; } await onOptionSelected([firstEnabledFilteredItem.value]); return true; }; const emitShowMoreEvent = () => { emit(BentoDropdownEvent.SHOW_MORE); }; const textboxKeyboardNavigationListeners = useTextboxKeyboardNavigation( { inputContainerRef, optionsContainerRef, selectedValueIndex, isDropdownOpen, isDynamicFltering: toRef(props, 'dynamicFiltering'), items: visibleDomOptions, isMultiple: toRef(props, 'multiple'), isSearching, activeDescendantIndex, selectFirstMatchedOption, totalItemCount: computed(() => filteredItems.value.length), isVirtualScroll: !props?.multiple && props?.virtualScroll !== false && props?.lazyLoadType === 'none', removeActiveDescendant: () => { // TODO: this solution is temporary. when we target dynamicFiltering accessibility, the component should use aria-activedescendant to navigate the options instead of focus shouldShowActiveDescendant.value = false; }, }, toggleDropdown, openDropdown, closeDropdown ); const listboxKeyboardNavigationListeners = useListboxKeyboardNavigation( { inputContainerRef, isDropdownOpen, }, closeDropdown ); const listboxListeners = { [BentoListboxEvent.CLOSE_DROPDOWN]: closeDropdown, [BentoListboxEvent.SELECT]: onOptionSelected, [BentoListboxEvent.SHOW_MORE]: emitShowMoreEvent, ...listboxKeyboardNavigationListeners, }; 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: 'bento-dropdown', components: { DropdownInputDefault, DropdownInputSmall, }, inheritAttrs: false, model: { prop: 'modelValue', event: 'update:model-value' }, }); </script> <style lang="scss" scoped src="./dropdown.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div class="b-all-filters-modal-selected-filter" :class="conditionalClasses" @keydown.left="$emit('keydown-left')"> <div v-if="smallScreen || shouldShowFilterTitle" class="b-all-filters-modal-selected-filter__text"> <bento-typography :id="selectedFilter.labelId" el="div" variant="title" medium> {{ selectedFilter.label }} </bento-typography> <bento-typography v-if="selectedFilter.description" el="div" variant="body" wide> {{ selectedFilter.description }} </bento-typography> </div> <div class="b-all-filters-modal-selected-filter__component"> <component :is="selectedFilter.type"
|
|
1
|
+
<template> <div class="b-all-filters-modal-selected-filter" :class="conditionalClasses" @keydown.left="$emit('keydown-left')"> <div v-if="smallScreen || shouldShowFilterTitle" class="b-all-filters-modal-selected-filter__text"> <bento-typography :id="selectedFilter.labelId" el="div" variant="title" medium> {{ selectedFilter.label }} </bento-typography> <bento-typography v-if="selectedFilter.description" el="div" variant="body" wide> {{ selectedFilter.description }} </bento-typography> </div> <div ref="selectedFilterRef" class="b-all-filters-modal-selected-filter__component"> <component :is="selectedFilter.type" :key="selectedFilter.field" :description="selectedFilter.description" :field="selectedFilter.field" :label="selectedFilter.label" :options="selectedFilter.options" :value="selectedFilter.value" :default-value="selectedFilter.defaultValue" :only-slot="true" :disabled="selectedFilter.disabled" :aria-labelledby="selectedFilter.labelId" @input="$emit('input', $event)" /> </div> <bento-button-actions v-if="smallScreen" class="b-all-filters-modal-selected-filter__actions" :actions="actions" layout="vertical-stack" /> <div v-else class="b-all-filters-modal-selected-filter__clear-button"> <bento-button variant="secondary" :disabled="isClearButtonDisabled" @click="$emit('clear')"> {{ clearButtonLabel }} </bento-button> </div> </div> </template> <script setup lang="ts"> import { computed, onMounted, ref, watch } from 'vue'; import { BentoButton, BentoButtonActions, type BentoButtonActionsList } from '@/components/button'; import { BentoTypography } from '@/components/typography'; import { useEventListener, useMutationObserver } from '@vueuse/core'; import { getFocusableIn } from 'focus-lock'; import { type BentoFilterModel } from '@/components/filter-bar/filter-bar.types'; import { type BentoAllFiltersModalSelectedFilter } from '../../all-filters-modal.types'; const props = withDefaults( defineProps<{ /** Actions displayed below the selected filter on small screens. */ actions?: BentoButtonActionsList; /** Label for the clear or reset button. */ clearButtonLabel: string; /** Disables the clear or reset button. */ isClearButtonDisabled: boolean; /** Currently selected filter. */ selectedFilter: BentoAllFiltersModalSelectedFilter; /** Displays the small-screen variant. */ smallScreen?: boolean; /** Controls whether the selected filter title is displayed. */ shouldShowFilterTitle: boolean; }>(), { actions: () => [], smallScreen: false, } ); defineEmits<{ /** * Emitted when the selected filter is cleared */ (event: 'clear'): void; /** * Emitted when the selected filter value changes */ (event: 'input', filter: BentoFilterModel): void; /** * Emitted when the left arrow key is pressed */ (event: 'keydown-left'): void; }>(); const conditionalClasses = computed(() => ({ 'b-all-filters-modal-selected-filter--small-screen': props.smallScreen, })); const selectedFilterRef = ref<HTMLElement | null>(null); // Element holding the focus while waiting for the filter to render, `null` when nothing is pending. let pendingFocusSource: Element | null = null; const getFirstFocusable = () => { const selectedFilterElement = selectedFilterRef.value; return selectedFilterElement ? getFocusableIn(selectedFilterElement).find(({ node }) => node !== selectedFilterElement) : undefined; }; const focusPendingInput = () => { if (!pendingFocusSource || pendingFocusSource !== document.activeElement) { return; } const firstFocusable = getFirstFocusable(); if (firstFocusable) { pendingFocusSource = null; firstFocusable.node.focus(); } }; const focusFirstInput = () => { const firstFocusable = getFirstFocusable(); // Async filters render after the focus is requested, so the focus is retried once they appear. pendingFocusSource = firstFocusable ? null : document.activeElement; firstFocusable?.node.focus(); }; const cancelPendingFocus = () => { pendingFocusSource = null; }; // Initialize after mount so VueUse observes the populated template ref before an async filter renders. onMounted(() => useMutationObserver(selectedFilterRef, focusPendingInput, { childList: true, subtree: true })); useEventListener(document, 'focusin', () => { if (document.activeElement !== pendingFocusSource) { cancelPendingFocus(); } }); watch(() => props.selectedFilter.field, cancelPendingFocus); defineExpose({ focusFirstInput }); </script> <script lang="ts"> import { filterBarFilterComponents } from '@/components/filter-bar/filter-bar-filter-components'; export default { name: 'all-filters-modal-selected-filter', components: filterBarFilterComponents, }; </script> <style lang="scss" scoped src="./all-filters-modal-selected-filter.scss" />
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import type { Component } from 'vue'; export type BentoAsyncFilterLoader = () => Promise<Component | { default: Component }>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div ref="filterBarRef" class="b-filter-bar" :class="rootElementConditionalClasses"> <div v-if="!showSearchbarInsideContainer && isSearchBarEnabled" class="b-filter-bar__search-external-container"> <bento-search-bar class="b-filter-bar__search-bar" v-bind="searchBarProps" :input-field-aria-label="searchBarAriaLabel" :value="searchTerm" condensed :style="searchBarWidthStyle" @input="onSearchbarInput" @clear="onSearchbarClear" /> </div> <component :is="screenLayoutComponent" :class="containerConditionalClasses" condensed> <div :class="contentConditionalClasses"> <div v-if="showSearchbarInsideContainer && isSearchBarEnabled" class="b-filter-bar__search-bar-container" :style="searchBarWidthStyle" > <bento-search-bar class="b-filter-bar__search-bar" v-bind="searchBarProps" :input-field-aria-label="searchBarAriaLabel" :value="searchTerm" condensed @input="onSearchbarInput" @clear="onSearchbarClear" /> </div> <template v-if="persistentFilters.length"> <component :is="filter.type" v-for="filter in persistentFilters" :key="filter.field" :field="filter.field" :label="filter.label" :disabled="filter?.disabled" :options="filter.options" :value="filter.value" :default-value="filter.defaultValue" @update="updateFilterHandler" /> </template> <component :is="filter.type" v-for="filter in regularFilters" :key="filter.field" :field="filter.field" :label="filter.label" :disabled="filter?.disabled" :options="filter.options" :value="filter.value" @update="updateFilterHandler" /> <!-- All filters button --> <all-filters-modal v-if="hasHiddenFilters" class="b-filter-bar__all-filters-modal" :value="filtersInAllFiltersModal" @input="updateFilterHandler" /> <!-- Clear all filters button --> <filter-bar-button v-if="isClearAllVisible" class="b-filter-bar__clear-all" @click="clearAllFilters"> {{ t('clearFilters') }} <template #iconLeft> <refresh-icon aria-hidden="true" /> </template> </filter-bar-button> <!-- Reset all filters button --> <filter-bar-button v-if="isResetAllVisible" class="b-filter-bar__reset-all" @click="resetAllFilters"> {{ t('resetFilters') }} <template #iconLeft> <refresh-icon aria-hidden="true" /> </template> </filter-bar-button> </div> </component> </div> </template> <script setup lang="ts"> import { computed, ref } from 'vue'; import { FilterBarButton } from './components/filter-bar-button'; import { type BentoFilterBarContainerSizeLayout, BentoFilterBarEvent, type BentoFilterBarFiltersWithValues, type BentoFilterBarFilterWithValue, type BentoFilterBarProps, } from './filter-bar.types'; import BentoSearchBar from '@/components/search-bar/search-bar.vue'; import { BentoSearchBarEvent } from '@/components/search-bar/search-bar.types'; import { useBentoToastController } from '@/composables'; import { useI18n } from '@/utils/ts/i18n'; import { useContainerSizeOptionsLayout } from './composables/use-container-size-options-layout/use-container-size-options-layout'; import AllFiltersModal from './components/all-filters-modal/all-filters-modal.vue'; import RefreshIcon from '@adyen/ui-assets-icons-16/vue/refresh'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; // TODO: setup Vue3 generics in Bento 2.0.0 const props = withDefaults(defineProps<BentoFilterBarProps>(), { config: () => [], filterValues: () => [], containerSizeLayout: null, search: undefined, searchTerm: undefined, showAppliedHiddenFilters: false, filterValuesObject: undefined, }); const emit = defineEmits([ BentoFilterBarEvent.INPUT, BentoFilterBarEvent.UPDATE_FILTER_VALUES_OBJECT, BentoFilterBarEvent.UPDATE_FILTER_VALUES_ARRAY, BentoSearchBarEvent.CLEAR, BentoSearchBarEvent.INPUT, BentoSearchBarEvent.UPDATE, ]); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const filterBarRef = ref<HTMLDivElement>(null); const valueAndFilterValuesPropsMerged = computed( () => props.config?.map(filterModel => { let filterValue = props.filterValues.find(({ field }) => field === filterModel.field)?.value; if (props.filterValuesObject && filterModel.field in props.filterValuesObject) { filterValue = props.filterValuesObject[filterModel.field]; } if (filterValue === undefined) { filterValue = filterModel?.defaultValue; } return { ...filterModel, value: filterValue }; }) as BentoFilterBarFiltersWithValues ); const { addToast } = useBentoToastController(); /** * Container size layout. * Allows to change the layout from multi-line to one-line for each container size type: * - large: >=700 * - medium: >=430 & <700 * - small: <430 */ const DEFAULT_CONTAINER_SIZE_LAYOUT: Required<BentoFilterBarContainerSizeLayout> = { large: 'multi-line', medium: 'one-line', small: 'multi-line', }; const mergedContainerSizeLayout = computed<Required<BentoFilterBarContainerSizeLayout>>(() => ({ ...DEFAULT_CONTAINER_SIZE_LAYOUT, ...props.containerSizeLayout, })); const { screenLayoutComponent, showSearchbarInsideContainer, isOneLineLayout, isLargeLayout, rootElementConditionalClasses, } = useContainerSizeOptionsLayout(filterBarRef, mergedContainerSizeLayout); /** * Classes */ /** * Handles the classes of the swappable component ('div' or 'fixed-scroller) */ const containerConditionalClasses = computed(() => ({ 'b-filter-bar__fixed-scroller': isOneLineLayout.value, 'b-filter-bar__container': !isOneLineLayout.value, // Multi-line })); /** * Handles the classes of the container inside the swappable component. */ const contentConditionalClasses = computed(() => ({ 'b-filter-bar__scroller-content': isOneLineLayout.value, 'b-filter-bar__filters': !isOneLineLayout.value, // Multi-line })); /** * Search bar pops */ const searchBarAriaLabel = computed(() => props.search?.inputFieldAriaLabel || (t('searchBar') as string)); const searchBarProps = computed(() => { if (!props.search) { return undefined; } const { debounceTime, disabled, inputFieldAriaLabel, hint, placeholder } = props.search; return { debounceTime, disabled, inputFieldAriaLabel, hint, placeholder }; }); const searchBarWidthStyle = computed(() => props.search?.width && isLargeLayout.value ? { flexBasis: props.search.width, maxWidth: props.search.width } : undefined ); const hasVisibleSet = computed(() => valueAndFilterValuesPropsMerged.value.some(({ visible }) => visible !== undefined && visible !== null) ); const hasHiddenFilters = computed(() => hasVisibleSet.value ? valueAndFilterValuesPropsMerged.value.some(({ visible }) => !visible) : false ); // Persistent filters i.e. filters with default value const persistentFilters = computed(() => valueAndFilterValuesPropsMerged.value.filter( ({ defaultValue, visible }) => defaultValue !== undefined && visible !== false ) ); const filtersInAllFiltersModal = computed(() => valueAndFilterValuesPropsMerged.value.filter( ({ defaultValue, visible }) => defaultValue === undefined || visible === false ) ); const hasPersistentFilters = computed(() => valueAndFilterValuesPropsMerged.value.some(({ defaultValue }) => defaultValue !== undefined) ); // Show non-persistent & visible filters const regularFilters = computed(() => { // If no filter has the `visible` prop set then all are visible, otherwise search for the "visible: true" elements return hasVisibleSet.value ? valueAndFilterValuesPropsMerged.value.filter( ({ defaultValue, visible, value }) => defaultValue === undefined && (visible || (props.showAppliedHiddenFilters && value !== undefined && value !== null)) ) : valueAndFilterValuesPropsMerged.value.filter(({ defaultValue }) => defaultValue === undefined); }); // Enable search bar. // Soft equality to check for "null" and "undefined" // eslint-disable-next-line eqeqeq const isSearchBarEnabled = computed(() => props.searchTerm != undefined); const hasActiveFilters = computed(() => valueAndFilterValuesPropsMerged.value .filter(({ defaultValue }) => defaultValue === undefined || defaultValue === null) // Disabling strict equality to check for both undefined and null // eslint-disable-next-line eqeqeq .some(({ value }) => value != undefined) ); // Show the "Clear all" button (true) if at least one filter is active (contains a value) // and there are not persistent filters present. const isClearAllVisible = computed(() => !hasPersistentFilters.value && hasActiveFilters.value); const isResetAllVisible = computed(() => { if (!hasPersistentFilters.value) { return false; } const areAllValuesEqualToDefaults = valueAndFilterValuesPropsMerged.value .filter(({ defaultValue }) => defaultValue !== undefined || defaultValue !== null) .every(({ value, defaultValue }) => JSON.stringify(value) === JSON.stringify(defaultValue)); return !areAllValuesEqualToDefaults || hasActiveFilters.value; }); const updateAllFilters = (newFilters: BentoFilterBarFiltersWithValues) => { emit(BentoFilterBarEvent.INPUT, newFilters); emit( BentoFilterBarEvent.UPDATE_FILTER_VALUES_ARRAY, newFilters ?.filter(({ value }) => value !== null || value !== undefined) .map(({ field, value }) => ({ field, value })) ); emit( BentoFilterBarEvent.UPDATE_FILTER_VALUES_OBJECT, newFilters ?.filter(({ value }) => value !== null || value !== undefined) .reduce((acc, filter) => { acc[filter.field] = filter.value; return acc; }, {}) ); }; const updateFilterHandler = (updatedFilters: BentoFilterBarFilterWithValue | BentoFilterBarFiltersWithValues) => { const filtersArray = Array.isArray(updatedFilters) ? updatedFilters : [updatedFilters]; const updatedFiltersMap = new Map(filtersArray.map(filter => [filter.field, filter.value])); const newFilters = valueAndFilterValuesPropsMerged.value.map(filter => { if (updatedFiltersMap.has(filter.field)) { return { ...filter, value: updatedFiltersMap.get(filter.field) }; } return filter; }); updateAllFilters(newFilters); }; const clearAllFilters = () => { const previousFilters = valueAndFilterValuesPropsMerged.value.slice(); const clearedFilters = valueAndFilterValuesPropsMerged.value.map(({ value, ...otherFields }) => ({ ...otherFields, value: undefined, })); addToast({ text: t('filtersHaveBeenCleared') as string, action: { handler: () => updateAllFilters(previousFilters), text: t('undo') as string, }, }); updateAllFilters(clearedFilters); }; const resetAllFilters = () => { const previousFilters = valueAndFilterValuesPropsMerged.value.slice(); const resetFilters = valueAndFilterValuesPropsMerged.value.map(({ defaultValue, ...otherFields }) => ({ ...otherFields, defaultValue, value: defaultValue, })); addToast({ text: t('filtersHaveBeenReset') as string, action: { handler: () => updateAllFilters(previousFilters), text: t('undo') as string, }, }); updateAllFilters(resetFilters); }; const onSearchbarInput = (newSearchTerm: string) => { emit(BentoSearchBarEvent.INPUT, newSearchTerm); emit(BentoSearchBarEvent.UPDATE, newSearchTerm); }; const onSearchbarClear = () => { emit(BentoSearchBarEvent.CLEAR); }; // Expose methods for BentoDataGrid to access defineExpose({ isClearAllVisible, clearAllFilters, isResetAllVisible, resetAllFilters }); </script> <script lang="ts"> import { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoToggleFilter, } from './components'; import { BentoTeleport, FixedScroller } from '@/internal'; /** * The filter bar filters item lists and tables according to various filter criteria. * * @example * import { BentoFilterBar } from '@adyen/bento-vue2' * * export default { * components: { BentoFilterBar }, * data: () => ({ * filtersConfig: [{ * field: 'name', * label: 'Name', * options: { ... }, * type: FilterType.TEXT, * }], * filterValues: [{ * field: 'name', * value: null, * }], * }), * template: ` * <bento-filter-bar * :config="filtersConfig" * :filter-values="filterValues" * @update:filter-values="filterValues = $event" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-filter-bar', components: { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoTeleport, BentoToggleFilter, FixedScroller, }, }; </script> <style lang="scss" scoped src="./filter-bar.scss" />
|
|
1
|
+
<template> <div ref="filterBarRef" class="b-filter-bar" :class="rootElementConditionalClasses"> <div v-if="!showSearchbarInsideContainer && isSearchBarEnabled" class="b-filter-bar__search-external-container"> <bento-search-bar class="b-filter-bar__search-bar" v-bind="searchBarProps" :input-field-aria-label="searchBarAriaLabel" :value="searchTerm" condensed :style="searchBarWidthStyle" @input="onSearchbarInput" @clear="onSearchbarClear" /> </div> <component :is="screenLayoutComponent" :class="containerConditionalClasses" condensed> <div :class="contentConditionalClasses"> <div v-if="showSearchbarInsideContainer && isSearchBarEnabled" class="b-filter-bar__search-bar-container" :style="searchBarWidthStyle" > <bento-search-bar class="b-filter-bar__search-bar" v-bind="searchBarProps" :input-field-aria-label="searchBarAriaLabel" :value="searchTerm" condensed @input="onSearchbarInput" @clear="onSearchbarClear" /> </div> <template v-if="persistentFilters.length"> <component :is="filter.type" v-for="filter in persistentFilters" :key="filter.field" :field="filter.field" :label="filter.label" :disabled="filter?.disabled" :options="filter.options" :value="filter.value" :default-value="filter.defaultValue" @update="updateFilterHandler" /> </template> <component :is="filter.type" v-for="filter in regularFilters" :key="filter.field" :field="filter.field" :label="filter.label" :disabled="filter?.disabled" :options="filter.options" :value="filter.value" @update="updateFilterHandler" /> <!-- All filters button --> <all-filters-modal v-if="hasHiddenFilters" class="b-filter-bar__all-filters-modal" :value="filtersInAllFiltersModal" @input="updateFilterHandler" /> <!-- Clear all filters button --> <filter-bar-button v-if="isClearAllVisible" class="b-filter-bar__clear-all" @click="clearAllFilters"> {{ t('clearFilters') }} <template #iconLeft> <refresh-icon aria-hidden="true" /> </template> </filter-bar-button> <!-- Reset all filters button --> <filter-bar-button v-if="isResetAllVisible" class="b-filter-bar__reset-all" @click="resetAllFilters"> {{ t('resetFilters') }} <template #iconLeft> <refresh-icon aria-hidden="true" /> </template> </filter-bar-button> </div> </component> </div> </template> <script setup lang="ts"> import { computed, ref } from 'vue'; import { FilterBarButton } from './components/filter-bar-button'; import { type BentoFilterBarContainerSizeLayout, BentoFilterBarEvent, type BentoFilterBarFiltersWithValues, type BentoFilterBarFilterWithValue, type BentoFilterBarProps, } from './filter-bar.types'; import BentoSearchBar from '@/components/search-bar/search-bar.vue'; import { BentoSearchBarEvent } from '@/components/search-bar/search-bar.types'; import { useBentoToastController } from '@/composables'; import { useI18n } from '@/utils/ts/i18n'; import { useContainerSizeOptionsLayout } from './composables/use-container-size-options-layout/use-container-size-options-layout'; import AllFiltersModal from './components/all-filters-modal/all-filters-modal.vue'; import RefreshIcon from '@adyen/ui-assets-icons-16/vue/refresh'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; // TODO: setup Vue3 generics in Bento 2.0.0 const props = withDefaults(defineProps<BentoFilterBarProps>(), { config: () => [], filterValues: () => [], containerSizeLayout: null, search: undefined, searchTerm: undefined, showAppliedHiddenFilters: false, filterValuesObject: undefined, }); const emit = defineEmits([ BentoFilterBarEvent.INPUT, BentoFilterBarEvent.UPDATE_FILTER_VALUES_OBJECT, BentoFilterBarEvent.UPDATE_FILTER_VALUES_ARRAY, BentoSearchBarEvent.CLEAR, BentoSearchBarEvent.INPUT, BentoSearchBarEvent.UPDATE, ]); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const filterBarRef = ref<HTMLDivElement>(null); const valueAndFilterValuesPropsMerged = computed( () => props.config?.map(filterModel => { let filterValue = props.filterValues.find(({ field }) => field === filterModel.field)?.value; if (props.filterValuesObject && filterModel.field in props.filterValuesObject) { filterValue = props.filterValuesObject[filterModel.field]; } if (filterValue === undefined) { filterValue = filterModel?.defaultValue; } return { ...filterModel, value: filterValue }; }) as BentoFilterBarFiltersWithValues ); const { addToast } = useBentoToastController(); /** * Container size layout. * Allows to change the layout from multi-line to one-line for each container size type: * - large: >=700 * - medium: >=430 & <700 * - small: <430 */ const DEFAULT_CONTAINER_SIZE_LAYOUT: Required<BentoFilterBarContainerSizeLayout> = { large: 'multi-line', medium: 'one-line', small: 'multi-line', }; const mergedContainerSizeLayout = computed<Required<BentoFilterBarContainerSizeLayout>>(() => ({ ...DEFAULT_CONTAINER_SIZE_LAYOUT, ...props.containerSizeLayout, })); const { screenLayoutComponent, showSearchbarInsideContainer, isOneLineLayout, isLargeLayout, rootElementConditionalClasses, } = useContainerSizeOptionsLayout(filterBarRef, mergedContainerSizeLayout); /** * Classes */ /** * Handles the classes of the swappable component ('div' or 'fixed-scroller) */ const containerConditionalClasses = computed(() => ({ 'b-filter-bar__fixed-scroller': isOneLineLayout.value, 'b-filter-bar__container': !isOneLineLayout.value, // Multi-line })); /** * Handles the classes of the container inside the swappable component. */ const contentConditionalClasses = computed(() => ({ 'b-filter-bar__scroller-content': isOneLineLayout.value, 'b-filter-bar__filters': !isOneLineLayout.value, // Multi-line })); /** * Search bar pops */ const searchBarAriaLabel = computed(() => props.search?.inputFieldAriaLabel || (t('searchBar') as string)); const searchBarProps = computed(() => { if (!props.search) { return undefined; } const { debounceTime, disabled, inputFieldAriaLabel, hint, placeholder } = props.search; return { debounceTime, disabled, inputFieldAriaLabel, hint, placeholder }; }); const searchBarWidthStyle = computed(() => props.search?.width && isLargeLayout.value ? { flexBasis: props.search.width, maxWidth: props.search.width } : undefined ); const hasVisibleSet = computed(() => valueAndFilterValuesPropsMerged.value.some(({ visible }) => visible !== undefined && visible !== null) ); const hasHiddenFilters = computed(() => hasVisibleSet.value ? valueAndFilterValuesPropsMerged.value.some(({ visible }) => !visible) : false ); // Persistent filters i.e. filters with default value const persistentFilters = computed(() => valueAndFilterValuesPropsMerged.value.filter( ({ defaultValue, visible }) => defaultValue !== undefined && visible !== false ) ); const filtersInAllFiltersModal = computed(() => valueAndFilterValuesPropsMerged.value.filter( ({ defaultValue, visible }) => defaultValue === undefined || visible === false ) ); const hasPersistentFilters = computed(() => valueAndFilterValuesPropsMerged.value.some(({ defaultValue }) => defaultValue !== undefined) ); // Show non-persistent & visible filters const regularFilters = computed(() => { // If no filter has the `visible` prop set then all are visible, otherwise search for the "visible: true" elements return hasVisibleSet.value ? valueAndFilterValuesPropsMerged.value.filter( ({ defaultValue, visible, value }) => defaultValue === undefined && (visible || (props.showAppliedHiddenFilters && value !== undefined && value !== null)) ) : valueAndFilterValuesPropsMerged.value.filter(({ defaultValue }) => defaultValue === undefined); }); // Enable search bar. // Soft equality to check for "null" and "undefined" // eslint-disable-next-line eqeqeq const isSearchBarEnabled = computed(() => props.searchTerm != undefined); const hasActiveFilters = computed(() => valueAndFilterValuesPropsMerged.value .filter(({ defaultValue }) => defaultValue === undefined || defaultValue === null) // Disabling strict equality to check for both undefined and null // eslint-disable-next-line eqeqeq .some(({ value }) => value != undefined) ); // Show the "Clear all" button (true) if at least one filter is active (contains a value) // and there are not persistent filters present. const isClearAllVisible = computed(() => !hasPersistentFilters.value && hasActiveFilters.value); const isResetAllVisible = computed(() => { if (!hasPersistentFilters.value) { return false; } const areAllValuesEqualToDefaults = valueAndFilterValuesPropsMerged.value .filter(({ defaultValue }) => defaultValue !== undefined || defaultValue !== null) .every(({ value, defaultValue }) => JSON.stringify(value) === JSON.stringify(defaultValue)); return !areAllValuesEqualToDefaults || hasActiveFilters.value; }); const updateAllFilters = (newFilters: BentoFilterBarFiltersWithValues) => { emit(BentoFilterBarEvent.INPUT, newFilters); emit( BentoFilterBarEvent.UPDATE_FILTER_VALUES_ARRAY, newFilters ?.filter(({ value }) => value !== null || value !== undefined) .map(({ field, value }) => ({ field, value })) ); emit( BentoFilterBarEvent.UPDATE_FILTER_VALUES_OBJECT, newFilters ?.filter(({ value }) => value !== null || value !== undefined) .reduce((acc, filter) => { acc[filter.field] = filter.value; return acc; }, {}) ); }; const updateFilterHandler = (updatedFilters: BentoFilterBarFilterWithValue | BentoFilterBarFiltersWithValues) => { const filtersArray = Array.isArray(updatedFilters) ? updatedFilters : [updatedFilters]; const updatedFiltersMap = new Map(filtersArray.map(filter => [filter.field, filter.value])); const newFilters = valueAndFilterValuesPropsMerged.value.map(filter => { if (updatedFiltersMap.has(filter.field)) { return { ...filter, value: updatedFiltersMap.get(filter.field) }; } return filter; }); updateAllFilters(newFilters); }; const clearAllFilters = () => { const previousFilters = valueAndFilterValuesPropsMerged.value.slice(); const clearedFilters = valueAndFilterValuesPropsMerged.value.map(({ value, ...otherFields }) => ({ ...otherFields, value: undefined, })); addToast({ text: t('filtersHaveBeenCleared') as string, action: { handler: () => updateAllFilters(previousFilters), text: t('undo') as string, }, }); updateAllFilters(clearedFilters); }; const resetAllFilters = () => { const previousFilters = valueAndFilterValuesPropsMerged.value.slice(); const resetFilters = valueAndFilterValuesPropsMerged.value.map(({ defaultValue, ...otherFields }) => ({ ...otherFields, defaultValue, value: defaultValue, })); addToast({ text: t('filtersHaveBeenReset') as string, action: { handler: () => updateAllFilters(previousFilters), text: t('undo') as string, }, }); updateAllFilters(resetFilters); }; const onSearchbarInput = (newSearchTerm: string) => { emit(BentoSearchBarEvent.INPUT, newSearchTerm); emit(BentoSearchBarEvent.UPDATE, newSearchTerm); }; const onSearchbarClear = () => { emit(BentoSearchBarEvent.CLEAR); }; // Expose methods for BentoDataGrid to access defineExpose({ isClearAllVisible, clearAllFilters, isResetAllVisible, resetAllFilters }); </script> <script lang="ts"> import { BentoTeleport, FixedScroller } from '@/internal'; import { filterBarFilterComponents } from './filter-bar-filter-components'; /** * The filter bar filters item lists and tables according to various filter criteria. * * @example * import { BentoFilterBar } from '@adyen/bento-vue2' * * export default { * components: { BentoFilterBar }, * data: () => ({ * filtersConfig: [{ * field: 'name', * label: 'Name', * options: { ... }, * type: FilterType.TEXT, * }], * filterValues: [{ * field: 'name', * value: null, * }], * }), * template: ` * <bento-filter-bar * :config="filtersConfig" * :filter-values="filterValues" * @update:filter-values="filterValues = $event" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-filter-bar', components: { ...filterBarFilterComponents, BentoTeleport, FixedScroller, }, }; </script> <style lang="scss" scoped src="./filter-bar.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { type OnClickOutsideOptions } from '@/directives/click-outside/types'; import { PopperContainerPosition } from '../popper-container/popper-container.types'; import { type BentoMenuItem } from './components/menu-item/menu-item.types'; import { type HTMLAttributes } from 'vue'; import { type BentoButtonProps } from '../button/button.types'; /** * @deprecated Since v3.0.0. Use string literal instead. */ const BentoMenuPositions = { ...PopperContainerPosition }; type BentoMenuPositions = PopperContainerPosition; export type * from './components/menu-item/menu-item.types'; export enum BentoMenuEvent { BACK = 'back', CLOSE = 'close', OPEN = 'open', } export interface BentoMenuButtonProps { 'aria-label'?: HTMLAttributes['aria-label']; condensed?: boolean; disabled?: boolean; variant?: BentoButtonProps['variant']; } export interface BentoMenuProps { /** * Object containing props to change the menu button appearance. Accepts the same props present in BentoButton. */ button?: BentoMenuButtonProps; /** * Controls if the menu should close when an item is clicked. * Defaults to `true`. */ closeMenuOnItemSelect?: boolean; /** * The data to be displayed in the list */ data: Array<BentoMenuItem>; /** * Disables opening the menu when the down arrow key is pressed. */ disableArrowDownOpeningMenu?: boolean; /** * The position of where the menu should open. */ menuPosition?: BentoMenuPositions | `${BentoMenuPositions}`; /** * Instructs the menu popover to be visible outside of the containing container. * @see {@link https://floating-ui.com/docs/usefloating#strategy} */ menuFixedPositioning?: boolean; /** * Sets a custom width for the menu popover. Values are clamped between 150 - 350. */ menuWidth?: number; /** * Returns focus to the menu button after popover is closed. * Disable if focus management is delegated to the parent component. * Defaults to `true`. */ revertFocusToButton?: boolean; /** * Can be used to set the inner teleport component to render the menu's popover in the root `body` DOM node * (or `dialog` if popover is inside a dialog). */ teleport?: boolean; /** *
|
|
1
|
+
import { type OnClickOutsideOptions } from '@/directives/click-outside/types'; import { PopperContainerPosition } from '../popper-container/popper-container.types'; import { type BentoMenuItem } from './components/menu-item/menu-item.types'; import { type HTMLAttributes } from 'vue'; import { type BentoButtonProps } from '../button/button.types'; /** * @deprecated Since v3.0.0. Use string literal instead. */ const BentoMenuPositions = { ...PopperContainerPosition }; type BentoMenuPositions = PopperContainerPosition; export type * from './components/menu-item/menu-item.types'; export enum BentoMenuEvent { BACK = 'back', CLOSE = 'close', OPEN = 'open', } export interface BentoMenuButtonProps { 'aria-label'?: HTMLAttributes['aria-label']; condensed?: boolean; disabled?: boolean; variant?: BentoButtonProps['variant']; } export interface BentoMenuProps { /** * Object containing props to change the menu button appearance. Accepts the same props present in BentoButton. */ button?: BentoMenuButtonProps; /** * Controls if the menu should close when an item is clicked. * Defaults to `true`. */ closeMenuOnItemSelect?: boolean; /** * The data to be displayed in the list */ data: Array<BentoMenuItem>; /** * Disables opening the menu when the down arrow key is pressed. */ disableArrowDownOpeningMenu?: boolean; /** * The position of where the menu should open. */ menuPosition?: BentoMenuPositions | `${BentoMenuPositions}`; /** * Instructs the menu popover to be visible outside of the containing container. * @see {@link https://floating-ui.com/docs/usefloating#strategy} */ menuFixedPositioning?: boolean; /** * Sets a custom width for the menu popover. Values are clamped between 150 - 350. */ menuWidth?: number; /** * Returns focus to the menu button after popover is closed. * Disable if focus management is delegated to the parent component. * Defaults to `true`. */ revertFocusToButton?: boolean; /** * Can be used to set the inner teleport component to render the menu's popover in the root `body` DOM node * (or `dialog` if popover is inside a dialog). */ teleport?: boolean; /** * Options forwarded to the click-outside handler that closes the menu. * The menu button is always ignored, in addition to any elements listed in `ignore`. * * @see {@link OnClickOutsideOptions} */ closeOnClickOutsideOptions?: OnClickOutsideOptions; }
|
package/dist/assets/components/pagination/components/pagination-context/pagination-context.vue
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div class="b-pagination-context"> <bento-typography v-if="totalPages > 1 && !hidePageSelection" el="div"> <component :is="i18nComponent" v-bind="getI18nProps('pageNumber')" tag="div"> <template #page> <bento-dropdown :id="dropdownId" :aria-label="t('pageNumber', { page: n(page) })" class="b-pagination-context__dropdown" condensed :items="listOfAvailablePages" :model-value="page" :virtual-scroll="virtualScroll" @update:model-value="onPageSelectChange" /> </template> </component> </bento-typography> <bento-typography v-else-if="showPageNumberOfTotal" el="span"> {{ t('pageNumberOfTotal', { page: n(page), totalPages: n(totalPages) }) }} </bento-typography> <bento-typography v-else el="span"> {{ t('pageNumber', { page: n(page) }) }} </bento-typography> </div> </template> <script setup lang="ts"> import { computed } from 'vue'; import { BentoTypography } from '@/components/typography'; import { BentoDropdown } from '@/components/dropdown'; import { type BentoListboxOptionItem } from '@/types/listbox'; import { generateUid } from '@/utils/ts/generate-uid'; import messages from './messages.json'; import { getI18nComponent, getI18nProps, useI18n } from '@/utils/ts/i18n'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults( defineProps<{ /** * Hides the page selection dropdown and displays a static "Page X of Y" text instead. * Useful for very large datasets where rendering all page options would cause performance issues. */ hidePageSelection?: boolean; /** * The current page number of the pager. */ page?: number; /** * The total number of
|
|
1
|
+
<template> <div class="b-pagination-context"> <bento-typography v-if="totalPages > 1 && !hidePageSelection" el="div"> <component :is="i18nComponent" v-bind="getI18nProps('pageNumber')" tag="div"> <template #page> <bento-dropdown :id="dropdownId" :aria-label="t('pageNumber', { page: n(page) })" class="b-pagination-context__dropdown" condensed :items="listOfAvailablePages" :model-value="page" :virtual-scroll="virtualScroll" @update:model-value="onPageSelectChange" /> </template> </component> </bento-typography> <bento-typography v-else-if="showPageNumberOfTotal" el="span"> {{ t('pageNumberOfTotal', { page: n(page), totalPages: n(totalPages) }) }} </bento-typography> <bento-typography v-else el="span"> {{ t('pageNumber', { page: n(page) }) }} </bento-typography> </div> </template> <script setup lang="ts"> import { computed } from 'vue'; import { BentoTypography } from '@/components/typography'; import { BentoDropdown } from '@/components/dropdown'; import { type BentoListboxOptionItem } from '@/types/listbox'; import { generateUid } from '@/utils/ts/generate-uid'; import messages from './messages.json'; import { getI18nComponent, getI18nProps, useI18n } from '@/utils/ts/i18n'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults( defineProps<{ /** * Hides the page selection dropdown and displays a static "Page X of Y" text instead. * Useful for very large datasets where rendering all page options would cause performance issues. */ hidePageSelection?: boolean; /** * The current page number of the pager. */ page?: number; /** * The total number of pages the pager is paging through. */ totalPages?: number; /** * Enables virtual scrolling on all dropdowns if set to true. */ virtualScroll?: boolean; }>(), { hidePageSelection: false, page: 1, totalPages: null, virtualScroll: false, } ); const emit = defineEmits<{ (e: 'page-selected', value: string | number); }>(); const { t, n } = useI18n<{ message: MessageSchema }>({ messages }); const dropdownId = generateUid('bento-pagination-context-current-page'); const onPageSelectChange = (selectedPage: string | number) => emit('page-selected', selectedPage); const i18nComponent = getI18nComponent(); // Generate a list of pages from 1 to "totalPages" const transformIntoOption = (value: number): BentoListboxOptionItem => ({ label: n(value), value, }); const transformToPageNumber = (_, index) => index + 1; const listOfAvailablePages = computed(() => Array.from(new Array(props.totalPages), transformToPageNumber).map(transformIntoOption) ); const showPageNumberOfTotal = computed( () => props.totalPages === 1 || (props.hidePageSelection && props.page <= props.totalPages) ); </script> <script lang="ts"> export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./pagination-context.scss" />
|
package/dist/assets/components/pagination/components/pagination-controls/pagination-controls.vue
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <ul class="b-pagination-controls"> <li v-if="!hideFirstLastPageButtons" key="first" class="b-pagination-controls__item"> <!-- Navigate to the first page --> <bento-button :disabled="isCurrentPageFirst" variant="tertiary" condensed @click="navigate(1)"> <template #iconLeft> <skip-left :svg-title="t('navigateToTheFirstPage')" /> </template> </bento-button> </li> <li key="previous" class="b-pagination-controls__item"> <!-- Navigate to the previous page --> <bento-button :disabled="isCurrentPageFirst" variant="tertiary" condensed @click="navigate(page - 1)"> <template #iconLeft> <chevron-left :svg-title="t('navigateToThePreviousPage')" /> </template> </bento-button> </li> <li key="next" class="b-pagination-controls__item"> <!-- Navigate to the next page --> <bento-button :disabled="isNextButtonDisabled" variant="tertiary" condensed @click="navigate(page + 1)"> <template #iconLeft> <chevron-right :svg-title="t('navigateToTheNextPage')" /> </template> </bento-button> </li> <li v-if="!hideFirstLastPageButtons" key="last" class="b-pagination-controls__item"> <!-- Navigate to the last page --> <bento-button :disabled="isLastPageButtonDisabled" variant="tertiary" condensed @click="navigate(totalPages)" > <template #iconLeft> <skip-right :svg-title="t('navigateToTheLastPage')" /> </template> </bento-button> </li> </ul> </template> <script setup lang="ts"> import { computed } from 'vue'; import { BentoButton } from '@/components/button'; import { useI18n } from '@/utils/ts/i18n'; import SkipLeft from '@adyen/ui-assets-icons-16/vue/skip-left'; import SkipRight from '@adyen/ui-assets-icons-16/vue/skip-right'; import ChevronLeft from '@adyen/ui-assets-icons-16/vue/chevron-left-small'; import ChevronRight from '@adyen/ui-assets-icons-16/vue/chevron-right-small'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults( defineProps<{ /** * Use to determine if the pager has a "next page". */ hasNext?: boolean; /** * Hides the "navigate to the first page" and "navigate to the last page" buttons. * Useful for cursor-based pagination, where jumping directly to the first or last page is not supported. */ hideFirstLastPageButtons?: boolean; /** * The current page number of the pager. */ page: number; /** * The
|
|
1
|
+
<template> <ul class="b-pagination-controls"> <li v-if="!hideFirstLastPageButtons" key="first" class="b-pagination-controls__item"> <!-- Navigate to the first page --> <bento-button :disabled="isCurrentPageFirst" variant="tertiary" condensed @click="navigate(1)"> <template #iconLeft> <skip-left :svg-title="t('navigateToTheFirstPage')" /> </template> </bento-button> </li> <li key="previous" class="b-pagination-controls__item"> <!-- Navigate to the previous page --> <bento-button :disabled="isCurrentPageFirst" variant="tertiary" condensed @click="navigate(page - 1)"> <template #iconLeft> <chevron-left :svg-title="t('navigateToThePreviousPage')" /> </template> </bento-button> </li> <li key="next" class="b-pagination-controls__item"> <!-- Navigate to the next page --> <bento-button :disabled="isNextButtonDisabled" variant="tertiary" condensed @click="navigate(page + 1)"> <template #iconLeft> <chevron-right :svg-title="t('navigateToTheNextPage')" /> </template> </bento-button> </li> <li v-if="!hideFirstLastPageButtons" key="last" class="b-pagination-controls__item"> <!-- Navigate to the last page --> <bento-button :disabled="isLastPageButtonDisabled" variant="tertiary" condensed @click="navigate(totalPages)" > <template #iconLeft> <skip-right :svg-title="t('navigateToTheLastPage')" /> </template> </bento-button> </li> </ul> </template> <script setup lang="ts"> import { computed } from 'vue'; import { BentoButton } from '@/components/button'; import { useI18n } from '@/utils/ts/i18n'; import SkipLeft from '@adyen/ui-assets-icons-16/vue/skip-left'; import SkipRight from '@adyen/ui-assets-icons-16/vue/skip-right'; import ChevronLeft from '@adyen/ui-assets-icons-16/vue/chevron-left-small'; import ChevronRight from '@adyen/ui-assets-icons-16/vue/chevron-right-small'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults( defineProps<{ /** * Use to determine if the pager has a "next page". */ hasNext?: boolean; /** * Hides the "navigate to the first page" and "navigate to the last page" buttons. * Useful for cursor-based pagination, where jumping directly to the first or last page is not supported. */ hideFirstLastPageButtons?: boolean; /** * The current page number of the pager. */ page: number; /** * The total number of pages the pager is paging through. */ totalPages?: number; }>(), { hasNext: null, hideFirstLastPageButtons: false, totalPages: null, } ); const emit = defineEmits<{ (e: 'navigate', value: number); }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const isCurrentPageFirst = computed(() => props.page === 1); const isCurrentPageLast = computed(() => props.page === props.totalPages); const isNextButtonDisabled = computed(() => { const hasNoNextPage = !props.hasNext; return hasNoNextPage || isCurrentPageLast.value; }); const isLastPageButtonDisabled = computed(() => { const hasNoNextPage = !props.hasNext; const hasNoTotalPagesNumber = !props.totalPages; return hasNoNextPage || isCurrentPageLast.value || hasNoTotalPagesNumber; }); const navigate = (pageArg: number) => { emit('navigate', pageArg); }; </script> <script lang="ts"> export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./pagination-controls.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { type BentoButtonActionObject } from '@/components/button'; import { type BentoButtonVariant } from '../button'; import { type BentoImageAspectRatio, type BentoImageProps } from '../image'; export interface BentoPromoBannerProps { /** * List of actions that will be used to render the buttons. * * Each object in the list should have a `title`, an `event` and a `variant`. * * `title`: Button's text. * `event`: Event triggered when clicking the Button. * `variant`: Button's style. * * ``` * [{ * title: 'Primary', * event: () => { ... }, * variant: 'primary' * }, { * title: 'Secondary', * event: () => { ... } * variant: 'secondary' * }] * ``` */ actions?: Array<BentoPromoBannerAction>; /** * Defines the color of banner's background */ color?: `${BentoPromoBannerColor}`; /** * Defines whether the banner has a `Dismiss` button */ isDismissible?: boolean; /** *
|
|
1
|
+
import { type BentoButtonActionObject } from '@/components/button'; import { type BentoButtonVariant } from '../button'; import { type BentoImageAspectRatio, type BentoImageProps } from '../image'; export interface BentoPromoBannerProps { /** * List of actions that will be used to render the buttons. * * Each object in the list should have a `title`, an `event` and a `variant`. * * `title`: Button's text. * `event`: Event triggered when clicking the Button. * `variant`: Button's style. * * ``` * [{ * title: 'Primary', * event: () => { ... }, * variant: 'primary' * }, { * title: 'Secondary', * event: () => { ... } * variant: 'secondary' * }] * ``` */ actions?: Array<BentoPromoBannerAction>; /** * Defines the color of banner's background */ color?: `${BentoPromoBannerColor}`; /** * Defines whether the banner has a `Dismiss` button */ isDismissible?: boolean; /** * Overrides the label of the `Dismiss` button. Defaults to the localized `dismiss` message. */ dismissButtonText?: string; /** * Defines the image used in the banner. * * The promotional banner adapts to different container sizes based on the image's `aspect-ratio`. * Only the following `aspect-ratio` values are supported: `16:9`, `2:1`, and `1:1`. * * In Storybook, to verify the image display, update `image.src` and `image.aspectRatio` as follows: * - `./1-1.png` with an aspect ratio of `1:1` * - `./2-1.png` with an aspect ratio of `2:1` * - `./16-9.png` with an aspect ratio of `16:9` */ image: BentoPromoBannerImage; /** * Defines style of the banner. * * The horizontal layout is responsive to the container size and * changes depending on the image aspect ratio. * * The vertical layout remains the same regardless of container size. */ layout?: `${BentoPromoBannerLayout}`; /** * Defines the title text to be shown on the banner */ title: string; } export type BentoPromoBannerImageAspectRatio = `${ | BentoImageAspectRatio.P16_9 | BentoImageAspectRatio.P2_1 | BentoImageAspectRatio.P1_1}`; export type BentoPromoBannerButtonVariant = `${BentoButtonVariant.PRIMARY | BentoButtonVariant.SECONDARY}`; /** * @deprecated Since v3.0.0. Use `BentoPromoBannerAction` without the generic instead. */ // eslint-disable-next-line @typescript-eslint/no-unused-vars export type BentoPromoBannerAction<IconType = object> = BentoButtonActionObject & { variant: BentoPromoBannerButtonVariant; }; export interface BentoPromoBannerImage { src: BentoImageProps['src']; alt?: BentoImageProps['alt']; aspectRatio: BentoPromoBannerImageAspectRatio; } export enum BentoPromoBannerColor { BLUE = 'blue', GREY = 'grey', GREEN = 'green', } export enum BentoPromoBannerLayout { HORIZONTAL = 'horizontal', VERTICAL = 'vertical', }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <dialog ref="sidepanel" class="b-sidepanel" :class="conditionalClasses" @animationend="closeSidepanelAfterAnimation" @keydown.esc.prevent="isDismissible && startCloseSidepanelAnimation" > <bento-focus-trap :id="sidepanelId" v-bento-click-outside-directive="closeSidepanelOnClickOutside"> <div v-if="!computedHideCloseButton" class="b-sidepanel__close-button"> <bento-button variant="tertiary" @click="startCloseSidepanelAnimation"> <cross-icon :svg-title="t('close')" /> </bento-button> </div> <slot /> </bento-focus-trap> </dialog> </template> <script setup lang="ts"> import { computed, onMounted, onUnmounted, provide, ref, toRefs, watch } from 'vue'; import { BentoButton } from '@/components/button'; import { BentoFocusTrap } from '@/components/focus-trap'; import CrossIcon from '@adyen/ui-assets-icons-16/vue/cross'; import { BentoSidepanelEvent } from './sidepanel.types'; import { BentoDialogPageAnimation, type BentoDialogPageConfigData, type BentoDialogPageRegisterPage, DIALOG_PAGE_CONFIG_INJECTION_KEY, DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY, } from '@/internal'; import { SIDEPANEL_DIALOG_ID_INJECTION_KEY } from './sidepanel.keys'; import { BentoClickOutsideDirective as vBentoClickOutsideDirective } from '@/directives/click-outside'; import { useI18n } from '@/utils/ts/i18n'; import messages from './messages.json'; import { generateUid } from '@/utils/ts/generate-uid'; import { deprecate } from '@/utils/ts/deprecate'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * The id of the current active page * Should be the same id as the BentoSidepanelPage to be displayed * If the component only has one page, it can be left null/undefined. */ activePage: { type: String, default: null, }, /** * If set to true, it allows the sidepanel to be closed. * Shows close button when `true` and hides close button when `false`. * * _Note: This property will not set the visibility of the close button in v3.0.0_ */ isDismissible: { type: Boolean, default: false, }, /** * Controls the sidepanel's state from the outside. Set it to true to open the sidepanel. */ isOpen: { type: Boolean, default: null, }, /** * Controls the visibility of close button on the sidepanel. * Defaults to `true` when `isDismissible=false`, and `
|
|
1
|
+
<template> <dialog ref="sidepanel" class="b-sidepanel" :class="conditionalClasses" @animationend="closeSidepanelAfterAnimation" @keydown.esc.prevent="isDismissible && startCloseSidepanelAnimation" > <bento-focus-trap :id="sidepanelId" v-bento-click-outside-directive="closeSidepanelOnClickOutside"> <div v-if="!computedHideCloseButton" class="b-sidepanel__close-button"> <bento-button variant="tertiary" @click="startCloseSidepanelAnimation"> <cross-icon :svg-title="t('close')" /> </bento-button> </div> <slot /> </bento-focus-trap> </dialog> </template> <script setup lang="ts"> import { computed, onMounted, onUnmounted, provide, ref, toRefs, watch } from 'vue'; import { BentoButton } from '@/components/button'; import { BentoFocusTrap } from '@/components/focus-trap'; import CrossIcon from '@adyen/ui-assets-icons-16/vue/cross'; import { BentoSidepanelEvent } from './sidepanel.types'; import { BentoDialogPageAnimation, type BentoDialogPageConfigData, type BentoDialogPageRegisterPage, DIALOG_PAGE_CONFIG_INJECTION_KEY, DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY, } from '@/internal'; import { SIDEPANEL_DIALOG_ID_INJECTION_KEY } from './sidepanel.keys'; import { BentoClickOutsideDirective as vBentoClickOutsideDirective } from '@/directives/click-outside'; import { useI18n } from '@/utils/ts/i18n'; import messages from './messages.json'; import { generateUid } from '@/utils/ts/generate-uid'; import { deprecate } from '@/utils/ts/deprecate'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * The id of the current active page * Should be the same id as the BentoSidepanelPage to be displayed * If the component only has one page, it can be left null/undefined. */ activePage: { type: String, default: null, }, /** * If set to true, it allows the sidepanel to be closed. * Shows close button when `true` and hides close button when `false`. * * _Note: This property will not set the visibility of the close button in v3.0.0_ */ isDismissible: { type: Boolean, default: false, }, /** * Controls the sidepanel's state from the outside. Set it to true to open the sidepanel. */ isOpen: { type: Boolean, default: null, }, /** * Controls the visibility of close button on the sidepanel. * Defaults to `true` when `isDismissible=false`, and `false` when `isDismissible=true`. * * _Note: This property will be set to `false` by default, independent of `isDismissible` in v3.0.0_ */ hideCloseButton: { type: Boolean, default: null, }, }); const emit = defineEmits<{ /** * Emits update event when page is changed internally */ (e: BentoSidepanelEvent.UPDATE_ACTIVE_PAGE, page: string): void; /** * Emits close-sidepanel event when the sidepanel is closed */ (e: BentoSidepanelEvent.UPDATE_IS_OPEN, isOpen: boolean): void; }>(); // Refs const { activePage, isOpen } = toRefs(props); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const sidepanel = ref(null); const pages = ref<Array<string>>([]); const previousPages = ref([]); const animation = ref(BentoDialogPageAnimation.TRANSITION_LEVEL_DEEPER); const isClosing = ref(false); const sidepanelId = generateUid('sidepanel'); const internalIsOpen = ref(false); const isAnimationActive = ref(false); const isMounted = ref(false); // Animation styling const conditionalClasses = computed(() => ({ 'b-sidepanel--hide': isClosing.value, 'b-sidepanel--animation': isAnimationActive.value, })); // Handle closing and closing animation for dialog element const closeSidepanelAfterAnimation = () => { if (isClosing.value && sidepanel.value) { sidepanel.value.close(); if (previousPages.value[0]) { emit(BentoSidepanelEvent.UPDATE_ACTIVE_PAGE, previousPages.value[0]); } previousPages.value = []; isClosing.value = false; internalIsOpen.value = false; emit(BentoSidepanelEvent.UPDATE_IS_OPEN, false); } }; const startCloseSidepanelAnimation = () => { if (!sidepanel.value || !sidepanel.value.open) { return; } isClosing.value = true; }; const closeSidepanelOnClickOutside = () => { if (props.isDismissible) { startCloseSidepanelAnimation(); } }; // TODO: Remove in upcoming major. See properties `hideCloseButton` and `isDismissible`. const computedHideCloseButton = computed(() => { if (props.hideCloseButton !== null) { return props.hideCloseButton; } return !props.isDismissible; }); onMounted(() => { isMounted.value = true; }); onUnmounted(() => { isMounted.value = false; }); // Watching isOpen prop to toggle open/close the dialog element watch( [() => isOpen.value, sidepanel], ([newIsOpen, newSidepanelValue]) => { if (!isMounted.value || !newSidepanelValue) { return; } if (newIsOpen) { internalIsOpen.value = true; if (!newSidepanelValue.open) { newSidepanelValue.showModal(); } if (!previousPages.value.length) { previousPages.value.push(activePage.value); } } else if (newSidepanelValue.open) { startCloseSidepanelAnimation(); } }, { immediate: true, flush: 'post' } ); // Pages const updatePage = page => emit(BentoSidepanelEvent.UPDATE_ACTIVE_PAGE, page); // unregister page if removed / destroyed const unregisterPage = (pageIdToRemove: string) => () => { pages.value = pages.value?.filter(pageId => pageId !== pageIdToRemove); }; // Provide data and function to register page to subcomponent const registerPage: BentoDialogPageRegisterPage = pageId => { pages.value.push(pageId); return { unregisterPage: unregisterPage(pageId) }; }; provide<BentoDialogPageConfigData>(DIALOG_PAGE_CONFIG_INJECTION_KEY, { activePage, animation, isAnimationActive, isOpen: internalIsOpen, pages, previousPages, updatePage, }); provide(DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY, registerPage); // Let's Children components know they are inside a SidePanel (dialog element) provide(SIDEPANEL_DIALOG_ID_INJECTION_KEY, sidepanelId); if (!props.isDismissible && props.hideCloseButton === null) { deprecate( 'BentoSidepanel "isDismissible" property’s behavior', 'BentoSidepanel "isDismissible" will no longer hide the close button. Use "hideCloseButton=true" instead.', '3.0.0' ); } </script> <script lang="ts"> /** * Sidepanel component is a slide-in panel that should be used for presenting actions, settings or features that are directly associated with a specific task or interaction on the main screen. * * @example * import { BentoSidepanel, BentoSidepanelPage } from '@adyen/bento-vue2'; * * export default { * components: { BentoSidepanel, BentoSidepanelPage }, * template: ` * <bento-sidepanel active-page='step1' is-open='true'> * <bento-sidepanel-page * title='Step 1' * pageId='step1' * :actions="[ * { * title: 'Next step', * event: () => { * activePage.value = 'step2'; * }, * }]"> * Step 1 info * </bento-sidepanel-page> * <bento-sidepanel-page * title='Step 2' * pageId='step2' * :actions="[ * { * title: 'Save', * event: () => {}, * }]"> * Step 2 info * </bento-sidepanel-page> * </bento-sidepanel> * ` * } */ export default { i18n: { messages }, name: 'bento-sidepanel', }; </script> <style lang="scss" scoped src="./sidepanel.scss" />
|