@adyen/bento-mcp 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/assets/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.vue +1 -1
  3. package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.vue +1 -1
  4. package/dist/assets/components/data-grid/data-grid.docs.mdx +1 -1
  5. package/dist/assets/components/data-grid/data-grid.vue +1 -1
  6. package/dist/assets/components/date-picker/date-picker.vue +1 -1
  7. package/dist/assets/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.vue +1 -1
  8. package/dist/assets/components/draggable/components/draggable-handle/draggable-handle.stories.ts +1 -0
  9. package/dist/assets/components/draggable/components/draggable-handle/draggable-handle.types.ts +1 -0
  10. package/dist/assets/components/draggable/components/draggable-handle/draggable-handle.vue +1 -0
  11. package/dist/assets/components/draggable/draggable.docs.mdx +178 -0
  12. package/dist/assets/components/draggable/draggable.stories.ts +1 -0
  13. package/dist/assets/components/draggable/draggable.types.ts +1 -0
  14. package/dist/assets/components/draggable/draggable.vue +1 -0
  15. package/dist/assets/components/filter-bar/components/date-filter/date-filter.vue +1 -1
  16. package/dist/assets/components/filter-bar/filter-bar.docs.mdx +1 -0
  17. package/dist/assets/components/filter-bar/filter-bar.stories.ts +1 -1
  18. package/dist/assets/components/filter-bar/filter-bar.types.ts +1 -1
  19. package/dist/assets/components/filter-bar/filter-bar.vue +1 -1
  20. package/dist/assets/components/form-layout/components/form-layout-group/form-layout-group.vue +1 -1
  21. package/dist/assets/components/form-layout/form-layout.docs.mdx +33 -20
  22. package/dist/assets/components/form-layout/form-layout.stories.ts +1 -1
  23. package/dist/assets/components/input-field/input-field.vue +1 -1
  24. package/dist/assets/components/input-time/input-time.vue +1 -1
  25. package/dist/assets/components/internal/listbox/components/listbox-multi-select/listbox-multi-select.vue +1 -1
  26. package/dist/assets/components.json +101 -100
  27. package/dist/assets/deprecations.md +196 -196
  28. package/dist/assets/index.ts +1 -1
  29. package/dist/assets/tailwind/README.md +45 -5
  30. package/dist/main.js +2 -3
  31. package/package.json +2 -3
  32. package/dist/assets/components/draggable-handle/draggable-handle.stories.ts +0 -1
  33. package/dist/assets/components/draggable-handle/draggable-handle.types.ts +0 -1
  34. package/dist/assets/components/draggable-handle/draggable-handle.vue +0 -1
  35. /package/dist/assets/components/{draggable-handle → draggable/components/draggable-handle}/draggable-handle.docs.mdx +0 -0
@@ -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, } from '@/components/date-picker'; import { type BentoDateRangePickerGranularityConfig, type BentoDateRangePickerValue, type BentoDateRangePickerVariant, } 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 from version 2.0.0. The persistent filter label now uses a counter instead of custom strings. This property is now redundant. * The translation object to handle any custom transations. e.g. perisistent filter labels. */ messages?: { [key: string]: { persistentButtonLabelPartiallySelected?: string; persistentButtonLabelAllSelected?: string; [key: string]: string; }; }; /** * @deprecated from version 2.0.0. 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 from version 2.0.0. 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; variant?: `${BentoDatePickerVariant}`; tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoDateRangeFilterOptions { allowTimeInput?: boolean; firstDayOfWeek?: BentoDatePickerFirstDayOfWeek; isDateDisabled?: BentoDatePickerIsDateDisabled; 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 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 This property will be removed in version 2.0, please use `config` & `filter-values` properties. */ 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'>; 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 from version 2.0.0. Use the config property 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, } from '@/components/date-picker'; import { type BentoDateRangePickerGranularityConfig, type BentoDateRangePickerValue, type BentoDateRangePickerVariant, } 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 from version 2.0.0. The persistent filter label now uses a counter instead of custom strings. This property is now redundant. * The translation object to handle any custom transations. e.g. perisistent filter labels. */ messages?: { [key: string]: { persistentButtonLabelPartiallySelected?: string; persistentButtonLabelAllSelected?: string; [key: string]: string; }; }; /** * @deprecated from version 2.0.0. 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 from version 2.0.0. 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; variant?: `${BentoDatePickerVariant}`; tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoDateRangeFilterOptions { allowTimeInput?: boolean; firstDayOfWeek?: BentoDatePickerFirstDayOfWeek; isDateDisabled?: BentoDatePickerIsDateDisabled; 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 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 This property will be removed in version 2.0, please use `config` & `filter-values` properties. */ 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 from version 2.0.0. Use the config property 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"> <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 @input="onSearchbarInput" @clear="onSearchbarClear" /> </div> <component :is="screenLayoutComponent" :class="containerConditionalClasses" condensed> <div :class="contentConditionalClasses"> <div v-if="showSearchbarInsideContainer && isSearchBarEnabled"> <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="valueAndFilterValuesPropsMerged" @input="updateAllFilters" /> <!-- 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 } = 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 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) ); // 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 = (updatedFilter: BentoFilterModel) => { const filterIndex = valueAndFilterValuesPropsMerged.value.findIndex( filter => filter.field === updatedFilter.field ); const newFilters = valueAndFilterValuesPropsMerged.value.slice(); newFilters[filterIndex].value = updatedFilter.value; 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"> <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> <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 ? { minWidth: 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 +1 @@
1
- <template> <div class="b-form-layout-group" :class="conditionalClasses"> <slot></slot> </div> </template> <script setup lang="ts"> import { computed, type PropType } from 'vue'; import { BentoLayoutGroupProportions } from './form-layout-group.types'; // Default proportion const DEFAULT_PROPORTION = 100; const props = defineProps({ /** * The layout of the pair group of components. * This will render the components within the width of the proportions. * Only two colums are available. * * If only one value is entered, then it will assume there's only one child in the group. * If two values are entered, the first value will be use for the first child and the second * value for the second child. * * If the value entered is not listed in the example * list below it will use 100% by default. * @example * '33-33-33' * '50-50' * '25-75' * '75-25' * '33-66' * '66-33' * '25' * '33' * '50' * '100' */ layout: { type: String as PropType<BentoLayoutGroupProportions>, default: '100', validator: (value: BentoLayoutGroupProportions) => BentoLayoutGroupProportions.includes(value), }, }); // Sets DEFAULT_PROPORTION by default if `layout` is not in the list const computedLayout = computed(() => BentoLayoutGroupProportions.includes(props.layout) ? props.layout : DEFAULT_PROPORTION ); const conditionalClasses = computed(() => ({ // Transform into column if the layout is unsupported 'b-form-layout-group--column': !BentoLayoutGroupProportions.includes(props.layout), // Assings the correct layout class [`b-form-layout-group--${computedLayout.value}`]: true, })); </script> <script lang="ts"> /** * Use to provide horizontal layouts to build forms * with two fields per line. * * These can be used for field combinations, * such as post code and city or title and first name. * * @example * import { BentoFormLayoutGroup } from '@adyen/bento-vue2'; * * export default { * components: { BentoFormLayoutGroup }, * template: ` * <bento-form-layout-group> * <first-element /> * <second-element /> * </bento-form-layout-group> * ` * } */ export default {}; </script> <style lang="scss" scoped src="./form-layout-group.scss" />
1
+ <template> <div class="b-form-layout-group" :class="conditionalClasses"> <slot></slot> <error-message v-if="!!errorMessage" :error-message="errorMessage" class="b-form-layout-group__error-message" /> </div> </template> <script setup lang="ts"> import { computed, type PropType } from 'vue'; import { ErrorMessage } from '@/internal'; import { BentoLayoutGroupProportions } from './form-layout-group.types'; // Default proportion const DEFAULT_PROPORTION = 100; const props = defineProps({ /** * The layout of the pair group of components. * This will render the components within the width of the proportions. * Only two columns are available. * * If only one value is entered, then it will assume there's only one child in the group. * If two values are entered, the first value will be use for the first child and the second * value for the second child. * * If the value entered is not listed in the example * list below it will use 100% by default. * @example * '33-33-33' * '50-50' * '25-75' * '75-25' * '33-66' * '66-33' * '25' * '33' * '50' * '100' */ layout: { type: String as PropType<BentoLayoutGroupProportions>, default: '100', validator: (value: BentoLayoutGroupProportions) => BentoLayoutGroupProportions.includes(value), }, /** * The error message to be displayed for the whole group. * * If set, individual error messages for all components within this group will be hidden. * This is particularly useful for field pairs where a single group error is displayed. */ errorMessage: { type: String, default: null }, }); // Sets DEFAULT_PROPORTION by default if `layout` is not in the list const computedLayout = computed(() => BentoLayoutGroupProportions.includes(props.layout) ? props.layout : DEFAULT_PROPORTION ); const conditionalClasses = computed(() => ({ // Transform into column if the layout is unsupported 'b-form-layout-group--column': !BentoLayoutGroupProportions.includes(props.layout), // Assings the correct layout class [`b-form-layout-group--${computedLayout.value}`]: true, 'b-form-layout-group--group-error-messages': !!props.errorMessage, })); </script> <script lang="ts"> /** * Use to provide horizontal layouts to build forms * with two fields per line. * * These can be used for field combinations, * such as post code and city or title and first name. * * @example * import { BentoFormLayoutGroup } from '@adyen/bento-vue2'; * * export default { * components: { BentoFormLayoutGroup }, * template: ` * <bento-form-layout-group> * <first-element /> * <second-element /> * </bento-form-layout-group> * ` * } */ export default {}; </script> <style lang="scss" scoped src="./form-layout-group.scss" />
@@ -1,5 +1,6 @@
1
1
  import { Canvas, Meta, Source } from '@storybook/blocks';
