@adyen/bento-mcp 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +7 -6
- package/bin/add-skill.js +12 -1
- package/dist/assets/components/anchor-scroller/anchor-scroller.vue +1 -1
- package/dist/assets/components/anchor-scroller/components/anchor-scroller-list/anchor-scroller-list.types.ts +1 -1
- package/dist/assets/components/anchor-scroller/components/anchor-scroller-list/anchor-scroller-list.vue +1 -1
- package/dist/assets/components/button/button.docs.mdx +24 -0
- package/dist/assets/components/button/button.vue +1 -1
- package/dist/assets/components/button/components/button-actions/button-actions.docs.mdx +14 -1
- package/dist/assets/components/date-range-picker/date-range-picker.vue +1 -1
- package/dist/assets/components/draggable/components/draggable-handle/draggable-handle.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-options-container/dropdown-options-container.vue +1 -1
- package/dist/assets/components/dropdown/dropdown.docs.mdx +58 -0
- package/dist/assets/components/dropdown/dropdown.stories.ts +1 -1
- package/dist/assets/components/dropdown/dropdown.vue +1 -1
- package/dist/assets/components/filter-bar/components/range-filter/range-filter.types.ts +1 -0
- package/dist/assets/components/filter-bar/components/range-filter/range-filter.vue +1 -1
- package/dist/assets/components/filter-bar/filter-bar.docs.mdx +74 -1
- package/dist/assets/components/filter-bar/filter-bar.stories.ts +1 -1
- package/dist/assets/components/filter-bar/filter-bar.types.ts +1 -1
- package/dist/assets/components/filter-bar/filter-bar.vue +1 -1
- package/dist/assets/components/header/header.docs.mdx +8 -0
- package/dist/assets/components/header-with-views/header-with-views.docs.mdx +12 -0
- package/dist/assets/components/input-field/input-field.vue +1 -1
- package/dist/assets/components/internal/listbox/components/listbox-lazy-load/listbox-lazy-load.types.ts +1 -0
- package/dist/assets/components/internal/listbox/components/listbox-lazy-load/listbox-lazy-load.vue +1 -0
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-category/listbox-multi-select-category.vue +1 -1
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-nested-lazy-load/listbox-multi-select-nested-lazy-load.types.ts +1 -0
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-nested-lazy-load/listbox-multi-select-nested-lazy-load.vue +1 -0
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/listbox-multi-select-options.vue +1 -1
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/listbox-multi-select.vue +1 -1
- package/dist/assets/components/internal/listbox/components/listbox-single-select/listbox-single-select.vue +1 -1
- package/dist/assets/components/internal/listbox/listbox.vue +1 -1
- package/dist/assets/components/table-of-contents/components/table-of-contents-list/table-of-contents-list.types.ts +1 -0
- package/dist/assets/components/table-of-contents/components/table-of-contents-list/table-of-contents-list.vue +1 -0
- package/dist/assets/components/table-of-contents/table-of-contents.docs.mdx +34 -0
- package/dist/assets/components/table-of-contents/table-of-contents.stories.ts +1 -0
- package/dist/assets/components/table-of-contents/table-of-contents.types.ts +1 -0
- package/dist/assets/components/table-of-contents/table-of-contents.vue +1 -0
- package/dist/assets/components.json +1 -0
- package/dist/assets/composables/use-numeric-input/use-numeric-input.types.ts +1 -0
- package/dist/assets/composables/use-section-navigation-keyboard/use-section-navigation-keyboard.types.ts +1 -0
- package/dist/assets/deprecations.md +136 -136
- package/dist/assets/index.ts +1 -1
- package/dist/assets/usage.json +80 -79
- package/dist/assets/utils/ts/scroll-to-section/scroll-to-section.types.ts +1 -0
- package/dist/main.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import { type BentoAvatarListboxOptions } from '@/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.types'; import { type BentoListboxEmptyStateProps, type BentoListboxIsOptionDisabled, type BentoListboxLazyLoadType, type BentoListboxOptions, type BentoListboxSearchOptions, type BentoListboxSelectedValue, type BentoListboxVirtulisationOptions, } from '@/types/listbox'; import { type OmitStrict } from '@/types/omit-strict'; import { type VueNodeElement } from '@/types/elements'; import { type BentoCountryListboxOptions } from '@/components/dropdown/components/variants/dropdown-country/dropdown-country.types'; import { type BentoTagListboxOptions } from '@/components/dropdown/components/variants/dropdown-tag/dropdown-tag.types'; import { type BentoCountryVariant } from '@/components/country'; import { type BentoCurrencyListboxOptions } from '@/components/dropdown/components/variants/dropdown-currency/dropdown-currency.types'; import { type BentoPaymentMethodListboxOptions } from '@/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.types'; import { type BentoInputDropdownProps, type BentoInputFieldElementPosition, type BentoInputFieldValue, } from '@/components/input-field'; import { type BentoDatePickerFirstDayOfWeek, type BentoDatePickerIsDateDisabled, type BentoDatePickerVariant, type BentoDatePickerYearRange, } from '@/components/date-picker'; import { type BentoDateRangePickerGranularityConfig, type BentoDateRangePickerValue, type BentoDateRangePickerVariant, type BentoDateRangePickerYearRange, } from '@/components/date-range-picker'; import { type BentoRadioGroupFilterOptions, type BentoRadioGroupFilterValue, } from './components/radio-group-filter/radio-group-filter.types'; import { type DateRangePickerCalendarRangeSelectorItems } from '../date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.types'; import { type BentoSearchBarProps } from '@/components/search-bar/search-bar.types'; import type { BentoInputFilterOptions, BentoInputFilterValue } from './components/input-filter/input-filter.types'; import type { BentoToggleFilterOptions, BentoToggleFilterValue } from './components/toggle-filter/toggle-filter.types'; import type { BentoTooltipPosition } from '@/directives'; import { type BentoCheckboxGroupFilterOptions, type BentoCheckboxGroupFilterValue, } from './components/checkbox-group-filter/checkbox-group-filter.types'; /** * Re-export filter component types */ export type * from './components/input-filter/input-filter.types'; export type * from './components/radio-group-filter/radio-group-filter.types'; export type * from './components/toggle-filter/toggle-filter.types'; export interface BentoSelectFilterOptions { /** * Empty state props that will be used with the inner empty state component to be displayed when no search results are found. */ emptyState?: BentoListboxEmptyStateProps; /** * Option items for the list-box */ listboxItems: BentoListboxOptions; /** * Defines if the list-box should be a multi-select filter */ multiple?: boolean; /** * @deprecated Since v2.0.0. Do not use. The persistent filter label now uses a counter instead of custom strings. This property is now redundant. * The translation object to handle any custom translations. e.g. persistent filter labels. */ messages?: { [key: string]: { persistentButtonLabelPartiallySelected?: string; persistentButtonLabelAllSelected?: string; [key: string]: string; }; }; /** * @deprecated Since v2.0.0. Do not use. The persistent filter label now uses a counter instead of custom strings. This property is now redundant. * The unique key to use in the messages translation object to use for the persistent button label * when the select filter has partially selected items. */ messagesPersistentButtonLabelPartiallySelectedKey?: string; /** * @deprecated Since v2.0.0. Do not use. The persistent filter label now uses a counter instead of custom strings. This property is now redundant. * The unique key to use in the messages translation object to use for the persistent button label * when the select filter has all items selected. */ messagesPersistentButtonLabelAllSelectedKey?: string; /** * The event fire when the filter is opened. */ onOpen?: () => void; lazy?: { /** * Indicates whether there are more items to load */ hasMoreItems: boolean; /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button */ lazyLoadType: BentoListboxLazyLoadType | `${BentoListboxLazyLoadType}`; /** * Indicates if new options are loading. */ loading: boolean; /** * The click event that fires when the user press on the "show more" button OR when the user * scrolls down to bottom of the select filter box. */ showMoreEvent?: () => void; }; /** * Toggles the loading state of the listbox. */ loading?: boolean; /** * Search options and events for the listbox filtering. */ search?: BentoListboxSearchOptions; /** * Defines if the list-box should support static categories */ staticCategories?: boolean; /** * A function that defines which option in the list-box is disabled */ isOptionDisabled?: BentoListboxIsOptionDisabled; /** * Enables virtual scrolling if set to true or by providing an object with itemHeight function. * The itemHeight function is used to calculate the height of rendered item given it's index. */ virtualScroll?: BentoListboxVirtulisationOptions; /** * Attaches a `BentoTooltip` if present. Text showed in the tooltip on hover. */ tooltipText?: string; /** * Position the tooltip should take if space is available. */ tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; /** * Enables value/label value handling. When `true`, the component expects and emits value/label * pair objects instead of primitive values. * * This affects both the `value` prop and the `input` event payload. * * - **Input (`value` prop):** Expects a `{ value: string | number, label: string }` object (or an array of them for multi-select). * - **Output (`input` event):** Emits a `{ value: string | number, label: string }` object (or an array of them) upon selection. * * This is particularly useful when a selected item's label is not available in the `items` prop, * allowing the component to display the correct label for values loaded from external sources. * @default false */ enableValueLabelPair?: boolean; /** * Enables a search bar that dynamically filters values * @default true */ dynamicFiltering?: boolean; } export type BentoSelectAvatarFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoAvatarListboxOptions; }; export type BentoSelectCountryFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoCountryListboxOptions; variant?: BentoCountryVariant | `${BentoCountryVariant}`; }; export type BentoSelectCurrencyFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoCurrencyListboxOptions; }; export type BentoSelectTagFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoTagListboxOptions; }; export type BentoSelectPaymentMethodFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoPaymentMethodListboxOptions; }; export interface BentoBooleanFilterOption { label: string; value: string | boolean; } export interface BentoBooleanFilterOptions { positiveOption?: BentoBooleanFilterOption; negativeOption?: BentoBooleanFilterOption; tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoInputWithDropdownFilterOptions { dropdown: BentoInputDropdownProps; dropdownPosition?: BentoInputFieldElementPosition | `${BentoInputFieldElementPosition}`; tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoRangeFilterOptions { tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } // Date export interface BentoDateFilterOptions { firstDayOfWeek?: BentoDatePickerFirstDayOfWeek; isDateDisabled?: BentoDatePickerIsDateDisabled; yearRange?: BentoDatePickerYearRange; variant?: `${BentoDatePickerVariant}`; tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoDateRangeFilterOptions { allowTimeInput?: boolean; firstDayOfWeek?: BentoDatePickerFirstDayOfWeek; isDateDisabled?: BentoDatePickerIsDateDisabled; yearRange?: BentoDateRangePickerYearRange; granularities?: Array<BentoDateRangePickerGranularityConfig>; maxRange?: number; min?: Date; max?: Date; numberOfMonths?: number; showEndDateOnOpen?: boolean; quickSelectRanges?: DateRangePickerCalendarRangeSelectorItems | boolean; variant?: `${BentoDateRangePickerVariant}`; tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoInpuWithDropdowntFilterValue { inputValue: BentoInputFieldValue; dropdownValue: BentoInputDropdownProps['value']; } export type BentoSelectFilterValue = BentoListboxSelectedValue; export type BentoBooleanFilterValue = string | null; export type BentoDateFilterValue = Date | null; export type BentoDateRangeFilterValue = BentoDateRangePickerValue | null; export type BentoFilterBarValue = | BentoInputFilterValue | BentoInpuWithDropdowntFilterValue | BentoSelectFilterValue | BentoBooleanFilterValue | BentoDateFilterValue | BentoDateRangeFilterValue | BentoRadioGroupFilterValue | BentoCheckboxGroupFilterValue | BentoToggleFilterValue; export type BentoFilterOptions = | BentoSelectFilterOptions | BentoSelectAvatarFilterOptions | BentoSelectCountryFilterOptions | BentoSelectCurrencyFilterOptions | BentoSelectPaymentMethodFilterOptions | BentoSelectTagFilterOptions | BentoDateFilterOptions | BentoDateRangeFilterOptions | BentoBooleanFilterOptions | BentoInputFilterOptions | BentoInputWithDropdownFilterOptions | BentoRadioGroupFilterOptions | BentoCheckboxGroupFilterOptions | BentoToggleFilterOptions | undefined; export enum BentoFilterEvent { // Fires upon the user interacting with the filter INPUT = 'input', // Fires on the (new) filter values being applied UPDATE = 'update', } export enum BentoFilterBarEvent { INPUT = 'input', UPDATE_FILTER_VALUES_OBJECT = 'update:filter-values-object', UPDATE_FILTER_VALUES_ARRAY = 'update:filter-values', } export enum BentoFilterItemType { /** * @deprecated Since v2.0.0. Use `BentoFilterItemType.RADIO_GROUP` instead. */ BOOLEAN = 'BentoBooleanFilter', RANGE = 'BentoRangeFilter', // Input INPUT = 'BentoInputFilter', INPUT_DROPDOWN = 'BentoInputWithDropdownFilter', // Select SELECT = 'BentoSelectFilter', SELECT_AVATAR = 'BentoSelectAvatarFilter', SELECT_COUNTRY = 'BentoSelectCountryFilter', SELECT_CURRENCY = 'BentoSelectCurrencyFilter', SELECT_PAYMENT_METHOD = 'BentoSelectPaymentMethodFilter', SELECT_TAG = 'BentoSelectTagFilter', // Date DATE = 'BentoDateFilter', DATE_RANGE = 'BentoDateRangeFilter', // Radio group RADIO_GROUP = 'BentoRadioGroupFilter', // Checkbox CHECKBOX_GROUP = 'BentoCheckboxGroupFilter', // Toggle TOGGLE = 'BentoToggleFilter', } export interface BentoFilterValue { field: string; value?: BentoFilterBarValue; } export interface BentoFilterModel<CustomOptions = any> extends BentoFilterValue { label: string; description?: string; options?: BentoFilterOptions | CustomOptions; disabled?: boolean; defaultValue?: BentoFilterBarValue; visible?: boolean; /** * Defines the filter to be used. * * Can be one filter defined in the {@link BentoFilterItemType} enum or a * custom filter component defined as a {@link VueNodeElement} */ type: BentoFilterItemType | VueNodeElement; /** * @deprecated Since v2.0.0. Use `config` and `filter-values` instead. */ value?: BentoFilterBarValue; } export type BentoFilterBarModel<CustomOptions = any> = Array<BentoFilterModel<CustomOptions>>; export type BentoFilterValues = Array<BentoFilterValue>; export type BentoFilterValueObject<T extends PropertyKey = string, CustomValue = BentoFilterBarValue> = Record< T, CustomValue >; export type BentoFilterBarSearchBarProps = OmitStrict<BentoSearchBarProps, 'value' | 'condensed'> & { /** * CSS width value applied to the search box element (e.g. `'200px'`, `'50%'`). */ width?: string; }; export type BentoFilterBarSearchTerm = BentoSearchBarProps['value']; export type BentoFilterBarScreenOptionsLayout = 'one-line' | 'multi-line'; export interface BentoFilterBarContainerSizeLayout { /** * @default 'one-line' */ medium?: BentoFilterBarScreenOptionsLayout; /** * @default 'multi-line' */ small?: BentoFilterBarScreenOptionsLayout; } export interface BentoFilterBarProps<CustomValue = BentoFilterBarValue | any, CustomOptions = any> { /** * Array of filters based on @see {@link BentoFilterBarModel}. * Each one of the elements in the array correspond to a filter that will * be created and rendered in the FilterBar. * * @example * [{ * field: 'company', * label: 'Company', * type: FilterType.TEXT, * }, { * field: 'name', * label: 'Name', * type: FilterType.TEXT, * }] */ config?: BentoFilterBarModel<CustomOptions>; /** * An array of the values for each filter in the filter bar. * @see {@link BentoFilterValues} * * @example * [{ * field: 'company', * value: '200', * }, { * field: 'name', * value: 'some text', * }] */ filterValues?: BentoFilterValues; /** * Defines the layout (multi-line or one-line) to be used in the filter bar for different * container sizes: * - Large (>=700px): Always multi-line * - Medium (>=430px & <700px) * - Small (<430px) * * @example * { * medium: 'multi-line', * small: 'one-line' * } */ containerSizeLayout?: BentoFilterBarContainerSizeLayout; /** * Search bar properties with which the placeholder, hint and other * "bento-search-bar" properties can be set. @see {@link BentoFilterBarSearchBarProps} */ search?: BentoFilterBarSearchBarProps; /** * Search term useful to filter all the data by text. * If the value is non-existant (null or undefined) the search bar won't be enabled. * Can be used with `.sync` */ searchTerm?: BentoFilterBarSearchTerm; /** * Will show all applied filters even if their visibility is set to false. */ showAppliedHiddenFilters?: boolean; /** * Array of filters based on @see {@link BentoFilterBarModel}. * Each one of the elements in the array correspond to a filter that will * be created and rendered in the FilterBar. * * Can be used with v-model or independently with the @input event. * * @example * [{ * field: 'company', * label: 'Company', * value: '200', * type: FilterType.TEXT, * }, { * field: 'name', * label: 'Name', * value: null, * type: FilterType.TEXT, * }] * * @deprecated Since v2.0.0. Use `config` instead. */ value?: BentoFilterBarModel; /** * An object of the values for each filter in the filter bar where key is a the field name and the value is the BentoFilterBarValue. * * @example * { * 'company': '200', * 'name': 'some text' * }, */ filterValuesObject?: BentoFilterValueObject<string, CustomValue>; }
|
|
1
|
+
import { type BentoAvatarListboxOptions } from '@/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.types'; import { type BentoListboxEmptyStateProps, type BentoListboxIsOptionDisabled, type BentoListboxLazyLoadType, type BentoListboxOptions, type BentoListboxSearchOptions, type BentoListboxSelectedValue, type BentoListboxVirtulisationOptions, } from '@/types/listbox'; import { type OmitStrict } from '@/types/omit-strict'; import { type VueNodeElement } from '@/types/elements'; import { type BentoCountryListboxOptions } from '@/components/dropdown/components/variants/dropdown-country/dropdown-country.types'; import { type BentoTagListboxOptions } from '@/components/dropdown/components/variants/dropdown-tag/dropdown-tag.types'; import { type BentoCountryVariant } from '@/components/country'; import { type BentoCurrencyListboxOptions } from '@/components/dropdown/components/variants/dropdown-currency/dropdown-currency.types'; import { type BentoPaymentMethodListboxOptions } from '@/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.types'; import { type BentoInputDropdownProps, type BentoInputFieldElementPosition, type BentoInputFieldValue, } from '@/components/input-field'; import { type BentoDatePickerFirstDayOfWeek, type BentoDatePickerIsDateDisabled, type BentoDatePickerVariant, type BentoDatePickerYearRange, } from '@/components/date-picker'; import { type BentoDateRangePickerGranularityConfig, type BentoDateRangePickerValue, type BentoDateRangePickerVariant, type BentoDateRangePickerYearRange, } from '@/components/date-range-picker'; import { type BentoRadioGroupFilterOptions, type BentoRadioGroupFilterValue, } from './components/radio-group-filter/radio-group-filter.types'; import { type DateRangePickerCalendarRangeSelectorItems } from '../date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.types'; import { type BentoSearchBarProps } from '@/components/search-bar/search-bar.types'; import type { BentoInputFilterOptions, BentoInputFilterValue } from './components/input-filter/input-filter.types'; import type { BentoToggleFilterOptions, BentoToggleFilterValue } from './components/toggle-filter/toggle-filter.types'; import type { BentoTooltipPosition } from '@/directives'; import { type BentoCheckboxGroupFilterOptions, type BentoCheckboxGroupFilterValue, } from './components/checkbox-group-filter/checkbox-group-filter.types'; /** * Re-export filter component types */ export type * from './components/input-filter/input-filter.types'; export type * from './components/radio-group-filter/radio-group-filter.types'; export type * from './components/toggle-filter/toggle-filter.types'; export interface BentoSelectFilterOptions { /** * Empty state props that will be used with the inner empty state component to be displayed when no search results are found. */ emptyState?: BentoListboxEmptyStateProps; /** * Option items for the list-box */ listboxItems: BentoListboxOptions; /** * Defines if the list-box should be a multi-select filter */ multiple?: boolean; /** * @deprecated Since v2.0.0. Do not use. The persistent filter label now uses a counter instead of custom strings. This property is now redundant. * The translation object to handle any custom translations. e.g. persistent filter labels. */ messages?: { [key: string]: { persistentButtonLabelPartiallySelected?: string; persistentButtonLabelAllSelected?: string; [key: string]: string; }; }; /** * @deprecated Since v2.0.0. Do not use. The persistent filter label now uses a counter instead of custom strings. This property is now redundant. * The unique key to use in the messages translation object to use for the persistent button label * when the select filter has partially selected items. */ messagesPersistentButtonLabelPartiallySelectedKey?: string; /** * @deprecated Since v2.0.0. Do not use. The persistent filter label now uses a counter instead of custom strings. This property is now redundant. * The unique key to use in the messages translation object to use for the persistent button label * when the select filter has all items selected. */ messagesPersistentButtonLabelAllSelectedKey?: string; /** * The event fire when the filter is opened. */ onOpen?: () => void; lazy?: { /** * Indicates whether there are more items to load */ hasMoreItems: boolean; /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button */ lazyLoadType: BentoListboxLazyLoadType | `${BentoListboxLazyLoadType}`; /** * Indicates if new options are loading. */ loading: boolean; /** * The click event that fires when the user press on the "show more" button OR when the user * scrolls down to bottom of the select filter box. */ showMoreEvent?: () => void; }; /** * Toggles the loading state of the listbox. */ loading?: boolean; /** * Search options and events for the listbox filtering. */ search?: BentoListboxSearchOptions; /** * Defines if the list-box should support static categories */ staticCategories?: boolean; /** * A function that defines which option in the list-box is disabled */ isOptionDisabled?: BentoListboxIsOptionDisabled; /** * Enables virtual scrolling if set to true or by providing an object with itemHeight function. * The itemHeight function is used to calculate the height of rendered item given it's index. */ virtualScroll?: BentoListboxVirtulisationOptions; /** * Attaches a `BentoTooltip` if present. Text showed in the tooltip on hover. */ tooltipText?: string; /** * Position the tooltip should take if space is available. */ tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; /** * Enables value/label value handling. When `true`, the component expects and emits value/label * pair objects instead of primitive values. * * This affects both the `value` prop and the `input` event payload. * * - **Input (`value` prop):** Expects a `{ value: string | number, label: string }` object (or an array of them for multi-select). * - **Output (`input` event):** Emits a `{ value: string | number, label: string }` object (or an array of them) upon selection. * * This is particularly useful when a selected item's label is not available in the `items` prop, * allowing the component to display the correct label for values loaded from external sources. * @default false */ enableValueLabelPair?: boolean; /** * Enables a search bar that dynamically filters values * @default true */ dynamicFiltering?: boolean; } export type BentoSelectAvatarFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoAvatarListboxOptions; }; export type BentoSelectCountryFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoCountryListboxOptions; variant?: BentoCountryVariant | `${BentoCountryVariant}`; }; export type BentoSelectCurrencyFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoCurrencyListboxOptions; }; export type BentoSelectTagFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoTagListboxOptions; }; export type BentoSelectPaymentMethodFilterOptions = OmitStrict<BentoSelectFilterOptions, 'listboxItems'> & { listboxItems: BentoPaymentMethodListboxOptions; }; export interface BentoBooleanFilterOption { label: string; value: string | boolean; } export interface BentoBooleanFilterOptions { positiveOption?: BentoBooleanFilterOption; negativeOption?: BentoBooleanFilterOption; tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoInputWithDropdownFilterOptions { dropdown: BentoInputDropdownProps; dropdownPosition?: BentoInputFieldElementPosition | `${BentoInputFieldElementPosition}`; tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoRangeFilterOptions { tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } // Date export interface BentoDateFilterOptions { firstDayOfWeek?: BentoDatePickerFirstDayOfWeek; isDateDisabled?: BentoDatePickerIsDateDisabled; yearRange?: BentoDatePickerYearRange; variant?: `${BentoDatePickerVariant}`; tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoDateRangeFilterOptions { allowTimeInput?: boolean; firstDayOfWeek?: BentoDatePickerFirstDayOfWeek; isDateDisabled?: BentoDatePickerIsDateDisabled; yearRange?: BentoDateRangePickerYearRange; granularities?: Array<BentoDateRangePickerGranularityConfig>; maxRange?: number; min?: Date; max?: Date; numberOfMonths?: number; showEndDateOnOpen?: boolean; quickSelectRanges?: DateRangePickerCalendarRangeSelectorItems | boolean; variant?: `${BentoDateRangePickerVariant}`; tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoInpuWithDropdowntFilterValue { inputValue: BentoInputFieldValue; dropdownValue: BentoInputDropdownProps['value']; } export type BentoSelectFilterValue = BentoListboxSelectedValue; export type BentoBooleanFilterValue = string | null; export type BentoDateFilterValue = Date | null; export type BentoDateRangeFilterValue = BentoDateRangePickerValue | null; export type BentoFilterBarValue = | BentoInputFilterValue | BentoInpuWithDropdowntFilterValue | BentoSelectFilterValue | BentoBooleanFilterValue | BentoDateFilterValue | BentoDateRangeFilterValue | BentoRadioGroupFilterValue | BentoCheckboxGroupFilterValue | BentoToggleFilterValue; export type BentoFilterOptions = | BentoSelectFilterOptions | BentoSelectAvatarFilterOptions | BentoSelectCountryFilterOptions | BentoSelectCurrencyFilterOptions | BentoSelectPaymentMethodFilterOptions | BentoSelectTagFilterOptions | BentoDateFilterOptions | BentoDateRangeFilterOptions | BentoBooleanFilterOptions | BentoInputFilterOptions | BentoInputWithDropdownFilterOptions | BentoRadioGroupFilterOptions | BentoCheckboxGroupFilterOptions | BentoToggleFilterOptions | undefined; export enum BentoFilterEvent { // Fires upon the user interacting with the filter INPUT = 'input', // Fires on the (new) filter values being applied UPDATE = 'update', } export enum BentoFilterBarEvent { INPUT = 'input', UPDATE_FILTER_VALUES_OBJECT = 'update:filter-values-object', UPDATE_FILTER_VALUES_ARRAY = 'update:filter-values', } export enum BentoFilterItemType { /** * @deprecated Since v2.0.0. Use `BentoFilterItemType.RADIO_GROUP` instead. */ BOOLEAN = 'BentoBooleanFilter', RANGE = 'BentoRangeFilter', // Input INPUT = 'BentoInputFilter', INPUT_DROPDOWN = 'BentoInputWithDropdownFilter', // Select SELECT = 'BentoSelectFilter', SELECT_AVATAR = 'BentoSelectAvatarFilter', SELECT_COUNTRY = 'BentoSelectCountryFilter', SELECT_CURRENCY = 'BentoSelectCurrencyFilter', SELECT_PAYMENT_METHOD = 'BentoSelectPaymentMethodFilter', SELECT_TAG = 'BentoSelectTagFilter', // Date DATE = 'BentoDateFilter', DATE_RANGE = 'BentoDateRangeFilter', // Radio group RADIO_GROUP = 'BentoRadioGroupFilter', // Checkbox CHECKBOX_GROUP = 'BentoCheckboxGroupFilter', // Toggle TOGGLE = 'BentoToggleFilter', } export interface BentoFilterValue { field: string; value?: BentoFilterBarValue; } export interface BentoFilterModel<CustomOptions = any> extends BentoFilterValue { label: string; description?: string; options?: BentoFilterOptions | CustomOptions; disabled?: boolean; defaultValue?: BentoFilterBarValue; visible?: boolean; /** * Defines the filter to be used. * * Can be one filter defined in the {@link BentoFilterItemType} enum or a * custom filter component defined as a {@link VueNodeElement} */ type: BentoFilterItemType | VueNodeElement; /** * @deprecated Since v2.0.0. Use `config` and `filter-values` instead. */ value?: BentoFilterBarValue; } export type BentoFilterBarModel<CustomOptions = any> = Array<BentoFilterModel<CustomOptions>>; export type BentoFilterValues = Array<BentoFilterValue>; export type BentoFilterValueObject<T extends PropertyKey = string, CustomValue = BentoFilterBarValue> = Record< T, CustomValue >; export type BentoFilterBarSearchBarProps = OmitStrict<BentoSearchBarProps, 'value' | 'condensed'> & { /** * CSS width value applied to the search box element (e.g. `'200px'`, `'50%'`). */ width?: string; }; export type BentoFilterBarSearchTerm = BentoSearchBarProps['value']; export type BentoFilterBarScreenOptionsLayout = 'one-line' | 'multi-line'; export interface BentoFilterBarContainerSizeLayout { /** * @default 'multi-line' */ large?: BentoFilterBarScreenOptionsLayout; /** * @default 'one-line' */ medium?: BentoFilterBarScreenOptionsLayout; /** * @default 'multi-line' */ small?: BentoFilterBarScreenOptionsLayout; } export interface BentoFilterBarProps<CustomValue = BentoFilterBarValue | any, CustomOptions = any> { /** * Array of filters based on @see {@link BentoFilterBarModel}. * Each one of the elements in the array correspond to a filter that will * be created and rendered in the FilterBar. * * @example * [{ * field: 'company', * label: 'Company', * type: FilterType.TEXT, * }, { * field: 'name', * label: 'Name', * type: FilterType.TEXT, * }] */ config?: BentoFilterBarModel<CustomOptions>; /** * An array of the values for each filter in the filter bar. * @see {@link BentoFilterValues} * * @example * [{ * field: 'company', * value: '200', * }, { * field: 'name', * value: 'some text', * }] */ filterValues?: BentoFilterValues; /** * Defines the layout (multi-line or one-line) to be used in the filter bar for different * container sizes: * - Large (>=700px) * - Medium (>=430px & <700px) * - Small (<430px) * * @example * { * large: 'multi-line', * medium: 'multi-line', * small: 'one-line' * } */ containerSizeLayout?: BentoFilterBarContainerSizeLayout; /** * Search bar properties with which the placeholder, hint and other * "bento-search-bar" properties can be set. @see {@link BentoFilterBarSearchBarProps} */ search?: BentoFilterBarSearchBarProps; /** * Search term useful to filter all the data by text. * If the value is non-existant (null or undefined) the search bar won't be enabled. * Can be used with `.sync` */ searchTerm?: BentoFilterBarSearchTerm; /** * Will show all applied filters even if their visibility is set to false. */ showAppliedHiddenFilters?: boolean; /** * Array of filters based on @see {@link BentoFilterBarModel}. * Each one of the elements in the array correspond to a filter that will * be created and rendered in the FilterBar. * * Can be used with v-model or independently with the @input event. * * @example * [{ * field: 'company', * label: 'Company', * value: '200', * type: FilterType.TEXT, * }, { * field: 'name', * label: 'Name', * value: null, * type: FilterType.TEXT, * }] * * @deprecated Since v2.0.0. Use `config` instead. */ value?: BentoFilterBarModel; /** * An object of the values for each filter in the filter bar where key is a the field name and the value is the BentoFilterBarValue. * * @example * { * 'company': '200', * 'name': 'some text' * }, */ filterValuesObject?: BentoFilterValueObject<string, CustomValue>; }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div ref="filterBarRef" class="b-filter-bar" :class="rootElementConditionalClasses"> <div v-if="!showSearchbarInsideContainer && isSearchBarEnabled" class="b-filter-bar__search-external-container"> <bento-search-bar class="b-filter-bar__search-bar" v-bind="searchBarProps" :input-field-aria-label="searchBarAriaLabel" :value="searchTerm" condensed :style="searchBarWidthStyle" @input="onSearchbarInput" @clear="onSearchbarClear" /> </div> <component :is="screenLayoutComponent" :class="containerConditionalClasses" condensed> <div :class="contentConditionalClasses"> <div v-if="showSearchbarInsideContainer && isSearchBarEnabled" class="b-filter-bar__search-bar-container" :style="searchBarWidthStyle" > <bento-search-bar class="b-filter-bar__search-bar" v-bind="searchBarProps" :input-field-aria-label="searchBarAriaLabel" :value="searchTerm" condensed @input="onSearchbarInput" @clear="onSearchbarClear" /> </div> <template v-if="persistentFilters.length"> <component :is="filter.type" v-for="filter in persistentFilters" :key="filter.field" :field="filter.field" :label="filter.label" :disabled="filter?.disabled" :options="filter.options" :value="filter.value" :default-value="filter.defaultValue" @update="updateFilterHandler" /> </template> <component :is="filter.type" v-for="filter in regularFilters" :key="filter.field" :field="filter.field" :label="filter.label" :disabled="filter?.disabled" :options="filter.options" :value="filter.value" @update="updateFilterHandler" /> <!-- All filters button --> <all-filters-modal v-if="hasHiddenFilters" class="b-filter-bar__all-filters-modal" :value="dynamicFilters" @input="updateFilterHandler" /> <!-- Clear all filters button --> <filter-bar-button v-if="isClearAllVisible" class="b-filter-bar__clear-all" @click="clearAllFilters"> {{ t('clearFilters') }} <template #iconLeft> <refresh-icon aria-hidden="true" /> </template> </filter-bar-button> <!-- Reset all filters button --> <filter-bar-button v-if="isResetAllVisible" class="b-filter-bar__reset-all" @click="resetAllFilters"> {{ t('resetFilters') }} <template #iconLeft> <refresh-icon aria-hidden="true" /> </template> </filter-bar-button> </div> </component> </div> </template> <script setup lang="ts"> import { computed, ref, toRef } from 'vue'; import { FilterBarButton } from './components/filter-bar-button'; import { BentoFilterBarEvent, type BentoFilterBarModel, type BentoFilterBarProps, type BentoFilterModel, } from './filter-bar.types'; import BentoSearchBar from '@/components/search-bar/search-bar.vue'; import { BentoSearchBarEvent } from '@/components/search-bar/search-bar.types'; import { useBentoToastController } from '@/composables'; import { useI18n } from '@/utils/ts/i18n'; import { useContainerSizeOptionsLayout } from './composables/use-container-size-options-layout/use-container-size-options-layout'; import AllFiltersModal from './components/all-filters-modal/all-filters-modal.vue'; import RefreshIcon from '@adyen/ui-assets-icons-16/vue/refresh'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; // TODO: setup Vue3 generics in Bento 2.0.0 const props = withDefaults(defineProps<BentoFilterBarProps>(), { config: () => [], filterValues: () => [], containerSizeLayout: () => ({ medium: 'one-line', small: 'multi-line', }), search: undefined, searchTerm: undefined, value: () => [], showAppliedHiddenFilters: false, filterValuesObject: undefined, }); const emit = defineEmits([ BentoFilterBarEvent.INPUT, BentoFilterBarEvent.UPDATE_FILTER_VALUES_OBJECT, BentoFilterBarEvent.UPDATE_FILTER_VALUES_ARRAY, BentoSearchBarEvent.CLEAR, BentoSearchBarEvent.INPUT, BentoSearchBarEvent.UPDATE, ]); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const filterBarRef = ref<HTMLDivElement>(null); const valueAndFilterValuesPropsMerged = computed<BentoFilterBarModel>(() => (props.config?.length > 0 ? props.config : props.value)?.map(filterModel => { let filterValue = props.filterValues.find(({ field }) => field === filterModel.field)?.value; if (props.filterValuesObject && filterModel.field in props.filterValuesObject) { filterValue = props.filterValuesObject[filterModel.field]; } if (filterValue === undefined) { // If the filter is using the deprecated `config` value prop use that // But if that is also not set then check if a default value exists set it to that if it is defined. filterValue = filterModel?.value === undefined && filterModel?.defaultValue !== undefined ? filterModel?.defaultValue : filterModel?.value; } return { ...filterModel, value: filterValue !== undefined ? filterValue : filterModel?.value }; }) ); const { addToast } = useBentoToastController(); /** * Container size layout. * Allows to change the layout from multi-line to one-line for each container size type: * - large: >=700 * - medium: >=430 & <700 * - small: <430 */ const { screenLayoutComponent, showSearchbarInsideContainer, isOneLineLayout, isLargeLayout, rootElementConditionalClasses, } = useContainerSizeOptionsLayout(filterBarRef, toRef(props, 'containerSizeLayout')); /** * Classes */ /** * Handles the classes of the swappable component ('div' or 'fixed-scroller) */ const containerConditionalClasses = computed(() => ({ 'b-filter-bar__fixed-scroller': isOneLineLayout.value, 'b-filter-bar__container': !isOneLineLayout.value, // Multi-line })); /** * Handles the classes of the container inside the swappable component. */ const contentConditionalClasses = computed(() => ({ 'b-filter-bar__scroller-content': isOneLineLayout.value, 'b-filter-bar__filters': !isOneLineLayout.value, // Multi-line })); /** * Search bar pops */ const searchBarAriaLabel = computed(() => props.search?.inputFieldAriaLabel || (t('searchBar') as string)); const searchBarProps = computed(() => { if (!props.search) { return undefined; } const { debounceTime, disabled, inputFieldAriaLabel, hint, placeholder } = props.search; return { debounceTime, disabled, inputFieldAriaLabel, hint, placeholder }; }); const searchBarWidthStyle = computed(() => props.search?.width && isLargeLayout.value ? { flexBasis: props.search.width, maxWidth: props.search.width } : undefined ); const hasHiddenFilters = computed(() => { // Has props with visible set const hasVisibleSet = valueAndFilterValuesPropsMerged.value.some( ({ visible }) => visible !== undefined && visible !== null ); return hasVisibleSet ? valueAndFilterValuesPropsMerged.value.some(({ visible }) => !visible) : false; }); // Persistent filters i.e. filters with default value const persistentFilters = computed(() => valueAndFilterValuesPropsMerged.value.filter(({ defaultValue }) => defaultValue !== undefined) ); // Dynamic filters i.e. filters without default value const dynamicFilters = computed(() => valueAndFilterValuesPropsMerged.value.filter(({ defaultValue }) => defaultValue === undefined) ); // Show non-persistent & visible filters const regularFilters = computed(() => { const hasVisibleSet = valueAndFilterValuesPropsMerged.value.some( ({ visible }) => visible !== undefined && visible !== null ); // If no filter has the `visible` prop set then all are visible, otherwise search for the "visible: true" elements return hasVisibleSet ? valueAndFilterValuesPropsMerged.value.filter( ({ defaultValue, visible, value }) => // eslint-disable-next-line eqeqeq (defaultValue === undefined && visible) || (props.showAppliedHiddenFilters && value != undefined) ) : valueAndFilterValuesPropsMerged.value.filter( ({ defaultValue, value }) => // eslint-disable-next-line eqeqeq defaultValue === undefined || (props.showAppliedHiddenFilters && value != undefined) ); }); // Enable search bar. // Soft equality to check for "null" and "undefined" // eslint-disable-next-line eqeqeq const isSearchBarEnabled = computed(() => props.searchTerm != undefined); const hasActiveFilters = computed(() => valueAndFilterValuesPropsMerged.value .filter(({ defaultValue }) => defaultValue === undefined || defaultValue === null) // Disabling strict equality to check for both undefined and null // eslint-disable-next-line eqeqeq .some(({ value }) => value != undefined) ); // Show the "Clear all" button (true) if at least one filter is active (contains a value) // and there are not persistent filters present. const isClearAllVisible = computed(() => persistentFilters.value.length === 0 && hasActiveFilters.value); const isResetAllVisible = computed(() => { if (persistentFilters.value.length === 0) { return false; } const areAllValuesEqualToDefaults = valueAndFilterValuesPropsMerged.value .filter(({ defaultValue }) => defaultValue !== undefined || defaultValue !== null) .every(({ value, defaultValue }) => JSON.stringify(value) === JSON.stringify(defaultValue)); return !areAllValuesEqualToDefaults || hasActiveFilters.value; }); const updateAllFilters = (newFilters: BentoFilterBarModel) => { emit(BentoFilterBarEvent.INPUT, newFilters); emit( BentoFilterBarEvent.UPDATE_FILTER_VALUES_ARRAY, newFilters ?.filter(({ value }) => value !== null || value !== undefined) .map(({ field, value }) => ({ field, value })) ); emit( BentoFilterBarEvent.UPDATE_FILTER_VALUES_OBJECT, newFilters ?.filter(({ value }) => value !== null || value !== undefined) .reduce((acc, filter) => { acc[filter.field] = filter.value; return acc; }, {}) ); }; const updateFilterHandler = (updatedFilters: BentoFilterModel | BentoFilterBarModel) => { const filtersArray = Array.isArray(updatedFilters) ? updatedFilters : [updatedFilters]; const updatedFiltersMap = new Map(filtersArray.map(filter => [filter.field, filter.value])); const newFilters = valueAndFilterValuesPropsMerged.value.map(filter => { if (updatedFiltersMap.has(filter.field)) { return { ...filter, value: updatedFiltersMap.get(filter.field) }; } return filter; }); updateAllFilters(newFilters); }; const clearAllFilters = () => { const previousFilters = valueAndFilterValuesPropsMerged.value.slice(); const clearedFilters = valueAndFilterValuesPropsMerged.value.map(({ value, ...otherFields }) => ({ ...otherFields, value: undefined, })); addToast({ text: t('filtersHaveBeenCleared') as string, action: { handler: () => updateAllFilters(previousFilters), text: t('undo') as string, }, }); updateAllFilters(clearedFilters); }; const resetAllFilters = () => { const previousFilters = valueAndFilterValuesPropsMerged.value.slice(); const resetFilters = valueAndFilterValuesPropsMerged.value.map(({ defaultValue, ...otherFields }) => ({ ...otherFields, defaultValue, value: defaultValue, })); addToast({ text: t('filtersHaveBeenReset') as string, action: { handler: () => updateAllFilters(previousFilters), text: t('undo') as string, }, }); updateAllFilters(resetFilters); }; const onSearchbarInput = (newSearchTerm: string) => { emit(BentoSearchBarEvent.INPUT, newSearchTerm); emit(BentoSearchBarEvent.UPDATE, newSearchTerm); }; const onSearchbarClear = () => { emit(BentoSearchBarEvent.CLEAR); }; // Expose methods for BentoDataGrid to access defineExpose({ isClearAllVisible, clearAllFilters, isResetAllVisible, resetAllFilters }); </script> <script lang="ts"> import { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoToggleFilter, } from './components'; import { BentoTeleport, FixedScroller } from '@/internal'; /** * The filter bar filters item lists and tables according to various filter criteria. * * @example * import { BentoFilterBar } from '@adyen/bento-vue2' * * export default { * components: { BentoFilterBar }, * data: () => ({ * filters: [{ * field: 'name', * label: 'Name', * value: null, * options: { ... }, * type: FilterType.TEXT, * }] * }), * template: ` * <bento-filter-bar * v-model="filter" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-filter-bar', components: { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoTeleport, BentoToggleFilter, FixedScroller, }, }; </script> <style lang="scss" scoped src="./filter-bar.scss" />
|
|
1
|
+
<template> <div ref="filterBarRef" class="b-filter-bar" :class="rootElementConditionalClasses"> <div v-if="!showSearchbarInsideContainer && isSearchBarEnabled" class="b-filter-bar__search-external-container"> <bento-search-bar class="b-filter-bar__search-bar" v-bind="searchBarProps" :input-field-aria-label="searchBarAriaLabel" :value="searchTerm" condensed :style="searchBarWidthStyle" @input="onSearchbarInput" @clear="onSearchbarClear" /> </div> <component :is="screenLayoutComponent" :class="containerConditionalClasses" condensed> <div :class="contentConditionalClasses"> <div v-if="showSearchbarInsideContainer && isSearchBarEnabled" class="b-filter-bar__search-bar-container" :style="searchBarWidthStyle" > <bento-search-bar class="b-filter-bar__search-bar" v-bind="searchBarProps" :input-field-aria-label="searchBarAriaLabel" :value="searchTerm" condensed @input="onSearchbarInput" @clear="onSearchbarClear" /> </div> <template v-if="persistentFilters.length"> <component :is="filter.type" v-for="filter in persistentFilters" :key="filter.field" :field="filter.field" :label="filter.label" :disabled="filter?.disabled" :options="filter.options" :value="filter.value" :default-value="filter.defaultValue" @update="updateFilterHandler" /> </template> <component :is="filter.type" v-for="filter in regularFilters" :key="filter.field" :field="filter.field" :label="filter.label" :disabled="filter?.disabled" :options="filter.options" :value="filter.value" @update="updateFilterHandler" /> <!-- All filters button --> <all-filters-modal v-if="hasHiddenFilters" class="b-filter-bar__all-filters-modal" :value="dynamicFilters" @input="updateFilterHandler" /> <!-- Clear all filters button --> <filter-bar-button v-if="isClearAllVisible" class="b-filter-bar__clear-all" @click="clearAllFilters"> {{ t('clearFilters') }} <template #iconLeft> <refresh-icon aria-hidden="true" /> </template> </filter-bar-button> <!-- Reset all filters button --> <filter-bar-button v-if="isResetAllVisible" class="b-filter-bar__reset-all" @click="resetAllFilters"> {{ t('resetFilters') }} <template #iconLeft> <refresh-icon aria-hidden="true" /> </template> </filter-bar-button> </div> </component> </div> </template> <script setup lang="ts"> import { computed, ref } from 'vue'; import { FilterBarButton } from './components/filter-bar-button'; import { type BentoFilterBarContainerSizeLayout, BentoFilterBarEvent, type BentoFilterBarModel, type BentoFilterBarProps, type BentoFilterModel, } from './filter-bar.types'; import BentoSearchBar from '@/components/search-bar/search-bar.vue'; import { BentoSearchBarEvent } from '@/components/search-bar/search-bar.types'; import { useBentoToastController } from '@/composables'; import { useI18n } from '@/utils/ts/i18n'; import { useContainerSizeOptionsLayout } from './composables/use-container-size-options-layout/use-container-size-options-layout'; import AllFiltersModal from './components/all-filters-modal/all-filters-modal.vue'; import RefreshIcon from '@adyen/ui-assets-icons-16/vue/refresh'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; // TODO: setup Vue3 generics in Bento 2.0.0 const props = withDefaults(defineProps<BentoFilterBarProps>(), { config: () => [], filterValues: () => [], containerSizeLayout: null, search: undefined, searchTerm: undefined, value: () => [], showAppliedHiddenFilters: false, filterValuesObject: undefined, }); const emit = defineEmits([ BentoFilterBarEvent.INPUT, BentoFilterBarEvent.UPDATE_FILTER_VALUES_OBJECT, BentoFilterBarEvent.UPDATE_FILTER_VALUES_ARRAY, BentoSearchBarEvent.CLEAR, BentoSearchBarEvent.INPUT, BentoSearchBarEvent.UPDATE, ]); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const filterBarRef = ref<HTMLDivElement>(null); const valueAndFilterValuesPropsMerged = computed<BentoFilterBarModel>(() => (props.config?.length > 0 ? props.config : props.value)?.map(filterModel => { let filterValue = props.filterValues.find(({ field }) => field === filterModel.field)?.value; if (props.filterValuesObject && filterModel.field in props.filterValuesObject) { filterValue = props.filterValuesObject[filterModel.field]; } if (filterValue === undefined) { // If the filter is using the deprecated `config` value prop use that // But if that is also not set then check if a default value exists set it to that if it is defined. filterValue = filterModel?.value === undefined && filterModel?.defaultValue !== undefined ? filterModel?.defaultValue : filterModel?.value; } return { ...filterModel, value: filterValue !== undefined ? filterValue : filterModel?.value }; }) ); const { addToast } = useBentoToastController(); /** * Container size layout. * Allows to change the layout from multi-line to one-line for each container size type: * - large: >=700 * - medium: >=430 & <700 * - small: <430 */ const DEFAULT_CONTAINER_SIZE_LAYOUT: Required<BentoFilterBarContainerSizeLayout> = { large: 'multi-line', medium: 'one-line', small: 'multi-line', }; const mergedContainerSizeLayout = computed<Required<BentoFilterBarContainerSizeLayout>>(() => ({ ...DEFAULT_CONTAINER_SIZE_LAYOUT, ...props.containerSizeLayout, })); const { screenLayoutComponent, showSearchbarInsideContainer, isOneLineLayout, isLargeLayout, rootElementConditionalClasses, } = useContainerSizeOptionsLayout(filterBarRef, mergedContainerSizeLayout); /** * Classes */ /** * Handles the classes of the swappable component ('div' or 'fixed-scroller) */ const containerConditionalClasses = computed(() => ({ 'b-filter-bar__fixed-scroller': isOneLineLayout.value, 'b-filter-bar__container': !isOneLineLayout.value, // Multi-line })); /** * Handles the classes of the container inside the swappable component. */ const contentConditionalClasses = computed(() => ({ 'b-filter-bar__scroller-content': isOneLineLayout.value, 'b-filter-bar__filters': !isOneLineLayout.value, // Multi-line })); /** * Search bar pops */ const searchBarAriaLabel = computed(() => props.search?.inputFieldAriaLabel || (t('searchBar') as string)); const searchBarProps = computed(() => { if (!props.search) { return undefined; } const { debounceTime, disabled, inputFieldAriaLabel, hint, placeholder } = props.search; return { debounceTime, disabled, inputFieldAriaLabel, hint, placeholder }; }); const searchBarWidthStyle = computed(() => props.search?.width && isLargeLayout.value ? { flexBasis: props.search.width, maxWidth: props.search.width } : undefined ); const hasHiddenFilters = computed(() => { // Has props with visible set const hasVisibleSet = valueAndFilterValuesPropsMerged.value.some( ({ visible }) => visible !== undefined && visible !== null ); return hasVisibleSet ? valueAndFilterValuesPropsMerged.value.some(({ visible }) => !visible) : false; }); // Persistent filters i.e. filters with default value const persistentFilters = computed(() => valueAndFilterValuesPropsMerged.value.filter(({ defaultValue }) => defaultValue !== undefined) ); // Dynamic filters i.e. filters without default value const dynamicFilters = computed(() => valueAndFilterValuesPropsMerged.value.filter(({ defaultValue }) => defaultValue === undefined) ); // Show non-persistent & visible filters const regularFilters = computed(() => { const hasVisibleSet = valueAndFilterValuesPropsMerged.value.some( ({ visible }) => visible !== undefined && visible !== null ); // If no filter has the `visible` prop set then all are visible, otherwise search for the "visible: true" elements return hasVisibleSet ? valueAndFilterValuesPropsMerged.value.filter( ({ defaultValue, visible, value }) => // eslint-disable-next-line eqeqeq (defaultValue === undefined && visible) || (props.showAppliedHiddenFilters && value != undefined) ) : valueAndFilterValuesPropsMerged.value.filter( ({ defaultValue, value }) => // eslint-disable-next-line eqeqeq defaultValue === undefined || (props.showAppliedHiddenFilters && value != undefined) ); }); // Enable search bar. // Soft equality to check for "null" and "undefined" // eslint-disable-next-line eqeqeq const isSearchBarEnabled = computed(() => props.searchTerm != undefined); const hasActiveFilters = computed(() => valueAndFilterValuesPropsMerged.value .filter(({ defaultValue }) => defaultValue === undefined || defaultValue === null) // Disabling strict equality to check for both undefined and null // eslint-disable-next-line eqeqeq .some(({ value }) => value != undefined) ); // Show the "Clear all" button (true) if at least one filter is active (contains a value) // and there are not persistent filters present. const isClearAllVisible = computed(() => persistentFilters.value.length === 0 && hasActiveFilters.value); const isResetAllVisible = computed(() => { if (persistentFilters.value.length === 0) { return false; } const areAllValuesEqualToDefaults = valueAndFilterValuesPropsMerged.value .filter(({ defaultValue }) => defaultValue !== undefined || defaultValue !== null) .every(({ value, defaultValue }) => JSON.stringify(value) === JSON.stringify(defaultValue)); return !areAllValuesEqualToDefaults || hasActiveFilters.value; }); const updateAllFilters = (newFilters: BentoFilterBarModel) => { emit(BentoFilterBarEvent.INPUT, newFilters); emit( BentoFilterBarEvent.UPDATE_FILTER_VALUES_ARRAY, newFilters ?.filter(({ value }) => value !== null || value !== undefined) .map(({ field, value }) => ({ field, value })) ); emit( BentoFilterBarEvent.UPDATE_FILTER_VALUES_OBJECT, newFilters ?.filter(({ value }) => value !== null || value !== undefined) .reduce((acc, filter) => { acc[filter.field] = filter.value; return acc; }, {}) ); }; const updateFilterHandler = (updatedFilters: BentoFilterModel | BentoFilterBarModel) => { const filtersArray = Array.isArray(updatedFilters) ? updatedFilters : [updatedFilters]; const updatedFiltersMap = new Map(filtersArray.map(filter => [filter.field, filter.value])); const newFilters = valueAndFilterValuesPropsMerged.value.map(filter => { if (updatedFiltersMap.has(filter.field)) { return { ...filter, value: updatedFiltersMap.get(filter.field) }; } return filter; }); updateAllFilters(newFilters); }; const clearAllFilters = () => { const previousFilters = valueAndFilterValuesPropsMerged.value.slice(); const clearedFilters = valueAndFilterValuesPropsMerged.value.map(({ value, ...otherFields }) => ({ ...otherFields, value: undefined, })); addToast({ text: t('filtersHaveBeenCleared') as string, action: { handler: () => updateAllFilters(previousFilters), text: t('undo') as string, }, }); updateAllFilters(clearedFilters); }; const resetAllFilters = () => { const previousFilters = valueAndFilterValuesPropsMerged.value.slice(); const resetFilters = valueAndFilterValuesPropsMerged.value.map(({ defaultValue, ...otherFields }) => ({ ...otherFields, defaultValue, value: defaultValue, })); addToast({ text: t('filtersHaveBeenReset') as string, action: { handler: () => updateAllFilters(previousFilters), text: t('undo') as string, }, }); updateAllFilters(resetFilters); }; const onSearchbarInput = (newSearchTerm: string) => { emit(BentoSearchBarEvent.INPUT, newSearchTerm); emit(BentoSearchBarEvent.UPDATE, newSearchTerm); }; const onSearchbarClear = () => { emit(BentoSearchBarEvent.CLEAR); }; // Expose methods for BentoDataGrid to access defineExpose({ isClearAllVisible, clearAllFilters, isResetAllVisible, resetAllFilters }); </script> <script lang="ts"> import { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoToggleFilter, } from './components'; import { BentoTeleport, FixedScroller } from '@/internal'; /** * The filter bar filters item lists and tables according to various filter criteria. * * @example * import { BentoFilterBar } from '@adyen/bento-vue2' * * export default { * components: { BentoFilterBar }, * data: () => ({ * filters: [{ * field: 'name', * label: 'Name', * value: null, * options: { ... }, * type: FilterType.TEXT, * }] * }), * template: ` * <bento-filter-bar * v-model="filter" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-filter-bar', components: { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoTeleport, BentoToggleFilter, FixedScroller, }, }; </script> <style lang="scss" scoped src="./filter-bar.scss" />
|
|
@@ -40,6 +40,14 @@ The only compulsory element of a header is title. Everything else can be turned
|
|
|
40
40
|
Buttons section should contain one primary and one secondary button, and a more options menu for any potential
|
|
41
41
|
additional actions. These are all passed as a `BentoButtonActionsList` to the component.
|
|
42
42
|
|
|
43
|
+
To listen to additional DOM events on the action buttons, such as `mouseenter` or `mouseleave`, use the action object's
|
|
44
|
+
`elementRef`. See
|
|
45
|
+
[additional DOM events on action buttons](../?path=/docs/components-button-button-actions--docs#additional-dom-events-on-action-buttons)
|
|
46
|
+
in the `bento-button-actions` documentation.
|
|
47
|
+
|
|
48
|
+
In `bento-header`, only the displayed action buttons can be referenced this way. Extra actions are placed in the more
|
|
49
|
+
options menu.
|
|
50
|
+
|
|
43
51
|
### Info icon content
|
|
44
52
|
|
|
45
53
|
If a page or component description and link exists, it’s displayed inside of a popover next to the title. Users can
|
|
@@ -39,6 +39,18 @@ The user can provide an empty state or a "default" view.
|
|
|
39
39
|
5. Listen to `reorder:view`.
|
|
40
40
|
6. On emission, update the order of your views that you have passed into the prop.
|
|
41
41
|
|
|
42
|
+
### Actions
|
|
43
|
+
|
|
44
|
+
The `actions` prop renders action buttons in the header using `bento-button-actions`.
|
|
45
|
+
|
|
46
|
+
To listen to additional DOM events on the action buttons, such as `mouseenter` or `mouseleave`, use the action object's
|
|
47
|
+
`elementRef`. See
|
|
48
|
+
[additional DOM events on action buttons](../?path=/docs/components-button-button-actions--docs#additional-dom-events-on-action-buttons)
|
|
49
|
+
in the `bento-button-actions` documentation.
|
|
50
|
+
|
|
51
|
+
In `bento-header-with-views`, only the displayed action buttons can be referenced this way. Extra actions are placed in
|
|
52
|
+
the more options menu.
|
|
53
|
+
|
|
42
54
|
## Modifiers
|
|
43
55
|
|
|
44
56
|
### Controlled Active View
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <bento-typography class="b-input-field" v-bind="getRootAttributes($attrs)" :data-testid="getDataTestId($attrs)" :class="inputFieldConditionalClasses" el="div" variant="body" > <field-label v-if="hasSlot('default') || label" :for="inputFieldId" data-testid="input-field-label" :tooltip-text="tooltipText" :optional="optional" :required="required" :label="label" @click="focusInput" > <slot></slot> </field-label> <div class="b-input-field__input-box" data-testid="input-box" :aria-disabled="disabled" @click="focusInput"> <!-- Optional Icon for Default text variant --> <span v-if="isDefaultVariant && (hasSlot('defaultIconBefore') || hasSlot('iconBefore'))" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-before" > <slot v-if="hasSlot('iconBefore')" name="iconBefore" /> <slot v-if="hasSlot('defaultIconBefore') && !hasSlot('iconBefore')" name="defaultIconBefore" /> </span> <!-- Mandatory Icon for Payment Method variant --> <span v-if="isPaymentMethodVariant && hasSlot('paymentMethod')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__payment-method" > <slot name="paymentMethod" /> </span> <!-- Dropdown at start --> <div v-if="shouldDisplayDropdownAtStart" :id="dropdownId" class="b-input-field__dropdown b-input-field__dropdown--start" @click.stop > <bento-dropdown v-if="dropdown" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> <!-- Static Value at start --> <bento-typography v-if="shouldDisplayStaticValueAtStart" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--start" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <div class="b-input-field__input-container"> <input :id="inputFieldId" v-bind="$attrs" ref="inputFieldElement" class="b-input-field__input" :aria-label="computedAriaLabel" :aria-describedby="computedAriaDescribedBy" :aria-owns="computedAriaOwns" :aria-invalid="shouldShowError" :placeholder="placeholder" :required="required" :aria-required="required" :type="type" :value="modelValue ?? value" :disabled="disabled" :readonly="isReadOnly" @input="onInput" @change="emit('change')" @focus="onFocus" @blur="onBlur" @keydown="emit('keydown', $event)" @keyup="emit('keyup', $event)" @keydown.esc="emit('escape-pressed')" @click="emit('click', $event)" /> <!-- Hint at the end --> <bento-typography v-if="hint && isFocused && (modelValue || value)" el="span" class="b-input-field__hint" > {{ hint }} </bento-typography> </div> <span v-if="hasSlot('iconAfter')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-after" data-testid="input-text__icon-after" > <slot name="iconAfter" /> </span> <!-- Static Value at the end --> <bento-typography v-if="shouldDisplayStaticValueAtEnd" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--end" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <!-- Dropdown at end --> <div v-if="shouldDisplayDropdownAtEnd" :id="dropdownId" data-testid="input-field-dropdown-container-end" class="b-input-field__dropdown b-input-field__dropdown--end" @click.stop > <bento-dropdown v-if="dropdown" :id="dropdownId" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> </div> <error-message v-if="shouldShowError && !!errorMessage" :id="errorId" :error-message="errorMessage" class="b-input-field__error-message" /> <span v-if="hasSlot('description') || description" :id="descriptionId" class="b-input-field__description" @click="focusInput" > <bento-typography el="span" variant="body"> <slot id="description" name="description"> {{ description }} </slot> </bento-typography> </span> </bento-typography> </template> <script setup lang="ts"> import { computed, inject, provide, type Ref, ref, toRef, toRefs, useAttrs, useSlots } from 'vue'; // Components import { BentoDropdown } from '@/components/dropdown'; import { DROPDOWN_SMALL_SIZE_INJECTION_KEY } from '@/components/dropdown/dropdown.keys'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { generateUid } from '@/core/utils/ts'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { INPUT_FIELD_HINT_INJECTION_KEY, INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, } from '@/components/input-field/input-field.keys'; import { type BentoListboxSelectedValue } from '@/types/listbox'; // Composables import { useFormLayoutFieldLoading, useHasSlot } from '@/composables'; // Types import { type BentoInputDropdownProps, BentoInputFieldElementPosition, type BentoInputFieldProps, BentoInputFieldStateClass, BentoInputFieldType, type BentoInputFieldValue, BentoInputFieldVariant, } from './input-field.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const props = withDefaults(defineProps<BentoInputFieldProps>(), { ariaHidden: true, ariaLabel: undefined, condensed: false, description: '', disabled: false, dropdown: undefined, dropdownPosition: BentoInputFieldElementPosition.START, error: false, errorMessage: null, label: '', modelValue: undefined, optional: false, placeholder: '', required: false, readonly: false, slashedZero: false, staticValue: '', staticValuePosition: BentoInputFieldElementPosition.START, tooltipText: null, type: BentoInputFieldType.TEXT, value: undefined, variant: BentoInputFieldVariant.DEFAULT, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the input is clicked */ (e: 'click', event: MouseEvent); /** * Emitted when the user modifies the element's value. Unlike the 'input' event, the change event is not necessarily fired for each alteration to an element's value */ (e: 'change'): void; /** * Emitted when the Input Field's internal dropdown changes it's value */ (e: 'dropdown-input', selectedValue: BentoListboxSelectedValue): void; /** * Emitted when the "ESC" key is pressed */ (e: 'escape-pressed'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when a key is pressed and lifted up */ (e: 'keyup', event: KeyboardEvent): void; /** * Emitted when the component's model changes * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', inputValue: string): void; /** * Emitted when the Input Field's internal dropdown changes it's value. Contains every dropdown prop passed with the updated selected value as 'value' */ (e: 'update:dropdown', updatedDropdownProps: BentoInputDropdownProps): void; /** * Emitted when the component's model changes */ (e: 'update:model-value', inputValue: BentoInputFieldValue): void; }>(); const { emitValue } = useFormFieldEmits<BentoInputFieldValue>(emit); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const hasSlot = useHasSlot(slots); const inputFieldElement = ref(null); const inputDropdownElement = ref(null); const inputFieldId = generateUid('input'); const dropdownId = generateUid('input-dropdown'); const staticValueId = generateUid('input-static-value'); const descriptionId = generateUid('input-description'); const errorId = generateUid('input-error'); const isFocused = ref(false); /** * Gets the overriden or the default data-testid attribute * @param attributes - the live attributes object */ function getDataTestId(attributes: Record<string, unknown>) { return (attributes['data-testid'] as string) ?? 'input-text-container'; } /** * Filters out all aria attributes for the root element. * @param attributes - the live attributes object */ function getRootAttributes(attributes: Record<string, unknown>) { return Object.fromEntries( Object.entries(attributes).filter(([key]) => !key.startsWith('aria-') && key !== 'data-testid') ); } // Provide DROPDOWN_SMALL_SIZE_INJECTION_KEY as true, to set the input only dropdown size variant in the BentoDropdown comp provide(DROPDOWN_SMALL_SIZE_INJECTION_KEY, true); // Renders the input with a hint if a string is provided const hint = inject<Ref<string | undefined>>(INPUT_FIELD_HINT_INJECTION_KEY, ref(undefined)); const onFocus = () => { isFocused.value = true; emit('focus'); }; const onBlur = () => { isFocused.value = false; emit('blur'); }; const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const isDropdownReadOnly = computed<BentoInputDropdownProps['readonly']>( () => isReadOnly.value || props.dropdown?.readonly ); const shouldShowStaticValue = computed( () => props.variant === BentoInputFieldVariant.STATIC_VALUE && !!slots.staticValue ); const isDefaultVariant = computed(() => props.variant === BentoInputFieldVariant.DEFAULT); const isPaymentMethodVariant = computed(() => props.variant === BentoInputFieldVariant.PAYMENT_METHOD); const shouldDisplayStaticValueAtStart = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.START ); const shouldDisplayStaticValueAtEnd = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.END ); const shouldShowDropdown = computed(() => props.variant === BentoInputFieldVariant.DROPDOWN && !!props.dropdown); const shouldDisplayDropdownAtStart = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.START ); const shouldDisplayDropdownAtEnd = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.END ); const shouldShowError = computed( () => !props.disabled && !isReadOnly.value && (!!props.errorMessage || props.error) ); const computedLabel = computed(() => (getSlotText(slots)('default') as string) || props.label); const computedDescription = computed(() => (getSlotText(slots)('description') as string) || props.description); const computedAriaOwns = computed(() => { if (shouldShowDropdown.value === true && shouldShowStaticValue.value === true) { return `${staticValueId} ${dropdownId}`; } if (shouldShowDropdown.value === true) { return `${dropdownId}`; } if (shouldShowStaticValue.value === true) { return `${staticValueId}`; } return null; }); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => attrs?.['aria-labelledby'] ? undefined : t('ariaLabelFallback') ); const computedAriaLabelDropdowFallback = computed(() => t('ariaLabelDropdownFallback', { inputLabel: computedAriaLabel.value }) ); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel } = toRefs(props); const computedAriaLabel = useAriaLabel({ ariaLabel: ariaLabelAttribute.value as string, // TODO: take this line away when the aria-label props is removed label: ariaLabel, defaultFallback: computedLabel.value || computedAriaLabelFallbackMessage, }); const computedAriaDescribedBy = computed( () => `${computedDescription.value ? descriptionId : ''} ${ shouldShowError.value && !!props.errorMessage ? errorId : '' }` ); const inputFieldConditionalClasses = computed(() => ({ [`b-input-field--${BentoInputFieldStateClass.CONDENSED}`]: props.condensed, [`b-input-field--${BentoInputFieldStateClass.DISABLED}`]: props.disabled, [`b-input-field--${BentoInputFieldStateClass.READONLY}`]: isReadOnly.value, [`b-input-field--${BentoInputFieldStateClass.ERROR}`]: shouldShowError.value, [`b-input-field--slashed-zero`]: props.slashedZero, })); const inputFieldDropdownProps = computed(() => ({ 'aria-label': computedAriaLabelDropdowFallback.value, ...props.dropdown, })); const onInput = (event: Event) => { const inputValue = (event.target as HTMLInputElement).value; if (!props.disabled && !isReadOnly.value) { emitValue(inputValue); } }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { emit('dropdown-input', selectedValue); emit('update:dropdown', { ...props?.dropdown, value: selectedValue }); }; const focusInput = () => { inputFieldElement.value.focus(); }; const focus = () => { if (props.variant === 'dropdown') { inputDropdownElement.value.focus(); } else { focusInput(); } }; // Expose method so that other components can focus on it in Vue3 defineExpose({ focusInput, focus, }); const inputFieldParentComponent = inject(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, ''); if (!Object.values(BentoInputFieldType).includes(props.type as BentoInputFieldType) && !inputFieldParentComponent) { if (props.type === 'tel') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-phone-number" instead.` ); } else if (props.type === 'password') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-password" instead.` ); } else { printDevelopmentWarning(`"bento-input-field" dropdown does not support the type '${props.type}'`); } } /** * Deprecations */ if (props.error) { deprecate( 'BentoInputField "error" property', `Use the BentoInputField "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-dropdown errorMessage="Error message text" />`, '2.0.0' ); } if (hasSlot('defaultIconBefore')) { deprecate( 'BentoInputField "defaultIconBefore" slot', `Use the BentoInputField "iconBefore" slot instead to add an icon at the left side of the input field <bento-input-field> \t<template #iconBefore> \t\t<my-icon-goes-here /> \t</template> </bento-input-field>`, '2.0.0' ); } if (props.value) { deprecate( 'BentoInputField "value" property', `The use of "value" prop in "BentoInputField" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field component is used to create interactive controls for web-based * forms in order to accept data from the user. * * @example * import { BentoInputField } from '@adyen/bento-vue2' * * const inputValue = ref(''); * * export default { * components: { BentoInputField }, * tempate: ` * <bento-input-field * :model-value="inputValue" * :@update:model-value="newValue => inputValue.value = newValue" * > * } */ export default { name: 'bento-input-field', inheritAttrs: false, model: { prop: 'modelValue' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./input-field.scss" />
|
|
1
|
+
<template> <bento-typography class="b-input-field" v-bind="getRootAttributes($attrs)" :data-testid="getDataTestId($attrs)" :class="inputFieldConditionalClasses" el="div" variant="body" > <field-label v-if="hasSlot('default') || label" :for="inputFieldId" data-testid="input-field-label" :tooltip-text="tooltipText" :optional="optional" :required="required" :label="label" @click="focusInput" > <slot></slot> </field-label> <div class="b-input-field__input-box" data-testid="input-box" :aria-disabled="disabled" @click="focusInput"> <!-- Optional Icon for Default text variant --> <span v-if="isDefaultVariant && (hasSlot('defaultIconBefore') || hasSlot('iconBefore'))" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-before" > <slot v-if="hasSlot('iconBefore')" name="iconBefore" /> <slot v-if="hasSlot('defaultIconBefore') && !hasSlot('iconBefore')" name="defaultIconBefore" /> </span> <!-- Mandatory Icon for Payment Method variant --> <span v-if="isPaymentMethodVariant && hasSlot('paymentMethod')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__payment-method" > <slot name="paymentMethod" /> </span> <!-- Dropdown at start --> <div v-if="shouldDisplayDropdownAtStart" :id="dropdownId" class="b-input-field__dropdown b-input-field__dropdown--start" @click.stop > <bento-dropdown v-if="dropdown" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> <!-- Static Value at start --> <bento-typography v-if="shouldDisplayStaticValueAtStart" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--start" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <div class="b-input-field__input-container"> <input :id="inputFieldId" v-bind="$attrs" ref="inputFieldElement" class="b-input-field__input" :aria-label="computedAriaLabel" :aria-describedby="computedAriaDescribedBy" :aria-owns="computedAriaOwns" :aria-invalid="shouldShowError" :placeholder="placeholder" :required="required" :aria-required="required" :type="type" :value="modelValue ?? value" :disabled="disabled" :readonly="isReadOnly" @input="onInput" @change="emit('change')" @focus="onFocus" @blur="onBlur" @keydown="emit('keydown', $event)" @keyup="emit('keyup', $event)" @keydown.esc="emit('escape-pressed')" @click="emit('click', $event)" /> <!-- Hint at the end --> <bento-typography v-if="hint && isFocused && (modelValue || value)" el="span" class="b-input-field__hint" > {{ hint }} </bento-typography> </div> <span v-if="hasSlot('iconAfter')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-after" data-testid="input-text__icon-after" > <slot name="iconAfter" /> </span> <!-- Static Value at the end --> <bento-typography v-if="shouldDisplayStaticValueAtEnd" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--end" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <!-- Dropdown at end --> <div v-if="shouldDisplayDropdownAtEnd" :id="dropdownId" data-testid="input-field-dropdown-container-end" class="b-input-field__dropdown b-input-field__dropdown--end" @click.stop > <bento-dropdown v-if="dropdown" :id="dropdownId" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> </div> <error-message v-if="shouldShowError && !!errorMessage" :id="errorId" :error-message="errorMessage" class="b-input-field__error-message" /> <span v-if="hasSlot('description') || description" :id="descriptionId" class="b-input-field__description" @click="focusInput" > <bento-typography el="span" variant="body"> <slot id="description" name="description"> {{ description }} </slot> </bento-typography> </span> </bento-typography> </template> <script setup lang="ts"> import { computed, inject, provide, type Ref, ref, toRef, toRefs, useAttrs, useSlots } from 'vue'; // Components import { BentoDropdown } from '@/components/dropdown'; import { DROPDOWN_SMALL_SIZE_INJECTION_KEY } from '@/components/dropdown/dropdown.keys'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { generateUid } from '@/core/utils/ts'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { INPUT_FIELD_HINT_INJECTION_KEY, INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, } from '@/components/input-field/input-field.keys'; import { type BentoListboxSelectedValue } from '@/types/listbox'; // Composables import { useFormLayoutFieldLoading, useHasSlot } from '@/composables'; // Types import { type BentoInputDropdownProps, BentoInputFieldElementPosition, type BentoInputFieldProps, BentoInputFieldStateClass, BentoInputFieldType, type BentoInputFieldValue, BentoInputFieldVariant, } from './input-field.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const props = withDefaults(defineProps<BentoInputFieldProps>(), { ariaHidden: true, ariaLabel: undefined, condensed: false, description: '', disabled: false, dropdown: undefined, dropdownPosition: BentoInputFieldElementPosition.START, error: false, errorMessage: null, label: '', modelValue: undefined, optional: false, placeholder: '', required: false, readonly: false, slashedZero: false, staticValue: '', staticValuePosition: BentoInputFieldElementPosition.START, tooltipText: null, type: BentoInputFieldType.TEXT, value: undefined, variant: BentoInputFieldVariant.DEFAULT, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the input is clicked */ (e: 'click', event: MouseEvent); /** * Emitted when the user modifies the element's value. Unlike the 'input' event, the change event is not necessarily fired for each alteration to an element's value */ (e: 'change'): void; /** * Emitted when the Input Field's internal dropdown changes it's value */ (e: 'dropdown-input', selectedValue: BentoListboxSelectedValue): void; /** * Emitted when the "ESC" key is pressed */ (e: 'escape-pressed'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when a key is pressed and lifted up */ (e: 'keyup', event: KeyboardEvent): void; /** * Emitted when the component's model changes * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', inputValue: string): void; /** * Emitted when the Input Field's internal dropdown changes it's value. Contains every dropdown prop passed with the updated selected value as 'value' */ (e: 'update:dropdown', updatedDropdownProps: BentoInputDropdownProps): void; /** * Emitted when the component's model changes */ (e: 'update:model-value', inputValue: BentoInputFieldValue): void; }>(); const { emitValue } = useFormFieldEmits<BentoInputFieldValue>(emit); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const hasSlot = useHasSlot(slots); const inputFieldElement = ref<HTMLInputElement>(null); const inputDropdownElement = ref(null); const inputFieldId = generateUid('input'); const dropdownId = generateUid('input-dropdown'); const staticValueId = generateUid('input-static-value'); const descriptionId = generateUid('input-description'); const errorId = generateUid('input-error'); const isFocused = ref(false); /** * Gets the overriden or the default data-testid attribute * @param attributes - the live attributes object */ function getDataTestId(attributes: Record<string, unknown>) { return (attributes['data-testid'] as string) ?? 'input-text-container'; } /** * Filters out all aria attributes for the root element. * @param attributes - the live attributes object */ function getRootAttributes(attributes: Record<string, unknown>) { return Object.fromEntries( Object.entries(attributes).filter(([key]) => !key.startsWith('aria-') && key !== 'data-testid') ); } // Provide DROPDOWN_SMALL_SIZE_INJECTION_KEY as true, to set the input only dropdown size variant in the BentoDropdown comp provide(DROPDOWN_SMALL_SIZE_INJECTION_KEY, true); // Renders the input with a hint if a string is provided const hint = inject<Ref<string | undefined>>(INPUT_FIELD_HINT_INJECTION_KEY, ref(undefined)); const onFocus = () => { isFocused.value = true; emit('focus'); }; const onBlur = () => { isFocused.value = false; emit('blur'); }; const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const isDropdownReadOnly = computed<BentoInputDropdownProps['readonly']>( () => isReadOnly.value || props.dropdown?.readonly ); const shouldShowStaticValue = computed( () => props.variant === BentoInputFieldVariant.STATIC_VALUE && !!slots.staticValue ); const isDefaultVariant = computed(() => props.variant === BentoInputFieldVariant.DEFAULT); const isPaymentMethodVariant = computed(() => props.variant === BentoInputFieldVariant.PAYMENT_METHOD); const shouldDisplayStaticValueAtStart = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.START ); const shouldDisplayStaticValueAtEnd = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.END ); const shouldShowDropdown = computed(() => props.variant === BentoInputFieldVariant.DROPDOWN && !!props.dropdown); const shouldDisplayDropdownAtStart = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.START ); const shouldDisplayDropdownAtEnd = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.END ); const shouldShowError = computed( () => !props.disabled && !isReadOnly.value && (!!props.errorMessage || props.error) ); const computedLabel = computed(() => (getSlotText(slots)('default') as string) || props.label); const computedDescription = computed(() => (getSlotText(slots)('description') as string) || props.description); const computedAriaOwns = computed(() => { if (shouldShowDropdown.value === true && shouldShowStaticValue.value === true) { return `${staticValueId} ${dropdownId}`; } if (shouldShowDropdown.value === true) { return `${dropdownId}`; } if (shouldShowStaticValue.value === true) { return `${staticValueId}`; } return null; }); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => attrs?.['aria-labelledby'] ? undefined : t('ariaLabelFallback') ); const computedAriaLabelDropdowFallback = computed(() => t('ariaLabelDropdownFallback', { inputLabel: computedAriaLabel.value }) ); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel } = toRefs(props); const computedAriaLabel = useAriaLabel({ ariaLabel: ariaLabelAttribute.value as string, // TODO: take this line away when the aria-label props is removed label: ariaLabel, defaultFallback: computedLabel.value || computedAriaLabelFallbackMessage, }); const computedAriaDescribedBy = computed( () => `${computedDescription.value ? descriptionId : ''} ${ shouldShowError.value && !!props.errorMessage ? errorId : '' }` ); const inputFieldConditionalClasses = computed(() => ({ [`b-input-field--${BentoInputFieldStateClass.CONDENSED}`]: props.condensed, [`b-input-field--${BentoInputFieldStateClass.DISABLED}`]: props.disabled, [`b-input-field--${BentoInputFieldStateClass.READONLY}`]: isReadOnly.value, [`b-input-field--${BentoInputFieldStateClass.ERROR}`]: shouldShowError.value, [`b-input-field--slashed-zero`]: props.slashedZero, })); const inputFieldDropdownProps = computed(() => ({ 'aria-label': computedAriaLabelDropdowFallback.value, ...props.dropdown, })); const onInput = (event: Event) => { const inputValue = (event.target as HTMLInputElement).value; if (!props.disabled && !isReadOnly.value) { emitValue(inputValue); } }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { emit('dropdown-input', selectedValue); emit('update:dropdown', { ...props?.dropdown, value: selectedValue }); }; const focusInput = () => { inputFieldElement.value.focus(); }; const focus = () => { if (props.variant === 'dropdown') { inputDropdownElement.value.focus(); } else { focusInput(); } }; // Expose focus methods for parent components in Vue 3. defineExpose({ /** * Focuses the input field. */ focusInput, /** * Focuses the dropdown when using the dropdown variant; otherwise focuses the input field. */ focus, /** * Reference to the native input element. */ inputFieldElement, }); const inputFieldParentComponent = inject(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, ''); if (!Object.values(BentoInputFieldType).includes(props.type as BentoInputFieldType) && !inputFieldParentComponent) { if (props.type === 'tel') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-phone-number" instead.` ); } else if (props.type === 'password') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-password" instead.` ); } else { printDevelopmentWarning(`"bento-input-field" dropdown does not support the type '${props.type}'`); } } /** * Deprecations */ if (props.error) { deprecate( 'BentoInputField "error" property', `Use the BentoInputField "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-dropdown errorMessage="Error message text" />`, '2.0.0' ); } if (hasSlot('defaultIconBefore')) { deprecate( 'BentoInputField "defaultIconBefore" slot', `Use the BentoInputField "iconBefore" slot instead to add an icon at the left side of the input field <bento-input-field> \t<template #iconBefore> \t\t<my-icon-goes-here /> \t</template> </bento-input-field>`, '2.0.0' ); } if (props.value) { deprecate( 'BentoInputField "value" property', `The use of "value" prop in "BentoInputField" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field component is used to create interactive controls for web-based * forms in order to accept data from the user. * * @example * import { BentoInputField } from '@adyen/bento-vue2' * * const inputValue = ref(''); * * export default { * components: { BentoInputField }, * tempate: ` * <bento-input-field * :model-value="inputValue" * :@update:model-value="newValue => inputValue.value = newValue" * > * } */ export default { name: 'bento-input-field', inheritAttrs: false, model: { prop: 'modelValue' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./input-field.scss" />
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { type BentoLazyLoadType } from '@/types/lazy-load'; export interface BentoLazyLoadProps { /** * Indicates if the 'Show more' button is using the condensed variant * @default false */ condensed?: boolean; /** * Indicates whether there are more items to load * @default false */ hasMoreItems?: boolean; /** * Indicates if new options are lazy loading. * @default false */ loading?: boolean; /** * Sets the tabindex to the button. * Used for the treeitem role */ tabindex?: number; /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button */ type: BentoLazyLoadType; }
|
package/dist/assets/components/internal/listbox/components/listbox-lazy-load/listbox-lazy-load.vue
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<template> <div v-if="shouldDisplayShowMoreButton || loading" class="b-listbox-lazy-load"> <bento-loading-button v-if="shouldDisplayShowMoreButton" :state="loadingButtonState" :tabindex="tabindex" :condensed="condensed" variant="secondary" @click.capture="$emit('show-more', $event)" > {{ showMoreButtonText }} </bento-loading-button> <bento-loading-indicator v-else-if="loading" /> </div> </template> <script setup lang="ts"> import { computed } from 'vue'; import { BentoListboxLazyLoadType } from '@/types/listbox'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoLoadingButton, BentoLoadingButtonState } from '@/components/button'; import { useI18n } from '@/utils/ts/i18n'; import { type BentoLazyLoadProps } from './listbox-lazy-load.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); defineEmits<{ /** * Emitted when the "Show more" button is clicked to request loading additional items. */ (e: 'show-more', event: Event); }>(); const props = withDefaults(defineProps<BentoLazyLoadProps>(), { condensed: false, hasMoreItems: false, loading: false, tabindex: undefined, }); const shouldDisplayShowMoreButton = computed( () => props.type === BentoListboxLazyLoadType.BUTTON && props.hasMoreItems ); const loadingButtonState = computed<BentoLoadingButtonState>(() => props.loading ? BentoLoadingButtonState.LOADING : BentoLoadingButtonState.START ); const showMoreButtonText = computed(() => (props.loading ? t('loading') : t('showMore'))); </script> <script lang="ts"> export default { name: 'listbox-lazy-load', i18n: { messages }, }; </script> <style lang="scss" scoped src="./listbox-lazy-load.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div> <bento-listbox-option v-if="!isStaticCategory" class="b-listbox-multi-select-category__option" :class="conditionalClass" :tabindex="tabindex" :aria-selected="ariaCheckedComputed !== 'false'" :aria-checked="ariaCheckedComputed" :aria-disabled="hasAllPropsDisabled" :aria-expanded="`${isOpen}`" role="treeitem" :aria-owns="categoryGroupId" :aria-labelledby="categoryNameId" @click="handleSelectAll" @enter-pressed="handleSelectAll" @space-pressed.prevent="handleSelectAll" @keydown.arrow-left.native="toggleOpen($event, false)" @keydown.arrow-right.native="toggleOpen($event, true)" > <decorative-checkbox :indeterminate="isIndeterminate" :disabled="hasAllPropsDisabled" :checked="isAllSelected" /> <span class="b-listbox-multi-select-category__label"> <bento-typography :id="categoryNameId" el="span" class="b-listbox-multi-select-category__label-text"> {{ category.label }} </bento-typography> <div aria-hidden="true" class="b-listbox-multi-select-category__label-chevron" data-testid="toggle-listbox" @click.capture.stop="toggleOpen" > <chevron-up-icon v-if="isOpen" :svg-title="chevronTitle" /> <chevron-down-icon v-else :svg-title="chevronTitle" /> </div> </span> </bento-listbox-option> <div v-else class="b-listbox-multi-select-category b-listbox-multi-select-category--static-category" aria-hidden="true" > <span class="b-listbox-multi-select-category__label"> <bento-typography el="span" variant="caption" class="b-listbox-multi-select-category__label-text"> {{ category.label }} </bento-typography> </span> </div> <div v-show="showItemChildren" :id="categoryGroupId" role="group" :aria-label="category.label"> <slot /> </div> </div> </template> <script lang="ts"> import { computed, defineComponent, type PropType, ref, toRef, watch } from 'vue'; import { BentoTypography } from '@/components/typography'; import { BentoListboxOption } from '../../../../../listbox-option'; import { DecorativeCheckbox } from '@/internal/decorative-checkbox'; import { type BentoListboxIsOptionDisabled, type BentoListboxMultiSelectValue, type BentoListboxOptionItem, type BentoListboxValue, } from '@/types/listbox'; import { useCheckboxSelectAll } from '@/composables'; import { ListboxMultiSelectCategoryClass, ListboxMultiSelectCategoryEvent, } from './listbox-multi-select-category.types'; import { useI18n } from '@/utils/ts/i18n'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import ChevronUpIcon from '@adyen/ui-assets-icons-16/vue/chevron-up'; import messages from './messages.json';
|
|
1
|
+
<template> <div> <bento-listbox-option v-if="!isStaticCategory" class="b-listbox-multi-select-category__option" :class="conditionalClass" :tabindex="tabindex" :aria-selected="ariaCheckedComputed !== 'false'" :aria-checked="ariaCheckedComputed" :aria-disabled="hasAllPropsDisabled" :aria-expanded="`${isOpen}`" role="treeitem" :aria-owns="categoryGroupId" :aria-labelledby="categoryNameId" @click="handleSelectAll" @enter-pressed="handleSelectAll" @space-pressed.prevent="handleSelectAll" @keydown.arrow-left.native="toggleOpen($event, false)" @keydown.arrow-right.native="toggleOpen($event, true)" > <decorative-checkbox :indeterminate="isIndeterminate" :disabled="hasAllPropsDisabled" :checked="isAllSelected" /> <span class="b-listbox-multi-select-category__label"> <bento-typography :id="categoryNameId" el="span" class="b-listbox-multi-select-category__label-text"> {{ category.label }} </bento-typography> <div aria-hidden="true" class="b-listbox-multi-select-category__label-chevron" data-testid="toggle-listbox" @click.capture.stop="toggleOpen" > <chevron-up-icon v-if="isOpen" :svg-title="chevronTitle" /> <chevron-down-icon v-else :svg-title="chevronTitle" /> </div> </span> </bento-listbox-option> <div v-else class="b-listbox-multi-select-category b-listbox-multi-select-category--static-category" aria-hidden="true" > <span class="b-listbox-multi-select-category__label"> <bento-typography el="span" variant="caption" class="b-listbox-multi-select-category__label-text"> {{ category.label }} </bento-typography> </span> </div> <!-- Nested options --> <div v-show="showItemChildren" :id="categoryGroupId" role="group" :aria-label="category.label"> <slot /> <!-- Nested items Lazy load --> <template v-if="category.lazyLoad?.hasMoreItems"> <bento-listbox-multi-select-nested-lazy-load ref="lazyLoadRef" :option="category" :index="optionIndex" /> </template> </div> </div> </template> <script lang="ts"> import { computed, defineComponent, type PropType, ref, toRef, watch } from 'vue'; import { BentoTypography } from '@/components/typography'; import { BentoListboxOption } from '../../../../../listbox-option'; import { DecorativeCheckbox } from '@/internal/decorative-checkbox'; import { type BentoListboxIsOptionDisabled, type BentoListboxMultiSelectValue, type BentoListboxOptionItem, type BentoListboxOptions, type BentoListboxValue, } from '@/types/listbox'; import { useCheckboxSelectAll } from '@/composables'; import { ListboxMultiSelectCategoryClass, ListboxMultiSelectCategoryEvent, } from './listbox-multi-select-category.types'; import { useI18n } from '@/utils/ts/i18n'; import { generateUid } from '@/core/utils/ts'; import { BentoListboxMultiSelectNestedLazyLoad } from '../listbox-multi-select-nested-lazy-load'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import ChevronUpIcon from '@adyen/ui-assets-icons-16/vue/chevron-up'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; /** * Listbox multi-select category list. * Allows the rendering of nested lists * * @example * import { ListboxMultiSelectCategory } from '@/internal/listbox'; * * export default { * components: { BentoListboxMultiSelectCategory }, * template: ` * <listbox-multi-select-category * :category="option" * :is-option-disabled="true" * :selected-items-list="['option-1']" * @select-category="$emit('select-category', option)" * @unselect-category="$emit('unselect-category', option)" * > * <listbox-multi-select-options * category-items * :items="[{ label: 'Option 1', value: 'option-1' }]" * :is-multi-level="true" * :selected-items-list="['option-']" * :searching="false" * @toggle-checkbox-selection="value => toggleCheckboxSelection(value)" * /> * </listbox-multi-select-category> * ` * } */ export default defineComponent({ i18n: { messages }, name: 'bento-listbox-multi-select-category', components: { BentoTypography, BentoListboxOption, BentoListboxMultiSelectNestedLazyLoad, ChevronDownIcon, ChevronUpIcon, DecorativeCheckbox, }, props: { /** * The option elements to populate the listbox with. * It must be an array of {@see BentoListboxOptionItem }. * * @property {string} value.label - Text to be displayed in the option * @property {string,number} value.value - Value of the option * @property {BentoListboxOptionItem} value.items - Category items list */ category: { type: Object as PropType<BentoListboxOptionItem>, required: true, }, /** * Function that allows the options to be disabled * * @type {BentoListboxIsOptionDisabled} * @param {BentoListboxOptionItem} option - listbox option object * @param {string} option.label - Option's label text * @param {string|number} option.value - Option's value * @param {unknown} option.data - Option's extra data to be passed to the default slot */ isOptionDisabled: { type: [Function] as PropType<BentoListboxIsOptionDisabled>, default: () => false, }, /** * Option's index within the Listbox `item` array. */ optionIndex: { type: Number, default: null, }, /** * The `input` value. * Providing an empty array will select no options. */ selectedItemsList: { type: Array as PropType<Array<BentoListboxValue>>, default: () => [], }, /** * If enabled, dropdown will display non-selectable, static categories */ staticCategories: { type: Boolean, default: false }, /** * Category's tabindex */ tabindex: { type: Number, default: -1 }, }, emits: [ ListboxMultiSelectCategoryEvent.SELECT_CATEGORY, ListboxMultiSelectCategoryEvent.UNSELECT_CATEGORY, ListboxMultiSelectCategoryEvent.TOGGLE_CATEGORY_VISIBILITY, ], setup(props, { emit }) { const isOpen = ref(false); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const categoryGroupId = generateUid('category-group'); const categoryNameId = generateUid('category-name'); const lazyLoadRef = ref(null); // This method should be used to determine if an option is disabled as it works for both vue 2 & 3. const vue2Vue3FixedisOptionDisabled = option => props.isOptionDisabled ? props.isOptionDisabled(option) : false; const hasAllPropsDisabled = computed(() => { // Do not disable empty categories if they are lazy loaded if (props.category.lazyLoad && props.category.items?.length === 0) { return false; } return props.category.items.every(option => vue2Vue3FixedisOptionDisabled(option)); }); const isStaticCategory = toRef(props, 'staticCategories'); // Display item children if is open state or if static categories is enabled const showItemChildren = computed(() => isOpen.value || props.staticCategories); const conditionalClass = computed(() => ({ [`b-listbox-multi-select-category--${ListboxMultiSelectCategoryClass.DISABLED}`]: hasAllPropsDisabled.value, })); const toggleOpen = (event: Event, open?: boolean) => { event.preventDefault(); // Load lazy categories if the items list is empty (0 items) when category is opened if (props.category.lazyLoad && props.category.items?.length === 0) { lazyLoadRef.value.showMore(event); } isOpen.value = open !== undefined ? open : !isOpen.value; emit(ListboxMultiSelectCategoryEvent.TOGGLE_CATEGORY_VISIBILITY, props.category.value); }; const optionStates = (option: BentoListboxOptionItem) => { const isDisabled = vue2Vue3FixedisOptionDisabled(option); const isPreSelected = (props.selectedItemsList as BentoListboxMultiSelectValue)?.includes(option.value) || false; return { isDisabled, isPreSelected }; }; const enabledItems = computed(() => props.category.items.filter(option => { const { isDisabled, isPreSelected } = optionStates(option); return (isDisabled && isPreSelected) || !isDisabled; }) ); const visibleSelectedList = computed<BentoListboxOptions>(() => enabledItems.value?.filter(({ value }) => props.selectedItemsList?.includes(value)) ); const chevronTitle = computed(() => { const messageName = isOpen.value ? 'categoryOpened' : 'categoryClosed'; return t(messageName, { category: props.category.label }) as string; }); const selectAll = () => { emit(ListboxMultiSelectCategoryEvent.SELECT_CATEGORY); }; const unselectAll = () => { emit(ListboxMultiSelectCategoryEvent.UNSELECT_CATEGORY); }; const isLazySelectedByValue = computed( () => !!props.category.lazyLoad && props.selectedItemsList.includes(props.category.value) ); const { isAllSelected: isAllChildrenSelected, isIndeterminate: isChildrenIndeterminate, toggleSelectAll, } = useCheckboxSelectAll(visibleSelectedList, enabledItems, selectAll, unselectAll); const isAllSelected = computed(() => isLazySelectedByValue.value || isAllChildrenSelected.value); const isIndeterminate = computed(() => !isLazySelectedByValue.value && isChildrenIndeterminate.value); const ariaCheckedComputed = computed(() => { if (isIndeterminate.value) { return 'mixed'; } return `${isAllSelected.value}`; }); const isSelectAllOn = ref(isAllSelected.value); const handleSelectAll = () => { isSelectAllOn.value = !isSelectAllOn.value; toggleSelectAll(isSelectAllOn.value); }; watch(isAllSelected, () => { isSelectAllOn.value = isAllSelected.value; }); return { // Refs lazyLoadRef, // Values chevronTitle, conditionalClass, hasAllPropsDisabled, isOpen, isAllSelected, isIndeterminate, isStaticCategory, categoryGroupId, categoryNameId, // Computed showItemChildren, ariaCheckedComputed, // Methods handleSelectAll, toggleOpen, toggleSelectAll, }; }, }); </script> <style lang="scss" scoped src="./listbox-multi-select-category.scss" />
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import type { BentoListboxOptionItem } from '@/types/listbox'; export interface ListboxMultiSelectNestedLazyLoadProps { /** * Index of the option that triggers the nested lazy-load event. */ index: number; /** * Option that contains the nested lazy-load configuration. */ option: BentoListboxOptionItem; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<template> <div role="treeitem" :tabindex="0" @keydown.enter.prevent="showMore" @keydown.space.prevent="showMore"> <bento-empty-state v-if="failedAndRetry" variant="basic" class="b-listbox-multi-select-nested-lazy-load__error-message" > {{ t('thereWasAnErrorLoadingNewItems') }} </bento-empty-state> <bento-listbox-lazy-load type="button" :tabindex="-1" condensed :has-more-items="option.lazyLoad?.hasMoreItems" :loading="loading" @show-more="showMore" /> </div> </template> <script setup lang="ts"> import { ref } from 'vue'; import { BentoEmptyState } from '@/components/empty-state'; import { BentoListboxLazyLoad } from '@/internal/listbox/components/listbox-lazy-load'; import { useI18n } from '@/utils/ts/i18n'; import type { ListboxMultiSelectNestedLazyLoadProps } from './listbox-multi-select-nested-lazy-load.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const loading = ref(false); const failedAndRetry = ref(false); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<ListboxMultiSelectNestedLazyLoadProps>(), {}); async function showMore(event: Event) { event.stopImmediatePropagation(); if (loading.value) { return; } failedAndRetry.value = false; loading.value = true; try { await props.option.lazyLoad?.lazyLoadEvent({ value: props.option.value, index: props.index, }); } catch { failedAndRetry.value = true; } loading.value = false; } defineExpose({ /** * Trigger the lazy load event programmatically. */ showMore, }); </script> <script lang="ts"> export default { name: 'listbox-multi-select-nested-lazy-load', i18n: { messages }, }; </script> <style lang="scss" scoped src="./listbox-multi-select-nested-lazy-load.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div> <div v-for="option in items" :key="`selected-${option.value}`"> <template v-if="searching && isMultiLevel"> <bento-listbox-option class="b-listbox-multi-select-options" :class="conditionalClasses" :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :tabindex="computedTabindex(option.value)" :aria-selected="`${isOptionSelected(option)}`" :aria-checked="`${isOptionSelected(option)}`" :aria-disabled="isOptionDisabled ? isOptionDisabled(option) : null" :role="$attrs['role'] || 'option'" @click="toggleSearchOption(option)" @enter-pressed="toggleSearchOption(option)" @space-pressed.prevent="toggleSearchOption(option)" > <decorative-checkbox :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :checked="isOptionSelected(option)" /> <slot v-bind="option"> <span class="b-listbox-multi-select__option-text"> <bento-typography el="span">{{ option.label }}</bento-typography> <bento-typography v-if="shouldShowDescription(option)" class="b-listbox-multi-select__option-description" el="span" > {{ option.description }} </bento-typography> </span> </slot> </bento-listbox-option> </template> <template v-else> <!-- Category --> <template v-if="option.items"> <listbox-multi-select-category :category="option" :static-categories="staticCategories" :is-option-disabled="isOptionDisabled" :selected-items-list="selectedItemsList" :tabindex="computedTabindex(option.value)" @select-category="$emit(ListboxMultiSelectOptionsEvent.SELECT_CATEGORY, option)" @unselect-category="$emit(ListboxMultiSelectOptionsEvent.UNSELECT_CATEGORY, option)" @toggle-category-visibility=" $emit(ListboxMultiSelectOptionsEvent.TOGGLE_CATEGORY_VISIBILITY, $event) " > <listbox-multi-select-options category-items :items="option.items" :is-option-disabled="isOptionDisabled" :items-index="itemsIndex" :focused-index="focusedIndex" :is-multi-level="isMultiLevel" :static-categories="staticCategories" :selected-items-list="selectedItemsList" :searching="searching" :role="$attrs['role'] || 'option'" @toggle-checkbox-selection="value => toggleCheckboxSelection(value)" /> </listbox-multi-select-category> </template> <!-- Option --> <bento-listbox-option v-else class="b-listbox-multi-select-options" :class="conditionalClasses" :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :tabindex="computedTabindex(option.value)" :aria-selected="`${isOptionSelected(option)}`" :aria-checked="`${isOptionSelected(option)}`" :aria-disabled="isOptionDisabled ? isOptionDisabled(option) : null" :role="$attrs['role'] || 'option'" @click="toggleCheckboxSelection(option)" @enter-pressed="toggleCheckboxSelection(option)" @space-pressed.prevent="toggleCheckboxSelection(option)" > <decorative-checkbox :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :checked="isOptionSelected(option)" /> <slot v-bind="option"> <span class="b-listbox-multi-select__option-text"> <bento-typography el="span">{{ option.label }}</bento-typography> <bento-typography v-if="shouldShowDescription(option)" class="b-listbox-multi-select__option-description" el="span" > {{ option.description }} </bento-typography> </span> </slot> </bento-listbox-option> </template> </div> </div> </template> <script lang="ts"> import { computed, defineComponent, type PropType } from 'vue'; import { BentoListboxOption } from '../../../listbox-option'; import { BentoTypography } from '@/components/typography'; import { ListboxMultiSelectCategory } from './components/listbox-multi-select-category'; import { DecorativeCheckbox } from '@/internal/decorative-checkbox'; import { BentoListboxEvent, type BentoListboxIsOptionDisabled, type BentoListboxOptionItem, type BentoListboxOptions, type BentoListboxValue, } from '@/types/listbox'; import { ListboxMultiSelectOptionsEvent } from './listbox-multi-select-options.types'; /** * Listbox multi-select options list * * @example * import { ListboxMultiSelectOptions } from './components/listbox-multi-select-options'; * * export default { * components: { ListboxMultiSelect }, * template: ` * <listbox-multi-select * :items="[{ label: 'Option 1', value: 'option-1' }]" * :selected-values="['option-1']" * /> * ` * } */ export default defineComponent({ name: 'listbox-multi-select-options', components: { BentoListboxOption, BentoTypography, ListboxMultiSelectCategory, DecorativeCheckbox, }, inheritAttrs: false, props: { /** * Indicates if the items listed are inside a category * so they can be padded to right by one level in recursion. * Items of categories are automatically defined as "categoryItems" */ categoryItems: { type: Boolean, default: false }, /** * Current focused option index */ focusedIndex: { type: Number, default: 0 }, /** * Function that allows the options to be disabled * * @type {BentoListboxIsOptionDisabled} * @param {BentoListboxOptionItem} option - listbox option object * @param {string} option.label - Option's label text * @param {string|number} option.value - Option's value * @param {unknown} option.data - Option's extra data to be passed to the default slot */ isOptionDisabled: { type: Function as PropType<BentoListboxIsOptionDisabled>, default: () => false, }, /** * The option elements to populate the listbox with. * It must be an array of {@see BentoListboxOptionItem }. * * @property {string} value.label - Text to be displayed in the option * @property {string,number} value.value - Value of the option */ items: { type: Array as PropType<BentoListboxOptions>, required: true, }, /** * Indicates if the elements options are inside a multi-level category */ isMultiLevel: { type: Boolean, default: false }, /** * Flag that indicates that an external search it's being made * to filter out the items inside the the multi-select listbox * * If enabled, hides the "Select all" checkbox in "multiple" mode. */ searching: { type: Boolean, default: false }, /** * List of selected items. * Providing an empty array will select no options. */ selectedItemsList: { type: Array as PropType<Array<BentoListboxValue>>, default: () => [], }, /** * If enabled, dropdown will display non-selectable, static categories */ staticCategories: { type: Boolean, default: false }, /** * Object with the index of each item. */ itemsIndex: { type: Object as PropType<Record<string, number>>, default: () => ({}), }, }, emits: [ ListboxMultiSelectOptionsEvent.TOGGLE_CHECKBOX_SELECTION, ListboxMultiSelectOptionsEvent.SELECT_CATEGORY, ListboxMultiSelectOptionsEvent.UNSELECT_CATEGORY, ListboxMultiSelectOptionsEvent.TOGGLE_CATEGORY_VISIBILITY, ], setup(props, { emit }) { const toggleCheckboxSelection = (option: BentoListboxOptionItem) => { if (props.isOptionDisabled(option)) { return; } emit(ListboxMultiSelectOptionsEvent.TOGGLE_CHECKBOX_SELECTION, option); }; const isCategory = (option: BentoListboxOptionItem) => !!option.items; const shouldShowDescription = (option: BentoListboxOptionItem) => { const isSearchingMultiLevel = props.isMultiLevel && props.searching; return option.description && (!props.isMultiLevel || isSearchingMultiLevel); }; const isOptionSelected = option => { if (option.items) { return !!option.items.find(item => props.selectedItemsList.includes(item.value)); } return props.selectedItemsList.includes(option.value); }; const toggleSearchOption = option => { if (isCategory(option)) { const event = isOptionSelected(option) ? ListboxMultiSelectOptionsEvent.UNSELECT_CATEGORY : ListboxMultiSelectOptionsEvent.SELECT_CATEGORY; emit(event, option); } else { toggleCheckboxSelection(option); } }; const conditionalClasses = computed(() => ({ 'b-listbox-multi-select-options__static-category-option': props.staticCategories, 'b-listbox-multi-select-options__category-option': props.categoryItems, })); const computedTabindex = value => (props.itemsIndex[value] === props.focusedIndex ? 0 : -1); return { // Values conditionalClasses, // Methods isOptionSelected, shouldShowDescription, toggleCheckboxSelection, toggleSearchOption, computedTabindex, // Enums BentoListboxEvent, ListboxMultiSelectOptionsEvent, }; }, }); </script> <style lang="scss" scoped src="./listbox-multi-select-options.scss" />
|
|
1
|
+
<template> <div> <div v-for="option in items" :key="`selected-${option.value}`"> <template v-if="searching && isMultiLevel"> <bento-listbox-option class="b-listbox-multi-select-options" :class="conditionalClasses" :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :tabindex="computedTabindex(option.value)" :aria-selected="`${isOptionSelected(option)}`" :aria-checked="`${isOptionSelected(option)}`" :aria-disabled="isOptionDisabled ? isOptionDisabled(option) : null" :role="$attrs['role'] || 'option'" @click="toggleSearchOption(option)" @enter-pressed="toggleSearchOption(option)" @space-pressed.prevent="toggleSearchOption(option)" > <decorative-checkbox :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :checked="isOptionSelected(option)" /> <slot v-bind="option"> <span class="b-listbox-multi-select__option-text"> <bento-typography el="span">{{ option.label }}</bento-typography> <bento-typography v-if="shouldShowDescription(option)" class="b-listbox-multi-select__option-description" el="span" > {{ option.description }} </bento-typography> </span> </slot> </bento-listbox-option> </template> <template v-else> <!-- Category --> <template v-if="option.items"> <listbox-multi-select-category :category="option" :static-categories="staticCategories" :is-option-disabled="isOptionDisabled" :selected-items-list="selectedItemsList" :option-index="itemsIndex[option.value]" :tabindex="computedTabindex(option.value)" @select-category="$emit(ListboxMultiSelectOptionsEvent.SELECT_CATEGORY, option)" @unselect-category="$emit(ListboxMultiSelectOptionsEvent.UNSELECT_CATEGORY, option)" @toggle-category-visibility=" $emit(ListboxMultiSelectOptionsEvent.TOGGLE_CATEGORY_VISIBILITY, $event) " > <listbox-multi-select-options category-items :items="option.items" :is-option-disabled="isOptionDisabled" :items-index="itemsIndex" :focused-index="focusedIndex" :is-multi-level="isMultiLevel" :static-categories="staticCategories" :selected-items-list="selectedItemsList" :searching="searching" :parent-category="option" :role="$attrs['role'] || 'option'" @toggle-checkbox-selection="value => toggleCheckboxSelection(value)" /> </listbox-multi-select-category> </template> <!-- Option --> <bento-listbox-option v-else class="b-listbox-multi-select-options" :class="conditionalClasses" :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :tabindex="computedTabindex(option.value)" :aria-selected="`${isOptionSelected(option)}`" :aria-checked="`${isOptionSelected(option)}`" :aria-disabled="isOptionDisabled ? isOptionDisabled(option) : null" :role="$attrs['role'] || 'option'" @click="toggleCheckboxSelection(option)" @enter-pressed="toggleCheckboxSelection(option)" @space-pressed.prevent="toggleCheckboxSelection(option)" > <decorative-checkbox :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :checked="isOptionSelected(option)" /> <slot v-bind="option"> <span class="b-listbox-multi-select__option-text"> <bento-typography el="span">{{ option.label }}</bento-typography> <bento-typography v-if="shouldShowDescription(option)" class="b-listbox-multi-select__option-description" el="span" > {{ option.description }} </bento-typography> </span> </slot> </bento-listbox-option> </template> </div> </div> </template> <script lang="ts"> import { computed, defineComponent, type PropType } from 'vue'; import { BentoListboxOption } from '../../../listbox-option'; import { BentoTypography } from '@/components/typography'; import { ListboxMultiSelectCategory } from './components/listbox-multi-select-category'; import { DecorativeCheckbox } from '@/internal/decorative-checkbox'; import { BentoListboxEvent, type BentoListboxIsOptionDisabled, type BentoListboxOptionItem, type BentoListboxOptions, type BentoListboxValue, } from '@/types/listbox'; import { ListboxMultiSelectOptionsEvent } from './listbox-multi-select-options.types'; /** * Listbox multi-select options list * * @example * import { ListboxMultiSelectOptions } from './components/listbox-multi-select-options'; * * export default { * components: { ListboxMultiSelect }, * template: ` * <listbox-multi-select * :items="[{ label: 'Option 1', value: 'option-1' }]" * :selected-values="['option-1']" * /> * ` * } */ export default defineComponent({ name: 'listbox-multi-select-options', components: { BentoListboxOption, BentoTypography, ListboxMultiSelectCategory, DecorativeCheckbox, }, inheritAttrs: false, props: { /** * Indicates if the items listed are inside a category * so they can be padded to right by one level in recursion. * Items of categories are automatically defined as "categoryItems" */ categoryItems: { type: Boolean, default: false }, /** * Current focused option index */ focusedIndex: { type: Number, default: 0 }, /** * Function that allows the options to be disabled * * @type {BentoListboxIsOptionDisabled} * @param {BentoListboxOptionItem} option - listbox option object * @param {string} option.label - Option's label text * @param {string|number} option.value - Option's value * @param {unknown} option.data - Option's extra data to be passed to the default slot */ isOptionDisabled: { type: Function as PropType<BentoListboxIsOptionDisabled>, default: () => false, }, /** * The option elements to populate the listbox with. * It must be an array of {@see BentoListboxOptionItem }. * * @property {string} value.label - Text to be displayed in the option * @property {string,number} value.value - Value of the option */ items: { type: Array as PropType<BentoListboxOptions>, required: true, }, /** * Indicates if the elements options are inside a multi-level category */ isMultiLevel: { type: Boolean, default: false }, /** * Flag that indicates that an external search it's being made * to filter out the items inside the the multi-select listbox * * If enabled, hides the "Select all" checkbox in "multiple" mode. */ searching: { type: Boolean, default: false }, /** * List of selected items. * Providing an empty array will select no options. */ selectedItemsList: { type: Array as PropType<Array<BentoListboxValue>>, default: () => [], }, /** * If enabled, dropdown will display non-selectable, static categories */ staticCategories: { type: Boolean, default: false }, /** * Object with the index of each item. */ itemsIndex: { type: Object as PropType<Record<string, number>>, default: () => ({}), }, /** * The parent category of the items. */ parentCategory: { type: Object as PropType<BentoListboxOptionItem>, default: null, }, }, emits: [ ListboxMultiSelectOptionsEvent.TOGGLE_CHECKBOX_SELECTION, ListboxMultiSelectOptionsEvent.SELECT_CATEGORY, ListboxMultiSelectOptionsEvent.UNSELECT_CATEGORY, ListboxMultiSelectOptionsEvent.TOGGLE_CATEGORY_VISIBILITY, ], setup(props, { emit }) { const toggleCheckboxSelection = (option: BentoListboxOptionItem) => { if (props.isOptionDisabled(option)) { return; } emit(ListboxMultiSelectOptionsEvent.TOGGLE_CHECKBOX_SELECTION, option); }; const isCategory = (option: BentoListboxOptionItem) => !!option.items; const shouldShowDescription = (option: BentoListboxOptionItem) => { const isSearchingMultiLevel = props.isMultiLevel && props.searching; return option.description && (!props.isMultiLevel || isSearchingMultiLevel); }; const isOptionSelected = option => { const isParentLazySelected = !!props.parentCategory?.lazyLoad && props.selectedItemsList.includes(props.parentCategory.value); if (option.items) { const isLazyCategorySelected = !!option.lazyLoad && props.selectedItemsList.includes(option.value); return ( isParentLazySelected || isLazyCategorySelected || !!option.items.find(item => props.selectedItemsList.includes(item.value)) ); } return isParentLazySelected || props.selectedItemsList.includes(option.value); }; const toggleSearchOption = option => { if (isCategory(option)) { const event = isOptionSelected(option) ? ListboxMultiSelectOptionsEvent.UNSELECT_CATEGORY : ListboxMultiSelectOptionsEvent.SELECT_CATEGORY; emit(event, option); } else { toggleCheckboxSelection(option); } }; const conditionalClasses = computed(() => ({ 'b-listbox-multi-select-options__static-category-option': props.staticCategories, 'b-listbox-multi-select-options__category-option': props.categoryItems, })); const computedTabindex = value => (props.itemsIndex[value] === props.focusedIndex ? 0 : -1); return { // Values conditionalClasses, // Methods isOptionSelected, shouldShowDescription, toggleCheckboxSelection, toggleSearchOption, computedTabindex, // Enums BentoListboxEvent, ListboxMultiSelectOptionsEvent, }; }, }); </script> <style lang="scss" scoped src="./listbox-multi-select-options.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div ref="listboxRef" class="b-listbox-multi-select"> <!-- Selected options --> <div v-if="showSelectedItems"> <ul v-bento-keyboard-navigation-directive class="b-listbox-multi-select__chips" v-on="chipsSectionListeners" > <li v-for="item in selectedOptions" :key="item.value"> <bento-chip ref="selectedListboxItemsRef" :disabled="isOptionDisabled(item)" :label="item.label" condensed @click="item.items ? unselectAllCategory(item) : toggleCheckboxSelection('select', item)" /> </li> </ul> <hr class="b-listbox-multi-select__divider" aria-hidden="true" /> </div> <div v-bento-keyboard-navigation-directive v-bind="ariaAttributes" :role="computedRole" v-on="listboxListeners"> <!-- Select all --> <bento-listbox-option v-if="showSelectAllCheckbox" ref="selectAllListboxItemsRef" class="b-listbox-multi-select__option" :tabindex="focusedIndex === 0 ? 0 : -1" :aria-selected="ariaCheckedComputed !== 'false'" :aria-checked="ariaCheckedComputed" :role="computedOptionRole" @click="handleSelectAll" @enter-pressed="handleSelectAll" @space-pressed.prevent="handleSelectAll" > <decorative-checkbox :checked="isAllSelected" :indeterminate="isIndeterminate" /> <bento-typography el="span"> {{ selectAllCheckboxText }} </bento-typography> </bento-listbox-option> <hr v-if="showSelectAllCheckbox" class="b-listbox-multi-select__divider" aria-hidden="true" /> <!-- Available options --> <listbox-multi-select-options ref="listboxItemsRef" :items="items" :is-option-disabled="isOptionDisabled" :is-multi-level="isMultiLevelSelect" :static-categories="staticCategories" :searching="searching" :selected-items-list="selectedItemsList" :focused-index="focusedIndex" :items-index="computedItemsIndex" :role="computedOptionRole" @toggle-checkbox-selection="toggleCheckboxSelection('select', $event)" @select-category="selectAllCategory" @unselect-category="unselectAllCategory" @toggle-category-visibility="toggleCategoryVisibility" @space-pressed="toggleCheckboxSelection(BentoListboxEvent.SPACE, $event)" @tab-pressed="toggleCheckboxSelection(BentoListboxEvent.TAB, $event)" @enter-pressed="toggleCheckboxSelection(BentoListboxEvent.ENTER, $event)" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </listbox-multi-select-options> </div> </div> </template> <script setup lang="ts"> import { computed, type HTMLAttributes, nextTick, ref, toRef, useAttrs, useSlots, watch } from 'vue'; import { isVue2 } from 'vue-demi'; import { DecorativeCheckbox } from '@/internal/decorative-checkbox'; import { BentoTypography } from '@/components/typography'; import { BentoChip } from '@/components/chip'; import { BentoListboxOption } from '../listbox-option'; import { ListboxMultiSelectOptions } from './components/listbox-multi-select-options'; import { useCheckboxSelectAll } from '@/composables'; import { useListboxKeyboardNavigation } from '../../composables/useListboxKeyboardNavigation'; import { useMultiLevelItems } from '../../composables/use-multi-level-items/use-multi-level-items'; import { useVisibleDomOptions } from '@/internal/listbox/composables/use-visible-dom-options/use-visible-dom-options'; import { useI18n } from '@/utils/ts/i18n'; import { BentoKeyboardNavigationDirective as vBentoKeyboardNavigationDirective } from '@/directives'; import { BentoListboxEvent, type BentoListboxMultiSelectValue, type BentoListboxOptionItem, type BentoListboxOptions, type BentoListboxValue, } from '@/types/listbox'; import { type Booleanish } from '@/types/prop-types'; import { BentoKeyboardNavigationKeyDownEvent } from '@/types/keyboard-navigation'; import { type ListboxMultiSelectProps } from './listbox-multi-select.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<ListboxMultiSelectProps>(), { hasMoreItems: false, isOptionDisabled: isVue2 ? () => () => false : () => false, lazyLoadType: 'none', searching: false, staticCategories: false, }); const emit = defineEmits<{ /** * Emits when the selection changes */ (e: 'select', value: Array<BentoListboxOptionItem>): void; }>(); const { t, n } = useI18n<{ message: MessageSchema }>({ messages }); const slots = useSlots(); const attrs = useAttrs(); const ariaAttributes = computed( () => ({ 'aria-label': attrs['aria-label']?.toString() as string, 'aria-busy': attrs['aria-busy']?.toString() as Booleanish, 'aria-atomic': attrs['aria-atomic']?.toString() as Booleanish, 'aria-live': attrs['aria-live']?.toString() as string, 'aria-multiselectable': true, }) as HTMLAttributes ); const listboxRef = ref(null); const selectAllListboxItemsRef = ref(null); const selectedListboxItemsRef = ref(null); const listboxItemsRef = ref(null); const selectedItemsList = computed<Array<BentoListboxValue>>(() => props.selectedValues.map(({ value }) => value)); const { isMultiLevelSelect, flattenedItemsList } = useMultiLevelItems(toRef(props, 'items')); const isTreePattern = computed(() => isMultiLevelSelect.value && !props.staticCategories); const computedRole = computed(() => (isTreePattern.value ? 'tree' : 'listbox')); const computedOptionRole = computed(() => (isTreePattern.value ? 'treeitem' : 'option')); const optionStates = (option: BentoListboxOptionItem) => { const isDisabled = props.isOptionDisabled(option); const isPreSelected = (selectedItemsList.value as BentoListboxMultiSelectValue)?.includes(option.value) || false; return { isDisabled, isPreSelected }; }; // Items that are not disabled and pre-selected disabled items const enabledItems = computed(() => { if (props.searching && isMultiLevelSelect) { return props.items; } const fullItemsList = flattenedItemsList.value.filter(option => { const { isDisabled, isPreSelected } = optionStates(option); return (isDisabled && isPreSelected) || !isDisabled; }); props.selectedValues.forEach(selectedValue => { if (!fullItemsList.find(item => item.value === selectedValue.value)) { fullItemsList.push(selectedValue); } }); return fullItemsList; }); const visibleSelectedList = computed( () => enabledItems.value?.filter(option => { if (option.items && isMultiLevelSelect.value && props.searching) { const amountOfSelectedCategoryOptions = option.items.filter(categoryOption => (props.selectedValues ?? []).includes(categoryOption.value) ).length; return amountOfSelectedCategoryOptions === option.items.length; } return selectedItemsList.value?.includes(option.value); // Check for category }) as BentoListboxMultiSelectValue ); const calculateTotalItems = () => { if (isMultiLevelSelect.value && !props.searching) { return flattenedItemsList.value.length; } return props.items.length; }; const selectAllCheckboxText = computed(() => { const possibleSelections = enabledItems.value.length; const total = calculateTotalItems(); const amountSelectedValues = visibleSelectedList.value?.length || 0; if (amountSelectedValues === 0) { return t('selectAll', { total: n(possibleSelections) }); } if (amountSelectedValues === total) { return t('allSelected', { total: n(total) }); } return amountSelectedValues <= total ? t('numberOfTotalSelected', { number: n(amountSelectedValues), total: n(total) }) : t('numbersSelected', { number: n(amountSelectedValues) }); }); const selectAll = (selectableItems: BentoListboxOptions) => { const filterOutDisabledAndNotPreselectedOptions = option => { const { isDisabled, isPreSelected } = optionStates(option); return (isDisabled && isPreSelected) || !isDisabled; }; // Adding or removing this list of values const addOrRemoveList = selectableItems .filter(filterOutDisabledAndNotPreselectedOptions) .filter(item => !selectedItemsList.value.includes(item.value)); // MAKE SURE NOT TO ADD DUPLICATES emit('select', [...props.selectedValues, ...addOrRemoveList]); }; const unselectAll = (selectableItems: BentoListboxOptions) => { const findEnabledUnselectedItems = option => { const { isDisabled, isPreSelected } = optionStates(option); return !(isDisabled && isPreSelected); }; const unselectedItemsList = selectableItems.filter(findEnabledUnselectedItems).map(({ value }) => value); const newSelectedList = props.selectedValues.filter(item => !unselectedItemsList?.includes(item.value)); emit('select', newSelectedList); }; const selectAllItems = () => { selectAll(flattenedItemsList.value); }; const unselectAllItems = () => { unselectAll(flattenedItemsList.value); }; const { isAllSelected, isIndeterminate, toggleSelectAll } = useCheckboxSelectAll( visibleSelectedList, enabledItems, selectAllItems, unselectAllItems ); const ariaCheckedComputed = computed(() => { if (isIndeterminate.value) { return 'mixed'; } return `${isAllSelected.value}`; }); const isSelectAllOn = ref(isAllSelected.value); const handleSelectAll = () => { isSelectAllOn.value = !isSelectAllOn.value; toggleSelectAll(isSelectAllOn.value); }; watch(isAllSelected, () => { isSelectAllOn.value = isAllSelected.value; }); const selectAllCategory = (category: BentoListboxOptionItem) => { selectAll(category.items); }; const unselectAllCategory = (category: BentoListboxOptionItem) => { unselectAll(category.items); }; const toggleCheckboxSelection = (eventName, selectedOption: BentoListboxOptionItem) => { let exists = false; const newSelectedList = props.selectedValues.filter(({ value }) => { if (selectedOption.value === value) { exists = true; return false; } return true; }); if (!exists) { newSelectedList.push(selectedOption); } emit(eventName, newSelectedList); }; const selectedOptions = computed(() => { if (!isMultiLevelSelect.value) { return props.selectedValues; } const isOptionSelected = (option: BentoListboxOptionItem): boolean => (selectedItemsList.value as unknown as Array<string | number>).includes(option.value); const categoryOptionsSelected = (categoryOptions: Array<BentoListboxOptionItem>): boolean => !props.staticCategories && categoryOptions.every(option => isOptionSelected(option)); const optionsFlat = props.items.flatMap(option => option.items && !categoryOptionsSelected(option.items) ? option.items : option ); const options = optionsFlat.filter(option => { if (option.items) { return true; } return isOptionSelected(option); }); // Append selected values not present in the current items list (e.g. not yet fetched) const knownValues = new Set<BentoListboxValue>(); optionsFlat.forEach(option => { knownValues.add(option.value); option.items?.forEach(sub => knownValues.add(sub.value)); }); const unfetchedSelected = props.selectedValues.filter(sv => !knownValues.has(sv.value)); return [...options, ...unfetchedSelected]; }); const showSelectedItems = computed(() => { const isLazyLoad = ['automatic', 'button'].includes(props.lazyLoadType); const hasAllItemsSelected = props.selectedValues?.length > 0 && visibleSelectedList.value?.length !== flattenedItemsList.value?.length; if (isLazyLoad && props.hasMoreItems) { return !!props.selectedValues?.length; } return hasAllItemsSelected; }); const showSelectAllCheckbox = computed(() => { const isLazyLoad = ['automatic', 'button'].includes(props.lazyLoadType); if (!isLazyLoad) { return true; } return !props.hasMoreItems; }); // Keyboard navigation const expandedCategories = ref([]); const { visibleDomOptions, updateVisibleDomOptions } = useVisibleDomOptions( listboxRef, toRef(props, 'items'), computedOptionRole ); const toggleCategoryVisibility = category => { if (expandedCategories.value.includes(category)) { expandedCategories.value = expandedCategories.value.filter(item => item !== category); } else { expandedCategories.value.push(category); } nextTick(() => { updateVisibleDomOptions(); }); }; const focusItem = (nextFocusedItemIndex: number) => { visibleDomOptions.value[nextFocusedItemIndex]?.focus(); }; const { moveToNextElement, moveToPreviousElement, moveToFirstElement, moveToLastElement, preventDefault, focusedIndex, setListboxFocusedIndex, } = useListboxKeyboardNavigation( { items: visibleDomOptions, }, focusItem ); const listboxListeners = { [BentoKeyboardNavigationKeyDownEvent.ARROW_UP]: (event: CustomEvent<KeyboardEvent>) => moveToPreviousElement(event), [BentoKeyboardNavigationKeyDownEvent.ARROW_DOWN]: (event: CustomEvent<KeyboardEvent>) => moveToNextElement(event), [BentoKeyboardNavigationKeyDownEvent.HOME]: moveToFirstElement, [BentoKeyboardNavigationKeyDownEvent.END]: moveToLastElement, }; defineExpose({ /** * Method to set the index of the item to be focused on * Used for focus management to be kept in-sync with parent e.g. dropdowns */ setListboxFocusedIndex, }); const chipsSectionListeners = { [BentoKeyboardNavigationKeyDownEvent.ARROW_UP]: preventDefault, [BentoKeyboardNavigationKeyDownEvent.ARROW_DOWN]: preventDefault, }; const computedItemsIndex = computed(() => { let itemIndex = showSelectAllCheckbox.value ? 1 : 0; const indexMap = {}; props.items.forEach(item => { let subItemsVisible = expandedCategories.value.includes(item.value); if (item.items) { if (props.staticCategories) { subItemsVisible = true; } else { indexMap[item.value] = itemIndex++; } if (subItemsVisible) { item.items.forEach(subItem => (indexMap[subItem.value] = itemIndex++)); } } else { indexMap[item.value] = itemIndex++; } }); return indexMap; }); </script> <script lang="ts"> /** * Listbox multi-select options list * * @example * import { BentoListboxMultiSelect } from '@adyen/bento-vue2'; * * export default { * components: { BentoListboxMultiSelect }, * template: ` * <bento-listbox-multi-select * :items="[{ label: 'Option 1', value: 'option-1' }]" * :selected-values="['option-1']" * /> * ` * } */ export default { i18n: { messages } }; </script> <style lang="scss" scoped src="./listbox-multi-select.scss" />
|
|
1
|
+
<template> <div ref="listboxRef" class="b-listbox-multi-select"> <!-- Selected options --> <div v-if="showSelectedItems"> <ul v-bento-keyboard-navigation-directive class="b-listbox-multi-select__chips" v-on="chipsSectionListeners" > <li v-for="item in selectedOptions" :key="item.value"> <bento-chip ref="selectedListboxItemsRef" :disabled="isOptionDisabled(item)" :label="item.label" condensed @click="item.items ? unselectAllCategory(item) : toggleCheckboxSelection('select', item)" /> </li> </ul> <hr class="b-listbox-multi-select__divider" aria-hidden="true" /> </div> <div v-bento-keyboard-navigation-directive v-bind="ariaAttributes" :role="computedRole" v-on="listboxListeners"> <!-- Select all --> <bento-listbox-option v-if="showSelectAllCheckbox" ref="selectAllListboxItemsRef" class="b-listbox-multi-select__option" :tabindex="focusedIndex === 0 ? 0 : -1" :aria-selected="ariaCheckedComputed !== 'false'" :aria-checked="ariaCheckedComputed" :role="computedOptionRole" @click="handleSelectAll" @enter-pressed="handleSelectAll" @space-pressed.prevent="handleSelectAll" > <decorative-checkbox :checked="isAllSelected" :indeterminate="isIndeterminate" /> <bento-typography el="span"> {{ selectAllCheckboxText }} </bento-typography> </bento-listbox-option> <hr v-if="showSelectAllCheckbox" class="b-listbox-multi-select__divider" aria-hidden="true" /> <!-- Available options --> <listbox-multi-select-options ref="listboxItemsRef" :items="items" :is-option-disabled="isOptionDisabled" :is-multi-level="isMultiLevelSelect" :static-categories="staticCategories" :searching="searching" :selected-items-list="selectedItemsList" :focused-index="focusedIndex" :items-index="computedItemsIndex" :role="computedOptionRole" @toggle-checkbox-selection="toggleCheckboxSelection('select', $event)" @select-category="selectAllCategory" @unselect-category="unselectAllCategory" @toggle-category-visibility="toggleCategoryVisibility" @space-pressed="toggleCheckboxSelection(BentoListboxEvent.SPACE, $event)" @tab-pressed="toggleCheckboxSelection(BentoListboxEvent.TAB, $event)" @enter-pressed="toggleCheckboxSelection(BentoListboxEvent.ENTER, $event)" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </listbox-multi-select-options> </div> </div> </template> <script setup lang="ts"> import { computed, type HTMLAttributes, nextTick, ref, toRef, useAttrs, useSlots, watch } from 'vue'; import { isVue2 } from 'vue-demi'; import { DecorativeCheckbox } from '@/internal/decorative-checkbox'; import { BentoTypography } from '@/components/typography'; import { BentoChip } from '@/components/chip'; import { BentoListboxOption } from '../listbox-option'; import { ListboxMultiSelectOptions } from './components/listbox-multi-select-options'; import { useCheckboxSelectAll } from '@/composables'; import { useListboxKeyboardNavigation } from '../../composables/useListboxKeyboardNavigation'; import { useMultiLevelItems } from '../../composables/use-multi-level-items/use-multi-level-items'; import { useVisibleDomOptions } from '@/internal/listbox/composables/use-visible-dom-options/use-visible-dom-options'; import { useI18n } from '@/utils/ts/i18n'; import { BentoKeyboardNavigationDirective as vBentoKeyboardNavigationDirective } from '@/directives'; import { BentoListboxEvent, type BentoListboxMultiSelectValue, type BentoListboxOptionItem, type BentoListboxOptions, type BentoListboxValue, } from '@/types/listbox'; import { type Booleanish } from '@/types/prop-types'; import { BentoKeyboardNavigationKeyDownEvent } from '@/types/keyboard-navigation'; import { type ListboxMultiSelectProps } from './listbox-multi-select.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<ListboxMultiSelectProps>(), { hasMoreItems: false, isOptionDisabled: isVue2 ? () => () => false : () => false, lazyLoadType: 'none', searching: false, staticCategories: false, }); const emit = defineEmits<{ /** * Emits when the selection changes */ (e: 'select', value: Array<BentoListboxOptionItem>): void; }>(); const { t, n } = useI18n<{ message: MessageSchema }>({ messages }); const slots = useSlots(); const attrs = useAttrs(); const ariaAttributes = computed( () => ({ 'aria-label': attrs['aria-label']?.toString() as string, 'aria-busy': attrs['aria-busy']?.toString() as Booleanish, 'aria-atomic': attrs['aria-atomic']?.toString() as Booleanish, 'aria-live': attrs['aria-live']?.toString() as string, 'aria-multiselectable': true, }) as HTMLAttributes ); const listboxRef = ref(null); const selectAllListboxItemsRef = ref(null); const selectedListboxItemsRef = ref(null); const listboxItemsRef = ref(null); const selectedItemsList = computed<Array<BentoListboxValue>>(() => props.selectedValues.map(({ value }) => value)); const { isMultiLevelSelect, flattenedItemsList } = useMultiLevelItems(toRef(props, 'items')); const hasLazyLoadCategory = computed(() => props.items.some(item => item.items && item.lazyLoad)); const lazyLoadAwareFlattenedItems = computed(() => { if (!isMultiLevelSelect.value) { return props.items; } return props.items.reduce<BentoListboxOptions>((acc, item) => { if (item.items) { if (item.lazyLoad) { acc.push(item); } else { acc.push(...item.items); } } else { acc.push(item); } return acc; }, []); }); const isTreePattern = computed(() => isMultiLevelSelect.value && !props.staticCategories); const computedRole = computed(() => (isTreePattern.value ? 'tree' : 'listbox')); const computedOptionRole = computed(() => (isTreePattern.value ? 'treeitem' : 'option')); const optionStates = (option: BentoListboxOptionItem) => { const isDisabled = props.isOptionDisabled(option); const isPreSelected = (selectedItemsList.value as BentoListboxMultiSelectValue)?.includes(option.value) || false; return { isDisabled, isPreSelected }; }; // Items that are not disabled and pre-selected disabled items const enabledItems = computed(() => { if (props.searching && isMultiLevelSelect) { return props.items; } const fullItemsList = lazyLoadAwareFlattenedItems.value.filter(option => { if (option.lazyLoad) { return true; } const { isDisabled, isPreSelected } = optionStates(option); return (isDisabled && isPreSelected) || !isDisabled; }); props.selectedValues.forEach(selectedValue => { if (!fullItemsList.find(item => item.value === selectedValue.value)) { fullItemsList.push(selectedValue); } }); return fullItemsList; }); const visibleSelectedList = computed<BentoListboxOptions>(() => enabledItems.value?.filter(option => { return selectedItemsList.value?.includes(option.value); // Check for category }) ); const calculateTotalItems = () => { if (isMultiLevelSelect.value && !props.searching) { return flattenedItemsList.value.length; } return props.items.length; }; const selectAllCheckboxText = computed(() => { const possibleSelections = enabledItems.value.length; const total = calculateTotalItems(); const amountSelectedValues = visibleSelectedList.value?.length || 0; if (amountSelectedValues === 0) { return t('selectAll', { total: n(possibleSelections) }); } if (amountSelectedValues === total) { return t('allSelected', { total: n(total) }); } return amountSelectedValues <= total ? t('numberOfTotalSelected', { number: n(amountSelectedValues), total: n(total) }) : t('numbersSelected', { number: n(amountSelectedValues) }); }); const selectAll = (selectableItems: BentoListboxOptions) => { const filterOutDisabledAndNotPreselectedOptions = option => { const { isDisabled, isPreSelected } = optionStates(option); return (isDisabled && isPreSelected) || !isDisabled; }; // Adding or removing this list of values const addOrRemoveList = selectableItems .filter(filterOutDisabledAndNotPreselectedOptions) .filter(item => !selectedItemsList.value.includes(item.value)); // MAKE SURE NOT TO ADD DUPLICATES emit('select', [...props.selectedValues, ...addOrRemoveList]); }; const unselectAll = (selectableItems: BentoListboxOptions) => { const findEnabledUnselectedItems = option => { const { isDisabled, isPreSelected } = optionStates(option); return !(isDisabled && isPreSelected); }; const unselectedItemsList = selectableItems.filter(findEnabledUnselectedItems).map(({ value }) => value); const newSelectedList = props.selectedValues.filter(item => !unselectedItemsList?.includes(item.value)); emit('select', newSelectedList); }; const selectAllItems = () => { selectAll(flattenedItemsList.value); }; const unselectAllItems = () => { unselectAll(flattenedItemsList.value); }; const { isAllSelected, isIndeterminate, toggleSelectAll } = useCheckboxSelectAll( visibleSelectedList, enabledItems, selectAllItems, unselectAllItems ); const ariaCheckedComputed = computed(() => { if (isIndeterminate.value) { return 'mixed'; } return `${isAllSelected.value}`; }); const isSelectAllOn = ref(isAllSelected.value); const handleSelectAll = () => { isSelectAllOn.value = !isSelectAllOn.value; toggleSelectAll(isSelectAllOn.value); }; watch(isAllSelected, () => { isSelectAllOn.value = isAllSelected.value; }); const selectAllCategory = (category: BentoListboxOptionItem) => { if (category.lazyLoad) { const childValues = (category.items ?? []).map(({ value }) => value); const withoutChildren = props.selectedValues.filter(({ value }) => !childValues.includes(value)); emit('select', [...withoutChildren, category]); } else { selectAll(category.items); } }; const unselectAllCategory = (category: BentoListboxOptionItem) => { if (category.lazyLoad) { emit( 'select', props.selectedValues.filter(({ value }) => value !== category.value) ); } else { unselectAll(category.items); } }; const toggleCheckboxSelection = (eventName, selectedOption: BentoListboxOptionItem) => { let exists = false; let newSelectedList = props.selectedValues.filter(({ value }) => { if (selectedOption.value === value) { exists = true; return false; } return true; }); if (exists) { emit(eventName, newSelectedList); return; } // Check if the item's parent lazy category is selected const parentLazyCategory = props.items.find( item => item.items?.some(subItem => subItem.value === selectedOption.value) && item.lazyLoad && selectedItemsList.value.includes(item.value) ); if (parentLazyCategory) { // Remove parent category newSelectedList = newSelectedList.filter(({ value }) => value !== parentLazyCategory.value); // Add other loaded children const otherLoadedChildren = parentLazyCategory.items.filter( subItem => subItem.value !== selectedOption.value ); newSelectedList.push(...otherLoadedChildren); } else { newSelectedList.push(selectedOption); } emit(eventName, newSelectedList); }; const selectedOptions = computed(() => { if (!isMultiLevelSelect.value) { return props.selectedValues; } const isOptionSelected = (option: BentoListboxOptionItem): boolean => (selectedItemsList.value as unknown as Array<string | number>).includes(option.value); const categoryOptionsSelected = (category: BentoListboxOptionItem): boolean => { if (category.lazyLoad && isOptionSelected(category)) { return true; } return ( !props.staticCategories && category.items.length > 0 && category.items.every(option => isOptionSelected(option)) ); }; const optionsFlat = props.items.flatMap(option => option.items && !categoryOptionsSelected(option) ? option.items : option ); const options = optionsFlat.filter(option => { if (option.items) { return true; } return isOptionSelected(option); }); const knownValues = new Set<BentoListboxValue>(); optionsFlat.forEach(option => { knownValues.add(option.value); option.items?.forEach(sub => knownValues.add(sub.value)); }); const unfetchedSelected = props.selectedValues.filter(sv => !knownValues.has(sv.value)); return [...options, ...unfetchedSelected]; }); const showSelectedItems = computed(() => { if (!selectedOptions.value?.length) { return false; } const isLazyLoad = ['automatic', 'button'].includes(props.lazyLoadType); const hasAllItemsSelected = props.selectedValues?.length > 0 && visibleSelectedList.value?.length !== enabledItems.value?.length; if (isLazyLoad && props.hasMoreItems) { return !!props.selectedValues?.length; } return hasAllItemsSelected; }); const showSelectAllCheckbox = computed(() => { if (hasLazyLoadCategory.value) { return false; } const isLazyLoad = ['automatic', 'button'].includes(props.lazyLoadType); if (!isLazyLoad) { return true; } return !props.hasMoreItems; }); // Keyboard navigation const expandedCategories = ref([]); const { visibleDomOptions, updateVisibleDomOptions } = useVisibleDomOptions( listboxRef, toRef(props, 'items'), computedOptionRole ); const toggleCategoryVisibility = category => { if (expandedCategories.value.includes(category)) { expandedCategories.value = expandedCategories.value.filter(item => item !== category); } else { expandedCategories.value.push(category); } nextTick(() => { updateVisibleDomOptions(); }); }; const focusItem = (nextFocusedItemIndex: number) => { visibleDomOptions.value[nextFocusedItemIndex]?.focus(); }; const { moveToNextElement, moveToPreviousElement, moveToFirstElement, moveToLastElement, preventDefault, focusedIndex, setListboxFocusedIndex, } = useListboxKeyboardNavigation( { items: visibleDomOptions, }, focusItem ); const listboxListeners = { [BentoKeyboardNavigationKeyDownEvent.ARROW_UP]: (event: CustomEvent<KeyboardEvent>) => moveToPreviousElement(event), [BentoKeyboardNavigationKeyDownEvent.ARROW_DOWN]: (event: CustomEvent<KeyboardEvent>) => moveToNextElement(event), [BentoKeyboardNavigationKeyDownEvent.HOME]: moveToFirstElement, [BentoKeyboardNavigationKeyDownEvent.END]: moveToLastElement, }; defineExpose({ /** * Method to set the index of the item to be focused on * Used for focus management to be kept in-sync with parent e.g. dropdowns */ setListboxFocusedIndex, }); const chipsSectionListeners = { [BentoKeyboardNavigationKeyDownEvent.ARROW_UP]: preventDefault, [BentoKeyboardNavigationKeyDownEvent.ARROW_DOWN]: preventDefault, }; const computedItemsIndex = computed(() => { let itemIndex = showSelectAllCheckbox.value ? 1 : 0; const indexMap = {}; props.items.forEach(item => { let subItemsVisible = expandedCategories.value.includes(item.value); if (item.items) { if (props.staticCategories) { subItemsVisible = true; } else { indexMap[item.value] = itemIndex++; } if (subItemsVisible) { item.items.forEach(subItem => (indexMap[subItem.value] = itemIndex++)); } } else { indexMap[item.value] = itemIndex++; } }); return indexMap; }); </script> <script lang="ts"> /** * Listbox multi-select options list * * @example * import { BentoListboxMultiSelect } from '@adyen/bento-vue2'; * * export default { * components: { BentoListboxMultiSelect }, * template: ` * <bento-listbox-multi-select * :items="[{ label: 'Option 1', value: 'option-1' }]" * :selected-values="['option-1']" * /> * ` * } */ export default { i18n: { messages } }; </script> <style lang="scss" scoped src="./listbox-multi-select.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div ref="listboxRef" v-bento-keyboard-navigation-directive v-bind="ariaAttributes" role="listbox" v-on="listboxListeners" @focusin="handleVirtualScrollingFocus" > <template v-if="hasCategories && staticCategories"> <template v-for="item in items"> <bento-listbox-single-select-category v-if="item.items && item.items.length > 0" :key="`category-${item.value}`" :category-label="item.label" > <bento-listbox-single-select-option v-for="option in item.items" :key="option.value" ref="listboxItemRef" role="option" :option="option" :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :selected="option.value === selectedItem.value" v-on="listboxOptionListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-single-select-option> </bento-listbox-single-select-category> <bento-listbox-single-select-option v-else :key="`option-${item.value}`" ref="listboxItemRef" role="option" :option="item" :disabled="isOptionDisabled ? isOptionDisabled(item) : null" :selected="item.value === selectedItem.value" v-on="listboxOptionListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-single-select-option> </template> </template> <template v-else> <bento-listbox-single-select-option v-for="option in items" :key="option.value" ref="listboxItemRef" role="option" :option="option" :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :selected="option.value === selectedItem.value" v-on="listboxOptionListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-single-select-option> </template> </div> </template> <script lang="ts"> import { computed, defineComponent, type HTMLAttributes, nextTick, type PropType, ref, toRef } from 'vue'; import { BentoListboxSingleSelectCategory } from './components/listbox-single-select-category'; import { BentoListboxSingleSelectOption } from '../listbox-single-select-option'; import { useListboxKeyboardNavigation } from '../../composables/useListboxKeyboardNavigation'; import { useVisibleDomOptions } from '@/internal/listbox/composables/use-visible-dom-options/use-visible-dom-options'; import { BentoListboxEvent, type BentoListboxIsOptionDisabled, type BentoListboxOptions } from '@/types/listbox'; import { BentoKeyboardNavigationKeyDownEvent } from '@/types/keyboard-navigation'; import { type Booleanish } from '@/types/prop-types'; import { BentoKeyboardNavigationDirective } from '@/directives'; /** * Listbox single-select options list * * @example * import { BentoListboxMultiSelect } from '@adyen/bento-vue2'; * * export default { * components: { BentoListboxMultiSelect }, * template: ` * <bento-listbox-multi-select * :items="[{ label: 'Option 1', value: 'option-1' }]" * :selected-values="['option-1']" * /> * ` * } */ export default defineComponent({ name: 'bento-listbox-single-select', components: { BentoListboxSingleSelectCategory, BentoListboxSingleSelectOption, }, directives: { BentoKeyboardNavigationDirective }, inheritAttrs: false, props: { /** * Function that allows the options to be disabled * * @type {BentoListboxIsOptionDisabled} * @param {BentoListboxOptionItem} option - listbox option object * @param {string} option.label - Option's label text * @param {string|number} option.value - Option's value * @param {unknown} option.data - Option's extra data to be passed to the default slot */ isOptionDisabled: { type: Function as PropType<BentoListboxIsOptionDisabled>, default: () => false, }, /** * The option elements to populate the listbox with. * It must be an array of {@see BentoListboxOptionItem }. * * @property {string} value.label - Text to be displayed in the option * @property {string,number} value.value - Value of the option */ items: { type: Array as PropType<BentoListboxOptions>, required: true, }, /** * 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 */ selectedValue: { type: Array as PropType<BentoListboxOptions>, default: undefined, }, /** * If enabled, dropdown will display non-selectable, static categories */ staticCategories: { type: Boolean, default: false }, /** * Total number of items in the full list (used for virtual scroll keyboard navigation). * When greater than 0, virtual scroll keyboard navigation is enabled. */ totalItemCount: { type: Number, default: 0 }, /** * Function to scroll the virtual list container to a given item index. */ scrollToIndex: { type: Function as PropType<(index: number) => void>, default: undefined }, /** * The start index of the currently rendered virtual scroll slice. */ virtualScrollStartIndex: { type: Number, default: 0 }, }, emits: [BentoListboxEvent.SELECT, BentoListboxEvent.ENTER, BentoListboxEvent.SPACE, BentoListboxEvent.TAB], setup(props, { emit, expose, slots, attrs }) { const listboxRef = ref(
|
|
1
|
+
<template> <div ref="listboxRef" v-bento-keyboard-navigation-directive v-bind="ariaAttributes" role="listbox" v-on="listboxListeners" @focusin="handleVirtualScrollingFocus" > <template v-if="hasCategories && staticCategories"> <template v-for="item in items"> <bento-listbox-single-select-category v-if="item.items && item.items.length > 0" :key="`category-${item.value}`" :category-label="item.label" > <bento-listbox-single-select-option v-for="option in item.items" :key="option.value" ref="listboxItemRef" role="option" :option="option" :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :selected="option.value === selectedItem.value" v-on="listboxOptionListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-single-select-option> </bento-listbox-single-select-category> <bento-listbox-single-select-option v-else :key="`option-${item.value}`" ref="listboxItemRef" role="option" :option="item" :disabled="isOptionDisabled ? isOptionDisabled(item) : null" :selected="item.value === selectedItem.value" v-on="listboxOptionListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-single-select-option> </template> </template> <template v-else> <bento-listbox-single-select-option v-for="option in items" :key="option.value" ref="listboxItemRef" role="option" :option="option" :disabled="isOptionDisabled ? isOptionDisabled(option) : null" :selected="option.value === selectedItem.value" v-on="listboxOptionListeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox-single-select-option> </template> </div> </template> <script lang="ts"> import { computed, defineComponent, type HTMLAttributes, nextTick, type PropType, ref, toRef } from 'vue'; import { BentoListboxSingleSelectCategory } from './components/listbox-single-select-category'; import { BentoListboxSingleSelectOption } from '../listbox-single-select-option'; import { useListboxKeyboardNavigation } from '../../composables/useListboxKeyboardNavigation'; import { useVisibleDomOptions } from '@/internal/listbox/composables/use-visible-dom-options/use-visible-dom-options'; import { BentoListboxEvent, type BentoListboxIsOptionDisabled, type BentoListboxOptions } from '@/types/listbox'; import { BentoKeyboardNavigationKeyDownEvent } from '@/types/keyboard-navigation'; import { type Booleanish } from '@/types/prop-types'; import { BentoKeyboardNavigationDirective } from '@/directives'; /** * Listbox single-select options list * * @example * import { BentoListboxMultiSelect } from '@adyen/bento-vue2'; * * export default { * components: { BentoListboxMultiSelect }, * template: ` * <bento-listbox-multi-select * :items="[{ label: 'Option 1', value: 'option-1' }]" * :selected-values="['option-1']" * /> * ` * } */ export default defineComponent({ name: 'bento-listbox-single-select', components: { BentoListboxSingleSelectCategory, BentoListboxSingleSelectOption, }, directives: { BentoKeyboardNavigationDirective }, inheritAttrs: false, props: { /** * Function that allows the options to be disabled * * @type {BentoListboxIsOptionDisabled} * @param {BentoListboxOptionItem} option - listbox option object * @param {string} option.label - Option's label text * @param {string|number} option.value - Option's value * @param {unknown} option.data - Option's extra data to be passed to the default slot */ isOptionDisabled: { type: Function as PropType<BentoListboxIsOptionDisabled>, default: () => false, }, /** * The option elements to populate the listbox with. * It must be an array of {@see BentoListboxOptionItem }. * * @property {string} value.label - Text to be displayed in the option * @property {string,number} value.value - Value of the option */ items: { type: Array as PropType<BentoListboxOptions>, required: true, }, /** * 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 */ selectedValue: { type: Array as PropType<BentoListboxOptions>, default: undefined, }, /** * If enabled, dropdown will display non-selectable, static categories */ staticCategories: { type: Boolean, default: false }, /** * Total number of items in the full list (used for virtual scroll keyboard navigation). * When greater than 0, virtual scroll keyboard navigation is enabled. */ totalItemCount: { type: Number, default: 0 }, /** * Function to scroll the virtual list container to a given item index. */ scrollToIndex: { type: Function as PropType<(index: number) => void>, default: undefined }, /** * The start index of the currently rendered virtual scroll slice. */ virtualScrollStartIndex: { type: Number, default: 0 }, }, emits: [BentoListboxEvent.SELECT, BentoListboxEvent.ENTER, BentoListboxEvent.SPACE, BentoListboxEvent.TAB], setup(props, { emit, expose, slots, attrs }) { const listboxRef = ref<HTMLElement>(); const listboxItemRef = ref([]); const selectedItem = computed(() => !!props.selectedValue?.length && props.selectedValue.at(0)); const ariaAttributes = computed( () => ({ 'aria-label': attrs['aria-label']?.toString() as string, 'aria-busy': attrs['aria-busy']?.toString() as Booleanish, 'aria-atomic': attrs['aria-atomic']?.toString() as Booleanish, 'aria-live': attrs['aria-live']?.toString() as string, }) as HTMLAttributes ); /** * Finds out if items have sub items */ const hasCategories = computed(() => !!props.items.some(({ items }) => !!items)); const onOptionSelected = eventName => (selectedOption: BentoListboxOptions) => { emit(eventName, [selectedOption]); }; // Keyboard navigation const { visibleDomOptions, updateVisibleDomOptions } = useVisibleDomOptions( listboxRef, toRef(props, 'items'), ref('option'), false ); const isVirtualScrollEnabled = computed(() => props.totalItemCount > 0); // When virtual scroll re-renders items (e.g. on mouse wheel scroll), the // browser may auto-focus an option. Sync the keyboard navigation index to // match the focused element so subsequent arrow key presses navigate from // the correct position. const handleVirtualScrollingFocus = (event: FocusEvent) => { if (!isVirtualScrollEnabled.value) { return; } const target = event.target as HTMLElement; if (target === listboxRef.value || target?.getAttribute('role') !== 'option') { return; } const localIndex = visibleDomOptions.value.indexOf(target as HTMLDivElement); if (localIndex !== -1) { const globalIndex = localIndex + props.virtualScrollStartIndex; setListboxFocusedIndex(globalIndex); } }; const focusItem = async (globalIndex: number) => { if (isVirtualScrollEnabled.value && props.scrollToIndex) { props.scrollToIndex(globalIndex); await nextTick(); await updateVisibleDomOptions(); const newDomIndex = globalIndex - props.virtualScrollStartIndex; visibleDomOptions.value[newDomIndex]?.focus(); } else { visibleDomOptions.value[globalIndex]?.focus(); } }; const navigateToItem = async (globalIndex: number) => { setListboxFocusedIndex(globalIndex); await focusItem(globalIndex); }; const scrollIntoContainer = (listboxItemElement?: HTMLElement, listboxElement?: HTMLElement) => { if (!listboxItemElement || !listboxElement) { return; } const elementRect = listboxItemElement.getBoundingClientRect(); const scrollableContainer = listboxElement; const containerRect = scrollableContainer.getBoundingClientRect(); if (elementRect.top < containerRect.top) { scrollableContainer.scrollTop -= containerRect.top - elementRect.top; } else if (elementRect.bottom > containerRect.bottom) { scrollableContainer.scrollTop += elementRect.bottom - containerRect.bottom; } }; const scrollToItem = async (globalIndex: number, listboxElement?: HTMLElement) => { await updateVisibleDomOptions(); if (isVirtualScrollEnabled.value && props.scrollToIndex) { props.scrollToIndex(globalIndex); } else { scrollIntoContainer(visibleDomOptions.value[globalIndex], listboxElement); } }; const { moveToNextElement, moveToPreviousElement, moveToFirstElement, moveToLastElement, setListboxFocusedIndex, } = useListboxKeyboardNavigation( { items: visibleDomOptions, totalItemCount: toRef(props, 'totalItemCount'), }, focusItem ); const listboxListeners = { [BentoKeyboardNavigationKeyDownEvent.ARROW_UP]: moveToPreviousElement, [BentoKeyboardNavigationKeyDownEvent.ARROW_DOWN]: moveToNextElement, [BentoKeyboardNavigationKeyDownEvent.HOME]: moveToFirstElement, [BentoKeyboardNavigationKeyDownEvent.END]: moveToLastElement, }; const listboxOptionListeners = { [BentoListboxEvent.SELECT]: onOptionSelected(BentoListboxEvent.SELECT), [BentoListboxEvent.SPACE]: onOptionSelected(BentoListboxEvent.SPACE), [BentoListboxEvent.TAB]: onOptionSelected(BentoListboxEvent.TAB), [BentoListboxEvent.ENTER]: onOptionSelected(BentoListboxEvent.ENTER), }; expose({ /** * Reference for a single listbox item. * Required for keyboard navigation in dropdown as $children is removed in vue@3 */ listboxItemRef, /** * Method to set the index of the item to be focused on * Used for focus management to be kept in-sync with parent e.g. dropdowns */ setListboxFocusedIndex, /** * Navigates to an item by global index, handling virtual scroll if enabled. * Scrolls the virtual list and focuses the item. */ navigateToItem, /** * Scrolls to an item by global index without changing focus. * Used when the dropdown opens via click to bring the selected option into view. */ scrollToItem, }); return { // Enums BentoListboxEvent, // Values listboxRef, listboxItemRef, selectedItem, slots, ariaAttributes, // Computed hasCategories, // Methods onOptionSelected, // Keyboard navigation listboxListeners, listboxOptionListeners, // Virtual scroll focus management handleVirtualScrollingFocus, }; }, }); </script>
|