2
2
  import * as FormLayoutStories from './form-layout.stories';
3
+ import BentoFormLayoutValidateExample from './__tests__/form-layout-validate-example.vue?raw';
3
4
 
4
5
  <Meta of={FormLayoutStories} />
5
6
 
@@ -24,6 +25,7 @@ page-specific guidance, such as multi-page forms and how to place a form in a pa
24
25
  - [Type](#type)
25
26
  - [Group](#group)
26
27
  - [Dynamic default slot content](#dynamic-default-slot-content)
28
+ - [Group-level validation](#group-level-validation)
27
29
  - [Form actions](#form-actions)
28
30
  - [Loading](#loading)
29
31
  - [Validation](#validation)
@@ -186,6 +188,17 @@ slot. If this functionality is needed:
186
188
  `}
187
189
  />
188
190
 
191
+ #### Group-level validation
192
+
193
+ To highlight inputs with errors, set the `error-message` prop on each input component. To display the error message at
194
+ the group level, set the `error-message` prop on the `bento-form-layout-group` component. When a group-level error
195
+ message is present, individual field error messages are visually hidden but remain accessible to screen readers via
196
+ `aria-describedby`. For optimal accessibility, ensure all fields within the group use the same error message text.
197
+
198
+ See an example of group-level validation below:
199
+
200
+ <Source dark language="html" code={BentoFormLayoutValidateExample} />
201
+
189
202
  ### Form actions
190
203
 
191
204
  The form provides two slots for actions:
@@ -241,8 +254,8 @@ import { z } from 'zod/v4'
241
254
  import { useForm, toTypedSchema } from '@adyen/bento-vue2/use-form-v4';
242
255
  const { values, errors, submitForm, handleReset } = useForm({
243
256
  validationSchema: toTypedSchema(
244
- z.object({
245
- // Schema here
257
+ z.object({
258
+ // Schema here
246
259
  })
247
260
  )
248
261
  });
@@ -281,7 +294,7 @@ import { z } from 'zod/v4'
281
294
  import { useForm, toTypedSchema } from '@adyen/bento-vue2/use-form-v4';
282
295
  const { values, errors, submitForm, handleReset } = useForm({
283
296
  validationSchema: toTypedSchema(
284
- z.object({
297
+ z.object({
285
298
  name: z.string(),
286
299
  lastName: z.string()
287
300
  }).refine(data => data.name === data.lastName, {
@@ -306,7 +319,7 @@ import { useForm, toTypedSchema } from '@adyen/bento-vue2';
306
319
  const externalValue = computed(() => { ... });
307
320
  const { values, errors, submitForm, handleReset } = useForm({
308
321
  validationSchema: toTypedSchema(
309
- z.object({
322
+ z.object({
310
323
  percentage: z
311
324
  .string({
312
325
  errorMap: (data, ctx) => {
@@ -341,7 +354,7 @@ import { useForm, toTypedSchema } from '@adyen/bento-vue2/use-form-v4';
341
354
  const externalValue = computed(() => { ... });
342
355
  const { values, errors, submitForm, handleReset } = useForm({
343
356
  validationSchema: toTypedSchema(
344
- z.object({
357
+ z.object({
345
358
  percentage: z
346
359
  .string({
347
360
  error: issue => 'Default error';
@@ -350,7 +363,7 @@ const { values, errors, submitForm, handleReset } = useForm({
350
363
  error: issue => 'Value must be positive'
351
364
  })
352
365
  .lte(100, {
353
- error: issue => \`Field needs to be greater than value \`\${externalValue.value\}\`
366
+ error: issue => \`Field needs to be greater than value \`\${externalValue.value\}\`
354
367
  }),
355
368
  })
356
369
  )
@@ -370,7 +383,7 @@ Vue2
370
383
  <Source
371
384
  dark
372
385
  language="typescript"
373
- code={`
386
+ code={`
374
387
  import { useForm, toTypedSchema } from '@adyen/bento-vue2/use-form-v4';
375
388
  import { z } from 'zod/v4'
376
389
  const { values, errors, submitForm } = useForm({
@@ -378,12 +391,12 @@ const { values, errors, submitForm } = useForm({
378
391
  z.object({
379
392
  name: z
380
393
  .string({
381
- error: issue =>
382
- issue.input === undefined
383
- ? 'This field is required'
394
+ error: issue =>
395
+ issue.input === undefined
396
+ ? 'This field is required'
384
397
  : 'Not a string',
385
398
  })
386
- .min(3, {
399
+ .min(3, {
387
400
  error: issue => {
388
401
  if (issue.code === 'too_small') {
389
402
  return \`Value must be >\${issue.minimum}\`
@@ -391,7 +404,7 @@ const { values, errors, submitForm } = useForm({
391
404
  }
392
405
  })
393
406
  .startsWith('Jane')
394
- .max(4, {
407
+ .max(4, {
395
408
  error: issue => {
396
409
  if (issue.code === 'too_big') {
397
410
  return \`Value must be >\${issue.maximum}\`
@@ -401,9 +414,9 @@ const { values, errors, submitForm } = useForm({
401
414
  .default('Joe'),
402
415
  })
403
416
  )
404
- });
405
- const handleSubmit = submitForm(() => {
406
- // Your code here
417
+ });
418
+ const handleSubmit = submitForm(() => {
419
+ // Your code here
407
420
  });
408
421
  `}
409
422
  />
@@ -421,12 +434,12 @@ const { values, errors, submitForm } = useForm({
421
434
  z.object({
422
435
  name: z
423
436
  .string({
424
- error: issue =>
425
- issue.input === undefined
426
- ? 'This field is required'
437
+ error: issue =>
438
+ issue.input === undefined
439
+ ? 'This field is required'
427
440
  : 'Not a string',
428
441
  })
429
- .min(3, {
442
+ .min(3, {
430
443
  error: issue => {
431
444
  if (issue.code === 'too_small') {
432
445
  return \`Value must be >\${issue.minimum}\`
@@ -434,7 +447,7 @@ const { values, errors, submitForm } = useForm({
434
447
  }
435
448
  })
436
449
  .startsWith('Jane')
437
- .max(4, {
450
+ .max(4, {
438
451
  error: issue => {
439
452
  if (issue.code === 'too_big') {
440
453
  return \`Value must be >\${issue.maximum}\`
@@ -1 +1 @@
1
- import { computed, ref } from 'vue'; import { isVue2 } from 'vue-demi'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import type { Meta, StoryObj } from '@storybook/vue'; import { BentoButton, BentoLoadingButton, BentoLoadingButtonState } from '@/components/button'; import { BentoInputField } from '@/components/input-field'; import { BentoDropdown } from '@/components/dropdown'; import { BentoDatePicker } from '@/components/date-picker'; import { BentoTextarea } from '@/components/textarea'; import { BentoCheckboxGroup } from '@/components/checkbox'; import { type BentoCheckboxGroupItems } from '@/components/checkbox/components/checkbox-group/checkbox-group.types'; import { type BentoListboxOptions } from '@/types/listbox'; import { BentoRadioGroup } from '@/components/radio-group'; import { BentoSelectionCardGroup } from '@/components/selection-card/components/selection-card-group'; import { BentoFileUploader } from '@/components/file-uploader'; import { type BentoRadioGroupItems } from '@/components/radio-group/radio-group.types'; import { BentoFormLayoutFieldTip, BentoFormLayoutGroup, BentoFormLayoutTitle } from './components'; import { toTypedSchema, useForm } from '@/composables/use-form-validate/use-form'; import { BentoAlert } from '@/components/alert'; import { BentoLink } from '@/components/link'; import BentoFormLayout from './form-layout.vue'; import ChevronLeftIcon from '@adyen/ui-assets-icons-16/vue/chevron-left'; import SaveIcon from '@adyen/ui-assets-icons-16/vue/save'; import { BentoFormLayoutFieldTipTypeOptions } from './components/form-layout-field-tip/form-layout-tip.types'; import * as z from 'zod'; import BentoFormLayoutFieldTipExample from './__tests__/form-layout-field-tip-example.vue?raw'; import BentoFormLayoutDefaultExample from './__tests__/form-layout-default-example.vue?raw'; export default { title: 'Form layout', component: BentoFormLayout, argTypes: { default: { name: 'default', defaultValue: '', description: 'Can include various common form components: `bento-input-field`, `bento-dropdown`, `bento-date-picker`, `bento-checkbox-group`, etc.', table: { type: { summary: 'VNode[]' }, }, control: { disable: true, }, }, 'error-message': { name: 'error-message', defaultValue: '', description: 'Can include any HTMLElement', table: { type: { summary: 'VNode[]' }, }, control: { type: 'text', }, }, }, } as Meta<typeof BentoFormLayout>; type Story = StoryObj<typeof BentoFormLayout>; const DROPDOWN_ITEMS: BentoListboxOptions = [ { label: 'Cat', value: 'cat' }, { label: 'Dog', value: 'dog' }, ]; const CHECKBOX_GROUP_ITEMS: BentoCheckboxGroupItems = [ { value: 1, label: 'Label' }, { value: 2, label: 'Label' }, { value: 3, label: 'Label' }, ]; const RADIO_GROUP_ITEMS: BentoRadioGroupItems = [ { value: 1, label: 'Label' }, { value: 2, label: 'Label' }, { value: 3, label: 'Label' }, ]; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown, BentoCheckboxGroup, BentoButton, BentoLoadingButton, BentoDatePicker, BentoInputField, BentoFileUploader, BentoFormLayout, BentoFormLayoutFieldTip, BentoFormLayoutGroup, BentoFormLayoutTitle, BentoRadioGroup, BentoTextarea, BentoSelectionCardGroup, }, props: Object.keys(argTypes), template: ` <bento-form-layout v-bind="args"> <template #error-message>{{ errorMessage }}</template> <bento-form-layout-title el="h1"> Section title <template #description> Optional section description </template> </bento-form-layout-title> <bento-input-field>Input field</bento-input-field> <bento-dropdown label="Dropdown" :items="dropdownItems" placeholder="Select an animal" /> <bento-date-picker label="Date picker" /> <bento-textarea label="Textarea" :character-limit="400" /> <bento-checkbox-group label="Checkbox group" :items="checkboxGroupItems" :hide-label="false" /> <bento-radio-group label="Radio group" :items="radioGroupItems" :hide-label="false" /> <bento-selection-card-group label="Selection card group" :items="selectionCardGroupItems" :hide-label="false" variant="radio" /> <bento-file-uploader label="File upload" description="Custom suppporting text" accept=".png,.jpeg,.pdf" :max-size="5242880" /> <bento-form-layout-field-tip> <bento-dropdown label="Dropdown" :items="dropdownItems" placeholder="Select an animal" /> <template #message>This is a small tip that can be used in small spaces</template> </bento-form-layout-field-tip> <bento-form-layout-title el="h2" subsection> Subsection title </bento-form-layout-title> <bento-form-layout-group> <bento-input-field>Input field (100%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-33-33"> <bento-input-field>Input field (33%)</bento-input-field> <bento-input-field>Input field (33%)</bento-input-field> <bento-input-field>Input field (33%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50-50"> <bento-input-field>Input field (50%)</bento-input-field> <bento-input-field>Input field (50%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25-75"> <bento-input-field>Input field (25%)</bento-input-field> <bento-input-field>Input field (75%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="75-25"> <bento-input-field>Input field (75%)</bento-input-field> <bento-input-field>Input field (25%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-66"> <bento-input-field>Input field (33%)</bento-input-field> <bento-input-field>Input field (66%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="66-33"> <bento-input-field>Input field (66%)</bento-input-field> <bento-input-field>Input field (33%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25"> <bento-input-field>Input field (25%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33"> <bento-input-field>Input field (33%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50"> <bento-input-field>Input field (50%)</bento-input-field> </bento-form-layout-group> <template #actions-left> <bento-button variant="secondary"> Action </bento-button> <bento-button variant="secondary"> Action </bento-button> </template> <template #actions-right> <bento-button variant="secondary"> Action </bento-button> <bento-loading-button> Action </bento-loading-button> </template> </bento-form-layout> `, setup(props) { return { args: isVue2 ? props : _args, errorMessage: 'There is an error. Correct the highlighted field.', dropdownItems: [...DROPDOWN_ITEMS], checkboxGroupItems: [...CHECKBOX_GROUP_ITEMS], radioGroupItems: [...RADIO_GROUP_ITEMS], selectionCardGroupItems: [ { title: 'Card title', value: '1', description: 'Description', }, { title: 'Card title', value: '2', description: 'Description', }, { title: 'Card title', value: '3', description: 'Description', }, { title: 'Card title', value: '4', description: 'Description', }, ], }; }, }), parameters: storybookDocsParameter(BentoFormLayoutDefaultExample), }; export const WithTip: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown, BentoFormLayout, BentoFormLayoutFieldTip, }, props: Object.keys(argTypes), template: ` <bento-form-layout v-bind="args"> <bento-form-layout-field-tip :type="args.type" :id="args.id ? args.id : undefined"> <bento-dropdown label="Dropdown" :items="dropdownItems" placeholder="Select an animal" :aria-describedby="args.id" /> <template #message>This is a small tip that can be used in small spaces</template> </bento-form-layout-field-tip> </bento-form-layout> `, setup(props) { return { args: isVue2 ? props : _args, dropdownItems: [...DROPDOWN_ITEMS], }; }, }), argTypes: { type: { options: BentoFormLayoutFieldTipTypeOptions, description: 'Type of a alert; set a distinctive icon and color. Defaults to highlight.', control: { type: 'select', }, table: { category: 'Form Layout Field Tip props', type: { summary: 'string' } }, }, id: { description: 'Unique identifier to the form field. Used for accessibility.', table: { category: 'Form Layout Field Tip props', type: { summary: 'string' } }, }, }, args: { id: 'dropdown_field_with_tip', type: 'highlight' }, parameters: storybookDocsParameter(BentoFormLayoutFieldTipExample), }; const groupCode = ` <bento-form-layout> <bento-form-layout-group> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50-50"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25-75"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="75-25"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-66"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="66-33"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-33-33"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> </bento-form-layout> `; export const Group: Story = { render: (_args, { argTypes }) => ({ components: { BentoAlert, BentoInputField, BentoFormLayout, BentoFormLayoutGroup, }, props: Object.keys(argTypes), template: ` <div> <bento-alert style="margin-bottom: 20px"> <template #description> The <code>bento-form-layout-group</code> <i>is not</i> a dynamic component and does not support changing it's content/default slot. If this functionality is needed, set the <code>v-if</code> statement <b>on</b> the component itself or around/outside it. </template> </bento-alert> <bento-form-layout v-bind="args"> <bento-form-layout-group> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50-50"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25-75"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="75-25"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-66"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="66-33"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-33-33"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> </bento-form-layout> </div> `, setup(props) { return { args: isVue2 ? props : _args, dropdownItems: [...DROPDOWN_ITEMS], }; }, }), parameters: storybookDocsParameter(groupCode), }; const loadingFormCode = ` <bento-form-layout loading> <bento-form-layout-title el="h1"> Form request loading time <template #description> Form fields state while waiting for request to be processed </template> </bento-form-layout-title> <bento-input-field required model-value="Input field">Input field</bento-input-field> <template #actions-right> <bento-loading-button state="loading">Loading</bento-loading-button> </template> </bento-form-layout> `; export const LoadingForm: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoCheckboxGroup, BentoDatePicker, BentoDropdown, BentoFormLayout, BentoFormLayoutGroup, BentoFormLayoutTitle, BentoInputField, BentoLoadingButton, BentoRadioGroup, BentoTextarea, ChevronLeftIcon, SaveIcon, }, props: Object.keys(argTypes), template: ` <bento-form-layout loading> <bento-form-layout-title el="h1"> Form request loading time <template #description> Form fields state while waiting for request to be processed </template> </bento-form-layout-title> <bento-date-picker label="Date picker" optional /> <bento-textarea label="Textarea" :characterLimit="400" model-value="Text area value..." required /> <bento-form-layout-group layout="25-75"> <bento-input-field model-value="Input value" required>Input field</bento-input-field> <bento-dropdown label="Dropdown" :items="[{ label: 'Dropdown value', value: 1 }]" :model-value="1" optional /> </bento-form-layout-group> <bento-radio-group label="Radio group" modelValue="no" optional :items="[ { label: 'No', value: 'no' }, { label: 'Yes', value: 'yes' }, ]" /> <bento-checkbox-group label="Checkbox group" optional :modelValue="['one', 'two']" :items="[ { label: 'One', value: 'one' }, { label: 'Two', value: 'two' }, ]" /> <template v-slot:actions-left> <bento-button variant="secondary"> Back <template v-slot:iconLeft> <chevron-left-icon /> </template> </bento-button> <bento-button variant="secondary"> Save for later <template v-slot:iconLeft> <save-icon /> </template> </bento-button> </template> <template v-slot:actions-right> <bento-loading-button state="loading">Validating</bento-loading-button> </template> </bento-form-layout> `, setup(props) { return { args: isVue2 ? props : _args, }; }, }), parameters: storybookDocsParameter(loadingFormCode), }; const validateCode = ` <template> <bento-form-layout :loading="isLoading"> <template #error-message v-if="errors"> There is an error. Correct the highlighted field. <ul v-if="errorArray.length"> <li v-for="(error, index) in errorArray" :key="index"> <bento-link to="#input-field-name" is-not-routing>{{ error }}</bento-link> </li> </ul> </template> <bento-form-layout-title el="h1"> Form validation <template #description> Form validation using useForm </template> </bento-form-layout-title> <bento-input-field id="input-field-name" :model-value="values.name" :error-message="errors?.name" @update:model-value="name => values.name = name" > Name </bento-input-field> <template #actions-right> <bento-loading-button :state="loadingButtonState" @click="handleSubmit"> Submit form </bento-loading-button> </template> </bento-form-layout> </template> <script setup lang="ts"> import { ref } from 'vue' import { // Components BentoFormLayout, BentoInputField, BentoLoadingButton, // Composables useForm, toTypedSchema, } from '@adyen/bento-vue2' import { z } from 'zod' const isLoading = ref(false); const loadingButtonState = ref<\`${BentoLoadingButtonState}\`>('start'); const { values, errors, submitForm } = useForm({ validationSchema: toTypedSchema( z.object({ name: z .string() .min(3, { message: 'Name is required' }) .startsWith('Jane') .max(4, { message: 'Name cannot be longer than 4 characters' }) .default('Joe'), }) ) }); const errorArray = computed(() => Object.values(errors.value)); const handleSubmit = submitForm(() => { isLoading.value = true; loadingButtonState.value = 'loading'; // Fake request time setTimeout(() => { isLoading.value = false; loadingButtonState.value = 'success'; }, 2000); }); </script> `; export const Validate: Story = { render: (_args, { argTypes }) => ({ components: { BentoInputField, BentoFormLayout, BentoFormLayoutGroup, BentoFormLayoutTitle, BentoLoadingButton, BentoLink, }, props: Object.keys(argTypes), template: ` <bento-form-layout v-bind="args" :loading="isLoading"> <template #error-message v-if="errors"> There is an error. Correct the highlighted field. <ul v-if="errorArray.length"> <li v-for="(error, index) in errorArray" :key="index"> <bento-link to="#input-field-name" is-not-routing>{{ error }}</bento-link> </li> </ul> </template> <bento-form-layout-title el="h1"> Form validation <template #description> Form validation using useForm </template> </bento-form-layout-title> <bento-input-field id="input-field-name" :model-value="values.name" :error-message="errors?.name" @update:model-value="name => values.name = name" > Name </bento-input-field> <template #actions-right> <bento-loading-button :state="loadingButtonState" @click="handleSubmit"> Submit form </bento-loading-button> </template> </bento-form-layout> `, setup(props) { const isLoading = ref(false); const loadingButtonState = ref<`${BentoLoadingButtonState}`>('start'); const validationSchema = toTypedSchema( z.object({ name: z .string() .min(3, { message: 'Name is required' }) .startsWith('Jane') .max(4, { message: 'Name cannot be longer than 4 characters' }) .default('Joe'), }) ); const { values, errors, submitForm } = useForm({ validationSchema, }); const handleSubmit = submitForm(() => { isLoading.value = true; loadingButtonState.value = 'loading'; // Fake request time setTimeout(() => { isLoading.value = false; loadingButtonState.value = 'success'; }, 2000); }); const errorArray = computed(() => Object.values(errors.value)); return { args: isVue2 ? props : _args, values, errors, errorArray, loadingButtonState, isLoading, // Methods handleSubmit, }; }, }), parameters: storybookDocsParameter(validateCode), };
1
+ import { computed, ref } from 'vue'; import { isVue2 } from 'vue-demi'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import type { Meta, StoryObj } from '@storybook/vue'; import { BentoButton, BentoLoadingButton, type BentoLoadingButtonState } from '@/components/button'; import { BentoInputField } from '@/components/input-field'; import { BentoDropdown } from '@/components/dropdown'; import { BentoDatePicker } from '@/components/date-picker'; import { BentoTextarea } from '@/components/textarea'; import { BentoCheckboxGroup } from '@/components/checkbox'; import { type BentoCheckboxGroupItems } from '@/components/checkbox/components/checkbox-group/checkbox-group.types'; import { type BentoListboxOptions } from '@/types/listbox'; import { BentoRadioGroup } from '@/components/radio-group'; import { BentoSelectionCardGroup } from '@/components/selection-card/components/selection-card-group'; import { BentoFileUploader } from '@/components/file-uploader'; import { type BentoRadioGroupItems } from '@/components/radio-group/radio-group.types'; import { BentoFormLayoutFieldTip, BentoFormLayoutGroup, BentoFormLayoutTitle } from './components'; import { toTypedSchema, useForm } from '@/composables/use-form-validate'; import { BentoAlert } from '@/components/alert'; import { BentoLink } from '@/components/link'; import BentoFormLayout from './form-layout.vue'; import ChevronLeftIcon from '@adyen/ui-assets-icons-16/vue/chevron-left'; import SaveIcon from '@adyen/ui-assets-icons-16/vue/save'; import { BentoFormLayoutFieldTipTypeOptions } from './components/form-layout-field-tip/form-layout-tip.types'; import * as z from 'zod'; import BentoFormLayoutFieldTipExample from './__tests__/form-layout-field-tip-example.vue?raw'; import BentoFormLayoutDefaultExample from './__tests__/form-layout-default-example.vue?raw'; import BentoFormLayoutValidateExample from './__tests__/form-layout-validate-example.vue?raw'; export default { title: 'Form layout', component: BentoFormLayout, argTypes: { default: { name: 'default', defaultValue: '', description: 'Can include various common form components: `bento-input-field`, `bento-dropdown`, `bento-date-picker`, `bento-checkbox-group`, etc.', table: { type: { summary: 'VNode[]' }, }, control: { disable: true, }, }, 'error-message': { name: 'error-message', defaultValue: '', description: 'Can include any HTMLElement', table: { type: { summary: 'VNode[]' }, }, control: { type: 'text', }, }, }, } as Meta<typeof BentoFormLayout>; type Story = StoryObj<typeof BentoFormLayout>; const DROPDOWN_ITEMS: BentoListboxOptions = [ { label: 'Cat', value: 'cat' }, { label: 'Dog', value: 'dog' }, ]; const CHECKBOX_GROUP_ITEMS: BentoCheckboxGroupItems = [ { value: 1, label: 'Label' }, { value: 2, label: 'Label' }, { value: 3, label: 'Label' }, ]; const RADIO_GROUP_ITEMS: BentoRadioGroupItems = [ { value: 1, label: 'Label' }, { value: 2, label: 'Label' }, { value: 3, label: 'Label' }, ]; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown, BentoCheckboxGroup, BentoButton, BentoLoadingButton, BentoDatePicker, BentoInputField, BentoFileUploader, BentoFormLayout, BentoFormLayoutFieldTip, BentoFormLayoutGroup, BentoFormLayoutTitle, BentoRadioGroup, BentoTextarea, BentoSelectionCardGroup, }, props: Object.keys(argTypes), template: ` <bento-form-layout v-bind="args"> <template #error-message>{{ errorMessage }}</template> <bento-form-layout-title el="h1"> Section title <template #description> Optional section description </template> </bento-form-layout-title> <bento-input-field>Input field</bento-input-field> <bento-dropdown label="Dropdown" :items="dropdownItems" placeholder="Select an animal" /> <bento-date-picker label="Date picker" /> <bento-textarea label="Textarea" :character-limit="400" /> <bento-checkbox-group label="Checkbox group" :items="checkboxGroupItems" :hide-label="false" /> <bento-radio-group label="Radio group" :items="radioGroupItems" :hide-label="false" /> <bento-selection-card-group label="Selection card group" :items="selectionCardGroupItems" :hide-label="false" variant="radio" /> <bento-file-uploader label="File upload" description="Custom suppporting text" accept=".png,.jpeg,.pdf" :max-size="5242880" /> <bento-form-layout-field-tip> <bento-dropdown label="Dropdown" :items="dropdownItems" placeholder="Select an animal" /> <template #message>This is a small tip that can be used in small spaces</template> </bento-form-layout-field-tip> <bento-form-layout-title el="h2" subsection> Subsection title </bento-form-layout-title> <bento-form-layout-group> <bento-input-field>Input field (100%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-33-33"> <bento-input-field>Input field (33%)</bento-input-field> <bento-input-field>Input field (33%)</bento-input-field> <bento-input-field>Input field (33%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50-50"> <bento-input-field>Input field (50%)</bento-input-field> <bento-input-field>Input field (50%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25-75"> <bento-input-field>Input field (25%)</bento-input-field> <bento-input-field>Input field (75%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="75-25"> <bento-input-field>Input field (75%)</bento-input-field> <bento-input-field>Input field (25%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-66"> <bento-input-field>Input field (33%)</bento-input-field> <bento-input-field>Input field (66%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="66-33"> <bento-input-field>Input field (66%)</bento-input-field> <bento-input-field>Input field (33%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25"> <bento-input-field>Input field (25%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33"> <bento-input-field>Input field (33%)</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50"> <bento-input-field>Input field (50%)</bento-input-field> </bento-form-layout-group> <template #actions-left> <bento-button variant="secondary"> Action </bento-button> <bento-button variant="secondary"> Action </bento-button> </template> <template #actions-right> <bento-button variant="secondary"> Action </bento-button> <bento-loading-button> Action </bento-loading-button> </template> </bento-form-layout> `, setup(props) { return { args: isVue2 ? props : _args, errorMessage: 'There is an error. Correct the highlighted field.', dropdownItems: [...DROPDOWN_ITEMS], checkboxGroupItems: [...CHECKBOX_GROUP_ITEMS], radioGroupItems: [...RADIO_GROUP_ITEMS], selectionCardGroupItems: [ { title: 'Card title', value: '1', description: 'Description', }, { title: 'Card title', value: '2', description: 'Description', }, { title: 'Card title', value: '3', description: 'Description', }, { title: 'Card title', value: '4', description: 'Description', }, ], }; }, }), parameters: storybookDocsParameter(BentoFormLayoutDefaultExample), }; export const WithTip: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdown, BentoFormLayout, BentoFormLayoutFieldTip, }, props: Object.keys(argTypes), template: ` <bento-form-layout v-bind="args"> <bento-form-layout-field-tip :type="args.type" :id="args.id ? args.id : undefined"> <bento-dropdown label="Dropdown" :items="dropdownItems" placeholder="Select an animal" :aria-describedby="args.id" /> <template #message>This is a small tip that can be used in small spaces</template> </bento-form-layout-field-tip> </bento-form-layout> `, setup(props) { return { args: isVue2 ? props : _args, dropdownItems: [...DROPDOWN_ITEMS], }; }, }), argTypes: { type: { options: BentoFormLayoutFieldTipTypeOptions, description: 'Type of a alert; set a distinctive icon and color. Defaults to highlight.', control: { type: 'select', }, table: { category: 'Form Layout Field Tip props', type: { summary: 'string' } }, }, id: { description: 'Unique identifier to the form field. Used for accessibility.', table: { category: 'Form Layout Field Tip props', type: { summary: 'string' } }, }, }, args: { id: 'dropdown_field_with_tip', type: 'highlight' }, parameters: storybookDocsParameter(BentoFormLayoutFieldTipExample), }; const groupCode = ` <bento-form-layout> <bento-form-layout-group> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50-50"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25-75"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="75-25"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-66"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="66-33"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-33-33"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> </bento-form-layout> `; export const Group: Story = { render: (_args, { argTypes }) => ({ components: { BentoAlert, BentoInputField, BentoFormLayout, BentoFormLayoutGroup, }, props: Object.keys(argTypes), template: ` <div> <bento-alert style="margin-bottom: 20px"> <template #description> The <code>bento-form-layout-group</code> <i>is not</i> a dynamic component and does not support changing it's content/default slot. If this functionality is needed, set the <code>v-if</code> statement <b>on</b> the component itself or around/outside it. </template> </bento-alert> <bento-form-layout v-bind="args"> <bento-form-layout-group> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="50-50"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="25-75"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="75-25"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33"> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-66"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="66-33"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> <bento-form-layout-group layout="33-33-33"> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> <bento-input-field>Input field</bento-input-field> </bento-form-layout-group> </bento-form-layout> </div> `, setup(props) { return { args: isVue2 ? props : _args, dropdownItems: [...DROPDOWN_ITEMS], }; }, }), parameters: storybookDocsParameter(groupCode), }; const loadingFormCode = ` <bento-form-layout loading> <bento-form-layout-title el="h1"> Form request loading time <template #description> Form fields state while waiting for request to be processed </template> </bento-form-layout-title> <bento-input-field required model-value="Input field">Input field</bento-input-field> <template #actions-right> <bento-loading-button state="loading">Loading</bento-loading-button> </template> </bento-form-layout> `; export const LoadingForm: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoCheckboxGroup, BentoDatePicker, BentoDropdown, BentoFormLayout, BentoFormLayoutGroup, BentoFormLayoutTitle, BentoInputField, BentoLoadingButton, BentoRadioGroup, BentoTextarea, ChevronLeftIcon, SaveIcon, }, props: Object.keys(argTypes), template: ` <bento-form-layout loading> <bento-form-layout-title el="h1"> Form request loading time <template #description> Form fields state while waiting for request to be processed </template> </bento-form-layout-title> <bento-date-picker label="Date picker" optional /> <bento-textarea label="Textarea" :characterLimit="400" model-value="Text area value..." required /> <bento-form-layout-group layout="25-75"> <bento-input-field model-value="Input value" required>Input field</bento-input-field> <bento-dropdown label="Dropdown" :items="[{ label: 'Dropdown value', value: 1 }]" :model-value="1" optional /> </bento-form-layout-group> <bento-radio-group label="Radio group" modelValue="no" optional :items="[ { label: 'No', value: 'no' }, { label: 'Yes', value: 'yes' }, ]" /> <bento-checkbox-group label="Checkbox group" optional :modelValue="['one', 'two']" :items="[ { label: 'One', value: 'one' }, { label: 'Two', value: 'two' }, ]" /> <template v-slot:actions-left> <bento-button variant="secondary"> Back <template v-slot:iconLeft> <chevron-left-icon /> </template> </bento-button> <bento-button variant="secondary"> Save for later <template v-slot:iconLeft> <save-icon /> </template> </bento-button> </template> <template v-slot:actions-right> <bento-loading-button state="loading">Validating</bento-loading-button> </template> </bento-form-layout> `, setup(props) { return { args: isVue2 ? props : _args, }; }, }), parameters: storybookDocsParameter(loadingFormCode), }; export const Validate: Story = { render: (_args, { argTypes }) => ({ components: { BentoInputField, BentoFormLayout, BentoFormLayoutGroup, BentoFormLayoutTitle, BentoLoadingButton, BentoLink, }, props: Object.keys(argTypes), template: ` <bento-form-layout v-bind="args" :loading="isLoading"> <template #error-message v-if="errorArray.length"> There is an error. Correct the highlighted field. <ul> <li v-for="(error, index) in errorArray" :key="index"> <bento-link to="#input-field-name" is-not-routing>{{ error }}</bento-link> </li> </ul> </template> <bento-form-layout-title el="h1"> Form validation <template #description> Form validation using useForm </template> </bento-form-layout-title> <bento-input-field id="input-field-name" :model-value="values.name" :error-message="errors.name" @update:model-value="name => { if (values) { values.name = String(name); } }" > Name </bento-input-field> <bento-form-layout-group layout="50-50" :error-message="errors.email || errors.phone"> <bento-input-field id="input-field-email" :model-value="values.email" :error-message="errors.email" @update:model-value="email => { if (values) { values.email = String(email); } }" > Email </bento-input-field> <bento-input-field id="input-field-phone" :model-value="values.phone" :error-message="errors.phone" @update:model-value="phone => { if (values) { values.phone = String(phone); } }" > Phone </bento-input-field> </bento-form-layout-group> <template #actions-right> <bento-loading-button :state="loadingButtonState" @click="handleSubmit"> Submit form </bento-loading-button> </template> </bento-form-layout> `, setup(props) { const isLoading = ref(false); const loadingButtonState = ref<`${BentoLoadingButtonState}`>('start'); const validationSchema = toTypedSchema( z .object({ name: z .string() .min(3, { message: 'Name is required' }) .startsWith('Jane') .max(4, { message: 'Name cannot be longer than 4 characters' }) .default('Joe'), email: z.string().default(''), phone: z.string().default(''), }) .superRefine((data, ctx) => { if (data.email.length === 0 && data.phone.length === 0) { const issue = { code: z.ZodIssueCode.custom, message: 'At least one of Email or Phone is required', }; ctx.addIssue({ ...issue, path: ['email'] }); } }) ); const { values, errors, submitForm } = useForm({ validationSchema, }); const handleSubmit = submitForm(() => { isLoading.value = true; loadingButtonState.value = 'loading'; // Fake request time setTimeout(() => { isLoading.value = false; loadingButtonState.value = 'success'; }, 2000); }); const errorArray = computed(() => [...new Set(Object.values(errors.value))]); return { args: isVue2 ? props : _args, values, errors, errorArray, loadingButtonState, isLoading, // Methods handleSubmit, }; }, }), parameters: storybookDocsParameter(BentoFormLayoutValidateExample), };
@@ -1 +1 @@
1
- <template> <bento-typography class="b-input-field" data-testid="input-text-container" :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 { 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_COMPONENT_INJECTION_KEY, 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 version 2.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); // Provide INPUT_FIELD_COMPONENT_INJECTION_KEY as true, to set the input only dropdown size variant in the BentoDropdown comp provide(INPUT_FIELD_COMPONENT_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(() => 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', model: { prop: 'modelValue' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./input-field.scss" />
1
+ <template> <bento-typography class="b-input-field" v-bind="computedRootAttributes" data-testid="input-text-container" :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 { 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_COMPONENT_INJECTION_KEY, 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 version 2.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 computedRootAttributes = computed(() => { return Object.fromEntries(Object.entries(attrs).filter(([key]) => !key.startsWith('aria-'))); }); 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); // Provide INPUT_FIELD_COMPONENT_INJECTION_KEY as true, to set the input only dropdown size variant in the BentoDropdown comp provide(INPUT_FIELD_COMPONENT_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 +1 @@
1
- <template> <bento-input-field class="b-input-time" :condensed="props.condensed" :disabled="props.disabled" :error-message="computedErrorMessage" :label="props.label" :model-value="props.modelValue" :optional="props.optional" :placeholder="props.placeholder" :readonly="props.readonly" :required="props.required" :tooltip-text="props.tooltipText" v-bind="$attrs" @update:model-value="onInput" @keydown="onKeyDown" > <template v-if="hasSlot('default')" #default> <slot name="default"></slot> </template> <template #description> <slot name="description">{{ props.description ? `${props.description} ${t('timeFormatDescription')}` : t('timeFormatDescription') }}</slot> </template> </bento-input-field> </template> <script setup lang="ts"> // Components import { BentoInputField } from '@/components/input-field'; // Utils import { debounce } from '@/utils/ts/debounce'; import { useI18n } from '@/utils/ts/i18n'; import { formatTimeInputValue, isValidTimeString, validateTimeInput } from '@/utils/ts/time-input'; // Types import type { BentoInputFieldValue } from '@/components/input-field/input-field.types'; import { type BentoInputTimeProps } from './input-time.types'; // Localization import messages from './messages.json'; import { computed, ref, useSlots, watch } from 'vue'; import { useHasSlot } from '@/composables'; const VALIDATION_DEBOUNCE_TIME = 300; const slots = useSlots(); const hasSlot = useHasSlot(slots); type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoInputTimeProps>(), { condensed: false, description: undefined, disabled: false, errorMessage: null, label: '', modelValue: undefined, optional: false, placeholder: '', readonly: false, required: false, tooltipText: null, }); const emit = defineEmits<{ /** * Emitted when the component's model changes */ (e: 'update:model-value', value: BentoInputFieldValue): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when the time value passes validation (complete HH:MM:SS format) */ (e: 'input:valid'): void; /** * Emitted when the time value fails validation */ (e: 'input:error'): void; }>(); const internalError = ref(false); const computedErrorMessage = computed(() => { if (props.errorMessage) { return props.errorMessage; } return internalError.value ? t('invalidTimeFormat') : null; }); const validateTime = (timeText: string) => { if (!timeText) { internalError.value = false; return; } const isValid = isValidTimeString(timeText); internalError.value = !isValid; if (isValid) { emit('input:valid'); } else { emit('input:error'); } }; const validateTimeWithDebounce = debounce(validateTime, VALIDATION_DEBOUNCE_TIME); // Watch for external modelValue changes (e.g., when parent resets the value) watch( () => props.modelValue, newValue => { const timeText = String(newValue ?? ''); // Clear the error only when the new value is a complete, valid time string if (timeText.length === 8 && isValidTimeString(timeText)) { internalError.value = false; } } ); const onInput = (value: BentoInputFieldValue) => { if (props.disabled || props.readonly) { return; } const formatted = formatTimeInputValue(String(value ?? '')); emit('update:model-value', formatted); validateTimeWithDebounce(formatted); }; const onKeyDown = (event: KeyboardEvent) => { validateTimeInput(event); emit('keydown', event); }; </script> <script lang="ts"> /** * Input time component for entering time values in HH:MM:SS format. * This is an extension of the `bento-input-field` component with built-in * time formatting and validation logic. * * @usage * import { BentoInputTime } from '@adyen/bento-vue2'; * * const timeValue = ref(''); * * export default { * components: { BentoInputTime }, * template: ` * <bento-input-time * label="Start time" * :model-value="timeValue" * @update:model-value="newValue => timeValue.value = newValue" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-input-time', inheritAttrs: false, model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./input-time.scss" />
1
+ <template> <bento-input-field class="b-input-time" :type="inputType" :step="isTouchDevice ? 1 : undefined" :condensed="props.condensed" :disabled="props.disabled" :error-message="inputErrorMessage" :label="props.label" :model-value="inputModelValue" :optional="props.optional" :placeholder="computedPlaceholder" :readonly="props.readonly" :required="props.required" :tooltip-text="props.tooltipText" v-bind="$attrs" @update:model-value="onInput" @keydown="onKeyDown" > <template v-if="hasSlot('default')" #default> <slot name="default"></slot> </template> <template v-if="!isTouchDevice" #description> <slot name="description">{{ props.description ? `${props.description} ${t('timeFormatDescription')}` : t('timeFormatDescription') }}</slot> </template> </bento-input-field> </template> <script setup lang="ts"> // Components import { BentoInputField } from '@/components/input-field'; import { INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY } from '@/components/input-field/input-field.keys'; // Utils import { debounce } from '@/utils/ts/debounce'; import { useI18n } from '@/utils/ts/i18n'; import { formatTimeInputValue, isValidTimeString, validateTimeInput } from '@/utils/ts/time-input'; // Types import type { BentoInputFieldValue } from '@/components/input-field/input-field.types'; import { type BentoInputTimeProps } from './input-time.types'; // Localization import messages from './messages.json'; import { computed, provide, ref, useSlots, watch } from 'vue'; import { useHasSlot, useTouchDevice } from '@/composables'; const VALIDATION_DEBOUNCE_TIME = 300; provide(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, 'bento-input-time'); const slots = useSlots(); const hasSlot = useHasSlot(slots); const { isTouchDevice } = useTouchDevice(); type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoInputTimeProps>(), { condensed: false, description: undefined, disabled: false, errorMessage: null, label: '', modelValue: undefined, optional: false, placeholder: '', readonly: false, required: false, tooltipText: null, }); const emit = defineEmits<{ /** * Emitted when the component's model changes */ (e: 'update:model-value', value: BentoInputFieldValue): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when the time value passes validation (complete HH:MM:SS format) */ (e: 'input:valid'): void; /** * Emitted when the time value fails validation */ (e: 'input:error'): void; }>(); const internalError = ref(false); const computedErrorMessage = computed(() => { if (props.errorMessage) { return props.errorMessage; } return internalError.value ? t('invalidTimeFormat') : null; }); const inputErrorMessage = computed(() => (isTouchDevice.value ? props.errorMessage : computedErrorMessage.value)); const inputType = computed(() => (isTouchDevice.value ? 'time' : undefined)); const inputModelValue = computed(() => props.modelValue); const computedPlaceholder = computed(() => (isTouchDevice ? undefined : props.placeholder)); const validateTime = (timeText: string) => { if (!timeText) { internalError.value = false; return; } const isValid = isValidTimeString(timeText); internalError.value = !isValid; if (isValid) { emit('input:valid'); } else { emit('input:error'); } }; const validateTimeWithDebounce = debounce(validateTime, VALIDATION_DEBOUNCE_TIME); // Watch for external modelValue changes (e.g., when parent resets the value) watch( () => props.modelValue, newValue => { const timeText = String(newValue ?? ''); // Clear the error only when the new value is a complete, valid time string if (timeText.length === 8 && isValidTimeString(timeText)) { internalError.value = false; } } ); const onInput = (value: BentoInputFieldValue) => { if (props.disabled || props.readonly) { return; } if (isTouchDevice.value) { emit('update:model-value', String(value ?? '')); emit('input:valid'); } else { const formatted = formatTimeInputValue(String(value ?? '')); emit('update:model-value', formatted); validateTimeWithDebounce(formatted); } }; const onKeyDown = (event: KeyboardEvent) => { if (!isTouchDevice.value) { validateTimeInput(event); } emit('keydown', event); }; </script> <script lang="ts"> /** * Input time component for entering time values in HH:MM:SS format. * This is an extension of the `bento-input-field` component with built-in * time formatting and validation logic. * * @usage * import { BentoInputTime } from '@adyen/bento-vue2'; * * const timeValue = ref(''); * * export default { * components: { BentoInputTime }, * template: ` * <bento-input-time * label="Start time" * :model-value="timeValue" * @update:model-value="newValue => timeValue.value = newValue" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-input-time', inheritAttrs: false, model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./input-time.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); }); return options; }); 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 = 1; 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 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); }); return options; }); 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" />