@adyen/bento-mcp 0.5.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0 (2026-06-09)
4
+
5
+ ### Features
6
+
7
+ - added eslint rule readme to mcp `get-deprecations` tool ([44b9ce380](https://github.com/Adyen/bento/commit/44b9ce380))
8
+
9
+ ### ❤️ Thank You
10
+
11
+ - erice
12
+
13
+
3
14
  ## 0.5.4 (2026-06-03)
4
15
 
5
16
  This was a version bump only for mcp to align it with other projects, there were no code changes.
@@ -1 +1 @@
1
- import { type BentoColumn, BentoColumnOverflow, type BentoDatagridData, type BentoDatagridDataItem, type BentoDatagridDataItemId, BentoDatagridEvent, type BentoDatagridPaginationProps, type BentoDatagridSelection, type BentoDatagridSortByColumn, BentoDatagridSortDirection, } from './data-grid.types'; import { BentoCard } from '@/components/card'; import { action } from '@storybook/addon-actions'; import { type BentoFilterBarModel, type BentoFilterBarSearchBarProps, BentoFilterItemType, type BentoFilterValues, } from '../filter-bar'; import { BentoCurrency } from '@/components/currency'; import { BentoStatus, BentoStatusVariant } from '@/components/status'; import { BentoLink } from '@/components/link'; import { BentoTypography } from '@/components/typography'; import { computed, ref, watch } from 'vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { BentoDate, BentoDateFormat } from '@/components/date'; import { BentoCountry, BentoCountryCode } from '@/components/country'; import type { Meta, StoryObj } from '@storybook/vue'; import BentoDataGrid from './data-grid.vue'; import { isVue2 } from 'vue-demi'; import { BentoButton } from '../button'; import { BentoDashedUnderline } from '../dashed-underline'; import { BentoDataGridSummaryCell, BentoDataGridTagWithTextCell, BentoDataGridTrendCell } from './components'; import type { BentoColumnCellAction, BentoColumnCellActionEvent, BentoColumnCellActionsProp, BentoDataGridRowActionsProp, } from './components/data-grid-cell-actions/data-grid-cell-actions.types'; // Examples import DataGridEverythingBagel from './__tests__/data-grid-everything-bagel.vue?raw'; import DataGridCellActionsExample from './__tests__/data-grid-cell-actions-example.vue?raw'; import DataGridRowActionsExample from './__tests__/data-grid-row-actions-example.vue?raw'; import DataGridFitContentInCardExample from './__tests__/data-grid-fit-content-in-card-example.vue?raw'; import DataGridWithSlotsExample from './__tests__/data-grid-with-slots-example.vue?raw'; import DataGridFilteringExample from './__tests__/data-grid-filtering-example.vue?raw'; import DataGridSelectionExample from './__tests__/data-grid-selection-example.vue?raw'; import DataGridTrendCellExample from './__tests__/data-grid-trend-cell-example.vue?raw'; import DataGridTagWithTextCellExample from './__tests__/data-grid-tag-with-text-cell-example.vue?raw'; import { DEFAULT_DATA } from './__tests__/data-grid-example-data'; const meta: Meta = { title: 'Data Grid', component: BentoDataGrid, argTypes: { // Values lazyLoadType: { control: { type: 'select', }, }, // Filter bar props filters: { table: { type: { summary: 'BentoFilterBarModel' }, }, }, filterSearchTerm: { name: 'filterSearchTerm', defaultValue: undefined, description: "Filter bar search term useful to filter all the data by text. If the value is non-existant (`null` or `undefined`) the search bar won't be enabled.", control: { type: 'text', }, }, // Slots default: { table: { disable: true }, }, header: { name: 'header', defaultValue: '', description: 'header text', table: { type: { summary: 'VNode[]' }, }, control: { type: 'text', }, }, footer: { name: 'footer', defaultValue: '', description: 'footer text', table: { type: { summary: 'VNode[]' }, }, control: { type: 'text', }, }, 'head-{columnKey}': { name: 'head-{columnKey}', defaultValue: '', description: 'the column slot where `{columnKey}` is the name of the column key. Allows the Column header to be customized.', table: { type: { summary: 'VNode[]' }, category: 'slots', }, control: { disable: true }, }, 'item-{columnKey}': { name: 'item-{columnKey}', defaultValue: '', description: 'the data item slot where `{columnKey}` is the name of the data item column key. Allow row cells to be customized.', table: { type: { summary: 'VNode[]' }, category: 'slots', }, control: { disable: true }, }, ['pagination-slot']: { control: { type: 'text' }, description: 'Passes content to the default pagination slot', table: { type: { summary: 'VNode[]' }, }, }, // Events [BentoDatagridEvent.COLUMN_RESIZE]: { name: BentoDatagridEvent.COLUMN_RESIZE, defaultValue: '', description: 'Triggered when a column is resized. Provides a field name and the new resized width in pixels.', table: { category: 'events', type: { summary: '({ field, newResizedWidth }) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.SELECT]: { name: BentoDatagridEvent.SELECT, defaultValue: '', description: 'Triggered when a row is selected. Provides an array of data item ids. `BentoDatagridDataItemId[]`', table: { category: 'events', type: { summary: '(selectedDataItemIds) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.NAVIGATE]: { name: BentoDatagridEvent.NAVIGATE, defaultValue: '', description: `Triggered when the navigation controls in the pagination are clicked (i.e. when the arrow buttons to the rightmost of the screen or the dropdown on the right hand are updated). It indicates to which page it should go next.`, table: { category: 'events', type: { summary: '(pageNumber: number) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.ITEMS_PAGE]: { name: BentoDatagridEvent.ITEMS_PAGE, defaultValue: '', description: `Triggered when the number of items per page is changed from the left hand dropdown of the pagination. It indicates the new number of items that will be displayed.`, table: { category: 'events', type: { summary: '(itemsPerPage: number) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.SORT]: { name: BentoDatagridEvent.SORT, defaultValue: '', description: `Triggered when a column is sorted. Provides an array of \`BentoDatagridSortByColumn\` which includes the \`field\` and the sort \`direction\` (\`BentoDatagridSortDirection\`) of the column:`, table: { category: 'events', type: { summary: '(sortByDirections) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_COLUMNS]: { name: BentoDatagridEvent.UPDATE_COLUMNS, defaultValue: '', description: `Triggered when a the columns have been updated. Provides an updated array of columns \`BentoColumn[]\` which should be passed back into the \`columns\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:columns.sync="columns"\``, table: { category: 'events', type: { summary: '(columns) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_CONDENSED]: { name: 'update:condensed', defaultValue: '', description: `Triggered when the config panel's condensed view toggle is interacted with. Provides the updated condensed \`boolean\`. (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:condensed.sync="condensed"\``, table: { category: 'events', type: { summary: '(condensed) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_FILTER_VALUES]: { name: BentoDatagridEvent.UPDATE_FILTER_VALUES, defaultValue: '', description: `Triggered when a the filters in the filterbar have updated. Provides an array of filters \`BentoFilterBarModel[]\` which should be passed back into the \`filter-values\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:filter-values.sync="filtersValues"\``, table: { category: 'events', type: { summary: '(filterValues) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_PAGINATION]: { name: BentoDatagridEvent.UPDATE_PAGINATION, defaultValue: '', description: `Triggered when a the pagination state has been updated. Provides an updated \`pagination\` properties object which should be passed back into the \`pagination\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:pagination.sync="pagination"\``, table: { category: 'events', type: { summary: '(paginationProps) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_SELECTION]: { name: BentoDatagridEvent.UPDATE_SELECTION, defaultValue: '', description: `Triggered when a the select data rows state has been updated. Provides an array of row IDs \`BentoDatagridDataItemId[]\` which should be passed back into the \`selection\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:selection.sync="selection"\``, table: { category: 'events', type: { summary: '(selection) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_SORT_BY]: { name: 'update:sort-by', defaultValue: '', description: `Triggered when a the sorting state has been updated. Provides an array of sorting directions \`BentoDatagridSortByColumn[]\` which should be passed back into the \`sort-by\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:sort-by.sync="sortBy"\``, table: { category: 'events', type: { summary: '(sortingDirections) => void' }, }, control: { disable: true }, }, // Filter bar search events [BentoDatagridEvent.FILTER_SEARCH_INPUT]: { name: BentoDatagridEvent.FILTER_SEARCH_INPUT, defaultValue: '', description: "Triggered whenever the filter's search is updated.", table: { category: 'events', type: { summary: '(newSearchTerm) => void' } }, control: { disable: true }, }, [BentoDatagridEvent.FILTER_SEARCH_CLEAR]: { name: BentoDatagridEvent.FILTER_SEARCH_CLEAR, defaultValue: '', description: "Triggered whenever the filter's search is cleared.", table: { category: 'events', type: { summary: '() => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM]: { name: BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM, defaultValue: '', description: "Triggered whenever the filter's search is updated to sync the model.", table: { category: 'events', type: { summary: '(newSearchTerm) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.ROW_CLICK]: { name: BentoDatagridEvent.ROW_CLICK, defaultValue: '', description: 'Triggered whenever a row has been clicked.', table: { category: 'events', type: { summary: '(row: BentoDatagridDataItem, event: MouseEvent) => void' }, }, }, [BentoDatagridEvent.NESTED_CONTENT_TOGGLED]: { name: BentoDatagridEvent.NESTED_CONTENT_TOGGLED, defaultValue: '', description: 'Triggered whenever a data item toggled is nested content', table: { category: 'events', type: { summary: '(dataItem: BentoDatagridDataItem) => void' }, }, control: { disable: true }, }, }, }; export default meta; type Story = StoryObj<typeof BentoDataGrid>; const DEFAULT_COLUMNS: Array<BentoColumn> = [ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Company', autoWidth: true }, { field: 'payment', label: 'Payment', minWidth: 350 }, ]; const ADDITIONAL_DATA: BentoDatagridData = [ { id: '03c0a799-1ced-4017-aea3-0cebb0106335', name: 'Additional data fetched here', payment: 160405, company: 'Merchant Helium', registered: '2023-06-30T13:56:09.498Z', tags: ['deserunt', 'et', 'esse'], reference: 'UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn', paymentStatus: 1, countryCode: BentoCountryCode.NL, }, { id: 'b6d4f1fd-b1f6-40a6-a443-f785c89604ef', name: 'Santos Palmer', payment: 163500, company: 'Merchant B', registered: '2023-04-28T13:11:09.498Z', tags: ['et', 'Lorem', 'sint'], reference: 'IxUZ9FsEPCZ9dFFPpI9C', paymentStatus: 2, countryCode: BentoCountryCode.BE, }, { id: '8f9e7877-1ca2-4929-9217-f1ec78709291', name: 'Joyce Johns', payment: 78500, company: 'Merchant C', registered: '2023-06-30T11:01:09.498Z', tags: ['laborum', 'enim', 'ullamco'], reference: 'y0qAQ0WDszRyqLY2WO9f', paymentStatus: 1, countryCode: BentoCountryCode.UA, }, { id: '0789a012-85a6-4657-87bb-319141938a61', name: 'Lynnette Melendez', payment: -45000, company: 'STRALOY', registered: '2019-08-01T13:56:01.498Z', tags: ['sit', 'consectetur', 'amet'], reference: '2vv5pzbRSwx8YsFoGGOC', paymentStatus: 1, countryCode: BentoCountryCode.BR, }, { id: '0b3981b6-c73b-47ad-81d1-b06f9b95d467', name: 'Wagner Mcintosh', payment: 1112, company: 'Merchant A', registered: '2022-12-30T02:12:09.498Z', tags: ['ad', 'ullamco', 'et'], reference: '54KN4jkpBuaRY5xMfe84', paymentStatus: 1, countryCode: BentoCountryCode.US, }, { id: '51d2b31d-d577-4fca-8329-a583babbbdda', name: 'Evans Zamora', payment: 1153.4, company: 'GEEKWAGON', registered: '2023-06-30T13:51:09.498Z', tags: ['consequat', 'magna', 'sit'], reference: 'Sj84kad0MGxpuozTjEk', paymentStatus: 0, countryCode: BentoCountryCode.FR, }, { id: '9bd59a6e-bd03-4477-813f-b844953b7645', name: 'Anderson Middleton', payment: 181722.23, company: 'BALOOBA', registered: '2021-03-02T01:34:09.498Z', tags: ['aliqua', 'adipisicing', 'magna'], reference: 'MtJ5Bnew0MGxpuozTjEk', paymentStatus: 1, countryCode: BentoCountryCode.IT, }, ]; const DEFAULT_ARGS = { data: DEFAULT_DATA, columns: DEFAULT_COLUMNS, allowColumnDragAndDrop: false, pagination: {}, emptyState: { title: 'No results were found', image: 'no-results-found', variant: 'embedded', action: { title: 'Clear filters', event: action('clear'), }, description: 'Try a different term or reset search filters', }, }; // Default const DefaultCode = ` <template> <bento-data-grid :allow-row-clicks="false" :allow-column-drag-and-drop="false" :bulk-actions="undefined" :condensed="false" :columns="columns" :column-panel="false" :data="data" :empty-state="undefined" :filters="undefined" :filters-search-term="undefined" :fit-content="false" :get-data-item-id="undefined" :has-resizeable-columns="false" :has-nested-content="false" :highlighted-rows="undefined" :is-data-item-disabled="undefined" lazy-load-type="none" :loading="false" :outline="false" :no-spacing-top="false" :pagination="undefinded" :selectable-accross-pages="false" :sticky-pagination="false" :sticky-filters="false" :selectable="false" :selection="undefined" :sort-by="undefined" :tab-title="undefined" :truncate-column-headers="false" /> </template> <script setup lang="ts"> import { BentoColumn, BentoDataGrid, BentoDatagridData } from '@adyen/bento-vue2'; import { ref } from 'vue'; const data = ref<BentoDatagridData>(${JSON.stringify(DEFAULT_DATA)}); const columns = ref<BentoColumn[]>([ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]); </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" /> `, setup(props) { return { args: isVue2 ? props : _args }; }, }), args: DEFAULT_ARGS, parameters: storybookDocsParameter(DefaultCode), }; // Fluid Columns const FluidTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" /> </template> <script setup lang="ts"> import { BentoColumn, BentoDataGrid, BentoDatagridData } from '@adyen/bento-vue2'; import { ref } from 'vue'; const data = ref<BentoDatagridData>(${JSON.stringify(DEFAULT_DATA)}); const columns = ref<BentoColumn[]>([ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ]); </script> `; const rowActionsFluid: BentoDataGridRowActionsProp = (item: BentoDatagridDataItem) => { if (item.id === DEFAULT_DATA[3].id) { return [ { variant: 'edit', event: action('edit'), }, ]; } else if (item.id === DEFAULT_DATA[2].id) { return [ { variant: 'edit', event: action('edit') }, { variant: 'open', isShort: true, menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ]; } else if (item.id === DEFAULT_DATA[1].id) { return []; } return [ { variant: 'edit', event: action('edit'), }, { title: 'Delete', critical: true, event: action('Delete'), }, ]; }; export const Fluid: Story = { ...Default, args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ], rowActions: rowActionsFluid, }, parameters: storybookDocsParameter(FluidTemplateCode), }; // Slots export const Slots: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" > <template #header> <bento-typography variant="title" el="h2"> Users </bento-typography> </template> <template #head-name="{column}"> <bento-typography variant="caption"> {{ column.label }} </bento-typography> </template> <template #item-name="{item}"> <bento-typography variant="body"> {{ item.name }} </bento-typography> <template v-if="item.company === 'Merchant Helium'"> <div style="display: inline-flex; gap: 4px;"> <bento-typography v-for="itemTag in item.tags" :key="itemTag" variant="caption" :style="{ color: 'var(--b-color-label-secondary)' }" > { {{ itemTag }} } </bento-typography> </div> </template> </template> <template #item-company="{item}"> <bento-link isNotRouting to="https://www.adyen.com" :title="item.company" > {{ item.company }} </bento-link> <bento-typography v-if="item.company === 'Merchant B'" variant="caption" :style="{ color: 'var(--b-color-label-secondary)' }"> {{ item.registered }} </bento-typography> </template> <template #item-payment="{item}"> $ {{ item.payment }} </template> <template #footer> <bento-typography variant="caption"> Footer </bento-typography> </template> </bento-data-grid> `, setup(props) { return { args: isVue2 ? props : _args }; }, }), args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ], }, parameters: storybookDocsParameter(DataGridWithSlotsExample), }; // Sorting const useSort = () => { const sortBy = ref<Array<BentoDatagridSortByColumn>>([]); const data = ref<BentoDatagridData>(DEFAULT_DATA); const sort = (sortedColumns: Array<BentoDatagridSortByColumn>) => { data.value = sortedColumns[0].direction === BentoDatagridSortDirection.ASCENDING ? data.value.sort((a, b) => (a[sortedColumns[0].field] as string).localeCompare(b[sortedColumns[0].field] as string) ) : data.value .sort((a, b) => (a[sortedColumns[0].field] as string).localeCompare(b[sortedColumns[0].field] as string) ) .reverse(); action('sort')(sortedColumns); }; return { data, sortBy, sort }; }; const SortingTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" @sort="sort" :sort-by.sync="sortBy" /> </template> <script setup lang="ts"> import { BentoDataGrid, type BentoDatagridSortByColumn, type BentoDatagridData, type BentoColumn, } from '@adyen/bento-vue2' // Sortable fields must have "sortable: true" const columns: BentoColumn = [ { field: 'id', label: 'ID', minWidth: 300, sortable: true, mandatory: true }, { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, ] const data = ref<BentoDatagridData>([ { id: 1, name: 'First' }, { id: 2, name: 'Second' }, ]); const sortBy = ref<Array<BentoDatagridSortByColumn>>([]); const sort = (sortedColumns: Array<BentoDatagridSortByColumn>) => { data.value = sortedColumns[0].direction === BentoDatagridSortDirection.ASCENDING ? data.value.sort((a, b) => (a[sortedColumns[0].field] as number) .toString() .localeCompare(b[sortedColumns[0].field] as string) ) : data.value .sort((a, b) => (a[sortedColumns[0].field] as number) .toString() .localeCompare(b[sortedColumns[0].field] as string) ) .reverse(); } </script> `; export const Sorting: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid :data="data" :columns="columns" :sort-by.sync="sortBy" @sort="sort" /> `, setup(props) { const { data, sortBy, sort } = useSort(); return { args: isVue2 ? props : _args, data, sortBy, sort, }; }, }), args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', minWidth: 300, sortable: true, mandatory: true }, { field: 'company', label: 'Company', minWidth: 150, sortable: true }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ], sortBy: [], }, parameters: storybookDocsParameter(SortingTemplateCode), }; export const Selection: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <div> <bento-data-grid v-bind="args" :data="pagedData" :pagination="pagination" :filters="filtersConfig" :filter-values="filterValues" :is-data-item-disabled="isDataItemDisabled" :selection="selection" :filter-search-term="searchTerm" @update:filter-search-term="searchTerm = $event" @update:pagination="pagination = $event" @update:filter-values="filterValues = $event" @select-all-across-pages="onSelectAllAcrossPages" @select-all-current-results="onSelectAllInCurrentResults" @deselect-all-current-results="onDeselectAllInCurrentResults" @filter-search-clear="onSearchCleared" @update:selection="onSelect" @row-click="rowClickAction" /> </div> `, setup(props) { const args = isVue2 ? props : _args; const selection = ref([]); const filtersConfig = computed<BentoFilterBarModel>(() => args?.filters ?? FILTERS); const filterValues = ref<BentoFilterValues>([]); const searchTerm = ref(''); const data = ref( Array.from({ length: 20 }, (_, i) => ({ id: `id-${i + 1}`, name: `Name ${i + 1}`, payment: (i + 1) * 100, company: `Merchant ${i + 1}`, })) ); const isDataItemDisabled = (row: BentoDatagridDataItem) => { return row.id === data.value[3].id; }; const pagination = ref<BentoDatagridPaginationProps>({ page: 1, size: 10, totalCount: 20, hasNext: true, }); const filteredData = computed<BentoDatagridData>(() => { let internalData = data.value; if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); const searchableFields = args.columns.map(column => column.field); internalData = internalData.filter((item: BentoDatagridDataItem) => { return searchableFields.some(field => { const value = String(item[field]); return value.toLowerCase().includes(lowerCaseSearchTerm); }); }); } return internalData.filter(item => filterValues.value.every(filter => filter.value ? String(item[filter.field]).toLowerCase().includes(String(filter.value).toLowerCase()) : true ) ); }); const pagedData = computed<BentoDatagridData>(() => { return filteredData.value.slice( (pagination.value.page - 1) * pagination.value.size, pagination.value.page * pagination.value.size ); }); const enabledIds = computed(() => data.value.filter(item => !isDataItemDisabled(item)).map(x => x.id)); const enabledFilteredIds = computed(() => filteredData.value.filter(item => !isDataItemDisabled(item)).map(x => x.id) ); const onSelect = value => { selection.value = value; action('update:selection')(value); }; const onSelectAllAcrossPages = () => { selection.value = enabledIds.value; action('select-all-across-pages')(); }; const onSelectAllInCurrentResults = () => { selection.value = [...new Set([...selection.value, ...enabledFilteredIds.value])]; action('select-all-current-results')(); }; const onDeselectAllInCurrentResults = () => { selection.value = selection.value.filter(item => !enabledFilteredIds.value.includes(item)); action('deselect-all-current-results')(); }; const onSearchCleared = () => { searchTerm.value = ''; action('search-clear')(); }; watch( filteredData, () => { pagination.value.totalCount = filteredData.value.length; }, { immediate: true } ); return { args, onSelect, onSelectAllAcrossPages, onSelectAllInCurrentResults, onDeselectAllInCurrentResults, pagination, selection, pagedData, filtersConfig, filterValues, searchTerm, rowClickAction: value => action('row-click')(value), isDataItemDisabled, onSearchCleared, }; }, }), args: { ...DEFAULT_ARGS, selectable: true, allowRowClicks: true, selectableAcrossPages: true, bulkActions: [ { title: 'Edit', event: () => true, }, { title: 'Delete', critical: true, event: () => true, }, ], }, parameters: storybookDocsParameter(DataGridSelectionExample), }; // Filtering const FILTERS: BentoFilterBarModel = [ { field: 'name', label: 'Name', type: BentoFilterItemType.INPUT, }, { field: 'company', label: 'Company', type: BentoFilterItemType.INPUT, }, ]; export const Filtering: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const filtersConfig = computed<BentoFilterBarModel>(() => args?.filters ?? FILTERS); const filterValues = ref<BentoFilterValues>([]); const searchTerm = ref(''); const searchConfig = ref<BentoFilterBarSearchBarProps>({ inputFieldAriaLabel: 'Search for items', placeholder: 'Placeholder', hint: 'Hint', }); const dataSet = computed<BentoDatagridData>(() => { let filteredData = args.data; // Apply global search term filter if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); const searchableFields = args.columns.map(column => column.field); filteredData = filteredData.filter((item: BentoDatagridDataItem) => { return searchableFields.some(field => { const value = String(item[field]); return value.toLowerCase().includes(lowerCaseSearchTerm); }); }); } filteredData = filteredData.filter(item => filterValues.value?.every(filter => { return filter.value ? String(item[filter.field]).toLowerCase().includes(String(filter.value).toLowerCase()) : true; }) ); return filteredData; }); watch(filterValues, () => { action('filter-values:update')(filterValues.value); }); const onSearchCleared = action('search-clear'); const onSearchInput = action('search-input'); const onSearchTermUpdated = action('update:search-term'); return { // Values dataSet, filtersConfig, filterValues, searchTerm, searchConfig, // Methods onSearchCleared, onSearchInput, onSearchTermUpdated, // Vue 3 storybook fix args, }; }, template: ` <bento-data-grid v-bind="args" :data="dataSet" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" :filter-search-config="searchConfig" @filter-search-clear="onSearchCleared" @filter-search-input="onSearchInput" @update:filter-search-term="onSearchTermUpdated" /> `, }), args: DEFAULT_ARGS, parameters: storybookDocsParameter(DataGridFilteringExample), }; // No Results const NoResultsTemplateCode = ` <bento-data-grid :data="[]" :columns="[ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]" :empty-state="{ title: 'No results were found', image: 'no-results-found', variant: 'embedded', action: { title: 'Clear filters', event: clearFiltersFunction, }, description: 'Try a different term or reset search filters', }" /> `; export const NoResults: Story = { ...Filtering, args: { ...DEFAULT_ARGS, data: [], filters: FILTERS, filterValues: [{ field: 'company', value: 'CIA' }], }, parameters: storybookDocsParameter(NoResultsTemplateCode), }; // Pagination const PaginationTemplateCode = ` <template> <bento-data-grid :data="dataGridData" :columns="columnsConfig" :pagination.sync="pagination" @update:pagination="onUpdatePagination" /> </template> <script lang="ts" setup> const data="[{ id: '1', name: 'Jane' }, { id: '1', name: 'John' }]" const pagination = ref({ description: 'datagrid navigation', hasNext: true, size: 10 }); // With API call watch( () => pagination.value, () => { makeAPICall(pagination.value); } ); // Without API call const onUpdatePagination = (newPagination: BentoDatagridPaginationProps) => { pagination.value = { ...newPagination }; const newSetStartIndex = pagination.value.page * pagination.value.size; const amountOfItems = newSetStartIndex + pagination.value.size - 1; data.value = arrayWithFullSetOfValues.slice(newSetStartIndex, amountOfItems); }; </script> `; export const Pagination: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="dataGridData" :pagination.sync="pagination" @update:pagination="updatePagination" @items-page="updateItemsPerPage" /> `, setup(props) { const args = isVue2 ? props : _args; const largeData = [ ...DEFAULT_ARGS.data, ...DEFAULT_ARGS.data.map(dataItem => ({ ...dataItem, id: `${dataItem.id}+1` })), ]; const pagination = ref(args.pagination); const dataGridData = computed(() => { const end = pagination.value.size <= largeData.length ? pagination.value.size : largeData.length; return largeData.slice(pagination.value.page - 1, end); }); const updateItemsPerPage = (itemsPerPage: number) => { action('items-page')(itemsPerPage); }; const updatePagination = (newPagination: BentoDatagridPaginationProps) => { action('navigate')(newPagination.page); action('update:pagination')(newPagination); }; return { // Values dataGridData, pagination, // Methods updateItemsPerPage, updatePagination, // Vue 3 storybook fixes args: isVue2 ? props : _args, }; }, }), args: { ...DEFAULT_ARGS, pagination: { hasNext: true, description: 'datagrid navigation', size: 10, page: 1, totalCount: DEFAULT_ARGS.data.length * 2, }, }, parameters: storybookDocsParameter(PaginationTemplateCode), }; // Payment Status (demo use case for datagrid V2) export const EverythingBagelExample: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoStatus, BentoCurrency, BentoDate, BentoCountry, BentoButton }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="filteredAndSortedData" :columns.sync="columns" :condensed="isCondensed" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" :sort-by="sortBy" @update:sort-by="sortBy = $event" @update:condensed="isCondensed = $event" :selection.sync="selection" :is-data-item-disabled="isDataItemDisabledForSelection" :row-actions="rowActions" selectable @row-click="onRowClick" > <template #item-paymentStatus="{item}"> <div style="display: flex"> <bento-status :variant="getPaymentStatus(item.paymentStatus)" /> <span>{{ getPaymentText(item.paymentStatus) }}</span> </div> </template> <template #item-reference="{item}"> <bento-link isNotRouting to="https://www.adyen.com" :title="item.reference" > {{ item.reference }} </bento-link> </template> <template #item-countryCode="{item}"> <bento-country :code="item.countryCode" /> </template> <template #item-payment="{item}"> <bento-currency currency="EUR" :value="item.payment" /> </template> <template #item-date="{item}"> <bento-date :date="new Date(item.registered)" :format='BentoDateFormat.FULL_DATE_TIME_WITH_PERIOD'/> </template> </bento-data-grid> `, setup(props) { const args = isVue2 ? props : _args; const columns = ref<Array<BentoColumn>>(args.columns); const filtersConfig = ref<BentoFilterBarModel>(args.filters); const filterValues = ref<BentoFilterValues>(args?.filterValues); const sortBy = ref<Array<BentoDatagridSortByColumn>>([]); const searchTerm = ref(''); const isCondensed = ref(args.condensed); const getPaymentStatus = (paymentStatus: number) => { switch (paymentStatus) { case 2: return BentoStatusVariant.GREEN; case 1: return BentoStatusVariant.YELLOW; case 0: default: return BentoStatusVariant.RED; } }; const getPaymentText = (paymentStatus: number) => { switch (paymentStatus) { case 2: return 'Paid'; case 1: return 'In progress'; case 0: default: return 'Not paid'; } }; const onRowClick = (item: BentoDatagridDataItem) => { action('row-click')(item); }; // Selection const isDataItemDisabledForSelection = (row: BentoDatagridDataItem) => row.id === DEFAULT_DATA[3].id; const selection = ref<BentoDatagridSelection>([DEFAULT_DATA[1].id]); // Filtering const filteredData = computed(() => // If any filters or search terms have been set filterValues.value?.length === 0 && searchTerm.value === '' ? args.data : args.data.filter(item => { const foundFilteredItem = filterValues.value?.length > 0 ? filterValues.value.every(filter => filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true ) : true; const foundSearchFilteredItem = searchTerm.value ? (item?.company as string) .toLowerCase() .includes((searchTerm.value as string).toLowerCase()) : true; return foundFilteredItem && foundSearchFilteredItem; }) ); const filteredAndSortedData = computed(() => { if (sortBy.value?.length === 0) { return filteredData.value; } return sortBy.value[0].direction === BentoDatagridSortDirection.ASCENDING ? filteredData.value.sort((a, b) => (a[sortBy.value[0].field] as string).localeCompare(b[sortBy.value[0].field] as string) ) : filteredData.value .sort((a, b) => (a[sortBy.value[0].field] as string).localeCompare(b[sortBy.value[0].field] as string) ) .reverse(); }); const rowActions: BentoDataGridRowActionsProp = (item: BentoDatagridDataItem) => { if (item.id === DEFAULT_DATA[3].id) { return [ { variant: 'edit', event: action('edit'), tooltipText: 'Edit payment', }, ]; } else if (item.id === DEFAULT_DATA[2].id) { return [ { variant: 'edit', event: action('edit'), tooltipText: 'Edit payment' }, { variant: 'open', isShort: true, menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ]; } else if (item.id === DEFAULT_DATA[1].id) { return []; } return [ { variant: 'edit', event: action('edit'), tooltipText: 'Edit payment', }, { title: 'Delete', critical: true, event: action('Delete'), tooltipText: 'Delete payment', }, ]; }; return { // Methods getPaymentStatus, getPaymentText, // Functions onRowClick, isDataItemDisabledForSelection, // Values columns, filteredAndSortedData, sortBy, filtersConfig, filterValues, searchTerm, isCondensed, selection, rowActions, // Enums BentoDateFormat, // Vue 3 storybook fix args, }; }, }), args: { ...DEFAULT_ARGS, zebraRows: true, filters: [ { field: 'company', label: 'Company', value: null, type: BentoFilterItemType.INPUT, }, { field: 'countryCode', label: 'Country/Region', value: undefined, options: { listboxItems: [ { value: BentoCountryCode.NL, }, { value: BentoCountryCode.BE, }, ], variant: 'name', }, type: BentoFilterItemType.SELECT_COUNTRY, }, ], columns: [ { field: 'company', label: 'Company', sortable: true, mandatory: true, autoWidth: true, frozen: true, minWidth: 200, }, { field: 'paymentStatus', label: 'Status', width: 180, infoIconTooltip: 'For current day' }, { field: 'reference', label: 'Reference', width: 320, overflow: BentoColumnOverflow.ELLIPSIS }, { field: 'countryCode', label: 'Country/Region', width: 320 }, { field: 'payment', label: 'Payment', width: 180, numeric: true }, { field: 'date', label: 'Execution Date', width: 200 }, ], allowRowClicks: true, allowColumnDragAndDrop: true, columnPanel: true, highlightedRows: [DEFAULT_DATA[0].id, DEFAULT_DATA[4].id], }, parameters: storybookDocsParameter(DataGridEverythingBagel), }; export const DisplayOptions = (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoDate }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :columns.sync="columns" :condensed.sync="isCondensed"> <template #item-date="{item}"> <bento-date :date="new Date(item.registered)" :format='BentoDateFormat.FULL_DATE_TIME_WITH_PERIOD'/> </template> </bento-data-grid> `, setup(props) { const args = isVue2 ? props : _args; const isCondensed = ref(args.condensed); const columns = ref([ { field: 'name', label: 'Name', minWidth: 200, mandatory: true }, { field: 'reference', label: 'Reference', width: 320, overflow: BentoColumnOverflow.ELLIPSIS, frozen: true, }, { field: 'company', label: 'Company', minWidth: 150, width: 250, frozen: true }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, { field: 'countryCode', label: 'Country/Region', width: 280 }, { field: 'date', label: 'Execution Date', width: 300 }, ]); return { // Values columns, isCondensed, // Enums BentoDateFormat, // Vue 3 storybook fix args, }; }, }); DisplayOptions.args = { ...DEFAULT_ARGS, allowColumnDragAndDrop: true, columnPanel: true, }; DisplayOptions.parameters = storybookDocsParameter(` <bento-data-grid column-panel :data="dataArray" :columns.sync="columns" :condensed.sync="isCondensed" />`); export const GridInCard: Story = { render: (_args, { argTypes }) => ({ components: { BentoCard, BentoDataGrid, BentoDate }, props: Object.keys(argTypes), template: ` <div style="max-width: 500px;"> <bento-card> Grid in a card <template #content> <div style="height: 300px;"> <bento-data-grid v-bind="args" :columns.sync="columns" :condensed.sync="isCondensed" :pagination="pagination" fit-content > <template #item-date="{item}"> <bento-date :date="new Date(item.registered)" :format='BentoDateFormat.FULL_DATE_TIME_WITH_PERIOD'/> </template> </bento-data-grid> </div> </template> </bento-card> </div> `, setup(props) { const args = isVue2 ? props : _args; const isCondensed = ref(args.condensed); const pagination = ref({ hasNext: true, size: 20, page: 1, description: 'datagrid navigation', }); const columns = ref([ { field: 'name', label: 'Name', minWidth: 200, mandatory: true }, { field: 'reference', label: 'Reference', width: 320, overflow: BentoColumnOverflow.ELLIPSIS, }, { field: 'company', label: 'Company', minWidth: 150, width: 250 }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, { field: 'countryCode', label: 'Country/Region', width: 280 }, { field: 'date', label: 'Execution Date', width: 300 }, ]); return { // Values columns, isCondensed, pagination, // Enums BentoDateFormat, // Vue 3 storybook fix args, }; }, }), args: { ...DEFAULT_ARGS, columnPanel: false, }, parameters: storybookDocsParameter(DataGridFitContentInCardExample), }; // Default const lazyLoadingCode = ` // With infinite scroll <bento-data-grid :data="dataArray" :columns="[ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]" lazy-load-type="automatic" :loading="yourLoadingLogic" @update:load-more="yourInfiniteScrollHandler" /> // With 'load more' button <bento-data-grid :data="dataArray" :columns="[ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]" lazy-load-type="button" :loading="yourLoadingLogic" @update:load-more="yourInfiniteScrollHandler" />`; export const lazyLoading: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <div style="height: 300px;"> <bento-data-grid v-bind="args" :lazy-load-type="lazyLoadType" :loading="loading" @update:show-more="infiniteScrollHandler"/> </div> `, setup(props) { const args = isVue2 ? props : _args; const loading = ref(false); const data = ref(DEFAULT_DATA); const infiniteScrollHandler = () => { loading.value = true; setTimeout(() => { data.value.push(...ADDITIONAL_DATA); loading.value = false; }, 2000); }; return { args, loading, infiniteScrollHandler, }; }, }), args: { ...DEFAULT_ARGS, lazyLoadType: 'automatic', }, parameters: storybookDocsParameter(lazyLoadingCode), }; // Added just to be able to easily play around with the sticky headers/pagination/filters // Not really relevant as a Storybook example, so not adding storybook code example export const LongDataWithStickyFiltersAndPagination: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const filtersConfig = ref<BentoFilterBarModel>([ { field: 'col1', label: 'Column 1', type: BentoFilterItemType.INPUT, }, { field: 'col2', label: 'Column 2', type: BentoFilterItemType.INPUT, }, ]); const filterValues = ref(); const searchTerm = ref(''); const isCondensed = ref(args.condensed); const columns = ref( Array.from({ length: 20 }, (_, index) => ({ field: `col${index + 1}`, label: `Column ${index + 1}`, width: 200, mandatory: index === 0, })) ); const data = ref( Array.from({ length: 50 }, (_, rowIndex) => columns.value.reduce((acc, col, colIndex) => { acc[col.field] = `Row ${rowIndex + 1} - Col ${colIndex + 1}`; return acc; }, {}) ) ); const dataSet = computed(() => // If any filters or search terms have been set filterValues.value?.length === 0 && searchTerm.value === '' ? data.value : data.value.filter(item => { const foundFilteredItem = filterValues.value?.length > 0 ? filterValues.value.every(filter => filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true ) : true; const foundSearchFilteredItem = searchTerm.value ? Object.keys(item).some(key => key ? (item[key] as string) .toLowerCase() .includes((searchTerm.value as string).toLowerCase()) : true ) : true; return foundFilteredItem && foundSearchFilteredItem; }) ); const onSearchCleared = action('search-clear'); const onSearchInput = action('search-input'); const onSearchTermUpdated = action('update:search-term'); return { // Values dataSet, filtersConfig, filterValues, searchTerm, columns, isCondensed, // Methods onSearchCleared, onSearchInput, onSearchTermUpdated, // Vue 3 storybook fix args, }; }, template: ` <bento-data-grid v-bind="args" :data="dataSet" :columns.sync="columns" :condensed.sync="isCondensed" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" @filter-search-clear="onSearchCleared" @filter-search-input="onSearchInput" @update:filter-search-term="onSearchTermUpdated" /> `, }), args: { ...DEFAULT_ARGS, columnPanel: true, stickyPagination: true, stickyFilters: true }, }; // Cell actions export const CellActions: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :columns="columns"> <template #item-custom="{item}"> Value </template> </bento-data-grid> `, setup(props) { // --- Action Handlers --- const editItem: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('edit')(rowDataItem, columnKey); }; const cancelAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('cancel')(rowDataItem, columnKey); }; const transferAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('transfer')(rowDataItem, columnKey); }; const deleteAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('delete')(rowDataItem, columnKey); }; // --- Dynamic Cell Actions Function --- // This function generates actions based on the row's data. const getNameCellActions: BentoColumnCellActionsProp = rowDataItem => { const actions: Array<BentoColumnCellAction> = []; // Conditionally add the 'edit' action only for the first row. if (rowDataItem.id === DEFAULT_DATA[3].id) { actions.push({ variant: 'edit', event: editItem, tooltipText: 'Edit this name' }); } // Add a 'more' menu with dynamic options. actions.push({ variant: 'more', isShort: true, tooltipText: 'Additional actions', menuData: [ { text: 'Cancel payment', event: cancelAction, critical: true, }, { text: 'Transfer payment', event: transferAction, }, { text: 'Delete payment', event: deleteAction, // Example: disable delete for the second row. disabled: rowDataItem.id === DEFAULT_DATA[2].id, }, ], }); return actions; }; // --- Column Definitions --- const columns: Array<BentoColumn> = [ { field: 'name', label: 'Name (Dynamic Actions)', minWidth: 300, mandatory: true, // Use a function for dynamic cell actions cellActions: getNameCellActions, }, { field: 'company', label: 'Reference', autoWidth: true }, { field: 'payment', label: 'Payment (Static Actions)', minWidth: 350, // Use a static array for cell actions cellActions: [ { variant: 'copy', getCopyData: (rowDataItem: BentoDatagridDataItem, columnKey: string) => `${rowDataItem[columnKey]}`, }, { variant: 'open', event: action('open-payment'), tooltipText: 'Open payment details' }, ], }, ]; return { args: isVue2 ? props : _args, columns }; }, }), args: { ...DEFAULT_ARGS, highlightedRows: [DEFAULT_DATA[3].id], columns: [ { field: 'name', label: 'Name (Dynamic Actions)', minWidth: 300, mandatory: true, cellActions: [ () => (rowDataItem: BentoDatagridDataItem) => ({ variant: 'edit', disabled: rowDataItem.id === DEFAULT_DATA[3].id, event: action('edit'), tooltipText: 'Edit this name', }), { variant: 'open', tooltipText: 'Additional actions', menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ], }, { field: 'company', label: 'Reference', autoWidth: true }, { field: 'payment', label: 'Payment (Static Actions)', minWidth: 350, cellActions: [ { variant: 'copy', getCopyData: (rowDataItem: BentoDatagridDataItem, columnKey: string) => `$${rowDataItem[columnKey]}`, }, { variant: 'more', tooltipText: 'Additional actions', menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], isShort: true, }, ], }, { field: 'custom', label: 'Custom template', minWidth: 200, cellActions: [{ variant: 'edit', event: action('edit'), tooltipText: 'Edit custom' }], }, ], }, parameters: storybookDocsParameter(DataGridCellActionsExample), }; // NestContentSlot const NestedContentSlotTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" has-nested-content :should-show-nested-content="filterRowsThatShowNestedContent" > <template #nested-slot="{item}"> <bento-typography>Nested content for {{ item.company }}</bento-typography> </template> </bento-data-grid> </template> <script setup lang="ts"> import { BentoDataGrid, type BentoColumn, type BentoDatagridData, type BentoDatagridShouldShowNestedContent } from '@adyen/bento-vue2' const columns: BentoColumn = [ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'payment', label: 'Payment', minWidth: 300, mandatory: true }, ] const data = ref<BentoDatagridData>([ { id: 1, name: 'First', payment: 20.9 }, { id: 2, name: 'Second', payment: 10.0 }, ]); const filterRowsThatShowNestedContent: BentoDatagridShouldShowNestedContent = (item) => item.id === 2; </script> `; export const NestedContentSlot: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" > <template #nested-slot="{item}"> <div style="height: 80px; text-align: center; display: flex; align-items: center;"> <bento-typography>Nested content slot for {{ item.company }}</bento-typography> </div> </template> </bento-data-grid> `, setup(props) { return { args: isVue2 ? props : _args }; }, }), args: { ...DEFAULT_ARGS, hasNestedContent: true, zebraRows: true, shouldShowNestedContent: (item: BentoDatagridDataItem) => item.id !== DEFAULT_ARGS.data[1].id, }, parameters: storybookDocsParameter(NestedContentSlotTemplateCode), }; export const StickyHorizontalScrollbar: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoButton }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const filtersConfig = ref<BentoFilterBarModel>([ { field: 'col1', label: 'Column 1', type: BentoFilterItemType.INPUT, }, { field: 'col2', label: 'Column 2', type: BentoFilterItemType.INPUT, }, ]); const filterValues = ref(); const searchTerm = ref(''); const isCondensed = ref(args.condensed); const columns = ref( Array.from({ length: 20 }, (_, index) => ({ field: `col${index + 1}`, label: `Column ${index + 1}`, width: 200, mandatory: index === 0, })) ); const data = ref( Array.from({ length: 50 }, (_, rowIndex) => columns.value.reduce((acc, col, colIndex) => { acc[col.field] = `Row ${rowIndex + 1} - Col ${colIndex + 1}`; return acc; }, {}) ) ); const dataSet = computed(() => // If any filters or search terms have been set filterValues.value?.length === 0 && searchTerm.value === '' ? data.value : data.value.filter(item => { const foundFilteredItem = filterValues.value?.length > 0 ? filterValues.value.every(filter => filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true ) : true; const foundSearchFilteredItem = searchTerm.value ? Object.keys(item).some(key => key ? (item[key] as string) .toLowerCase() .includes((searchTerm.value as string).toLowerCase()) : true ) : true; return foundFilteredItem && foundSearchFilteredItem; }) ); const onSearchCleared = action('search-clear'); const onSearchInput = action('search-input'); const onSearchTermUpdated = action('update:search-term'); return { // Values dataSet, filtersConfig, filterValues, searchTerm, columns, isCondensed, // Methods onSearchCleared, onSearchInput, onSearchTermUpdated, // Vue 3 storybook fix args, }; }, template: ` <bento-data-grid v-bind="args" :data="dataSet" :columns.sync="columns" :condensed.sync="isCondensed" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" @filter-search-clear="onSearchCleared" @filter-search-input="onSearchInput" @update:filter-search-term="onSearchTermUpdated" > <template #pagination-slot> <bento-button variant="tertiary"> Pagination slot button </bento-button> </template> </bento-data-grid> `, }), args: { ...DEFAULT_ARGS, pagination: { page: 1, hidePageSize: true }, columnPanel: true, stickyPagination: true, stickyFilters: true, stickyHorizontalScrollbar: true, }, }; const NestedRowsTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" has-nested-rows :should-show-nested-row="filterRowsThatShowNestedContent" @nested-content-toggled="onParentRowToggle" /> </template> <script setup lang="ts"> import { BentoDataGrid, type BentoColumn, type BentoDatagridData, type BentoDatagridShouldShowNestedRowsFn } from '@adyen/bento-vue2' const columns: BentoColumn = [ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'payment', label: 'Payment', minWidth: 300, mandatory: true }, ] const data = ref<BentoDatagridData>([ { id: 1, name: 'First', payment: 20.9 }, { id: 2, name: 'Second', payment: 10.0 }, ]); const filterRowsThatShowNestedRows: BentoDatagridShouldShowNestedRowsFn = (item) => item.id === 2; const onParentRowToggle = async (item: BentoDatagridDataItem) => { const childrenForToggledParentItem = await makeAPICallToGetChildren(item.id); dataState.value = dataState.value.map(itemToChange => { if (itemToChange.id === item.id) { return { ...itemToChange, children: childrenForToggledParentItem } } return itemToChange; }); }; </script> `; export const NestedRows: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="dataState" @nested-content-toggled="onParentRowToggle" /> `, setup(props) { const dataState = ref<Array<BentoDatagridDataItem>>(DEFAULT_ARGS.data); const selection = ref<BentoDatagridDataItemId>(); const onParentRowToggle = (item: BentoDatagridDataItem) => { setTimeout(() => { const children = [ { id: `03c0a799-1ced-4017-aea3-111111${item.id}`, name: `Child of ${item.name} #1`, payment: 160405, company: 'Merchant Helium', registered: '2023-06-30T13:56:09.498Z', tags: ['deserunt', 'et', 'esse'], reference: 'UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn', paymentStatus: 1, countryCode: BentoCountryCode.NL, parentId: DEFAULT_ARGS.data[1].id, }, { id: `03c0a799-1ced-4017-aea3-1111113${item.id}`, name: `Child of ${item.name} #2`, payment: 163500, company: 'Merchant B', registered: '2023-04-28T13:11:09.498Z', tags: ['et', 'Lorem', 'sint'], reference: 'IxUZ9FsEPCZ9dFFPpI9C', paymentStatus: 2, countryCode: BentoCountryCode.BE, parentId: DEFAULT_ARGS.data[1].id, }, { id: `03c0a799-1ced-4017-aea3-1111114${item.id}`, name: `Child of ${item.name} #3`, payment: 78500, company: 'Merchant C', registered: '2023-06-30T11:01:09.498Z', tags: ['laborum', 'enim', 'ullamco'], reference: 'y0qAQ0WDszRyqLY2WO9f', paymentStatus: 1, countryCode: BentoCountryCode.UA, parentId: DEFAULT_ARGS.data[1].id, }, ]; dataState.value = dataState.value.map(itemToChange => { if (itemToChange.id === item.id) { return { ...itemToChange, children, }; } return itemToChange; }); }, 1000); }; return { args: isVue2 ? props : _args, dataState, selection, onParentRowToggle }; }, }), args: { ...DEFAULT_ARGS, hasNestedRows: true, zebraRows: true, shouldShowNestedRows: dataItem => { return dataItem.id !== DEFAULT_ARGS.data[1].id; }, }, parameters: storybookDocsParameter(NestedRowsTemplateCode), }; export const RowActions: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :row-actions="rowActions" /> `, setup(props) { const rowActions: BentoDataGridRowActionsProp = (item: BentoDatagridDataItem) => { if (item.id === DEFAULT_DATA[0].id) { return [ { variant: 'edit', event: action('edit'), tooltipText: 'Edit row' }, { title: 'Delete', critical: true, event: action('Delete'), tooltipText: 'Delete row', }, ]; } else if (item.id === DEFAULT_DATA[1].id) { return []; } else if (item.id === DEFAULT_DATA[2].id) { return [ { variant: 'edit', event: action('edit'), tooltipText: 'Edit row' }, { variant: 'open', isShort: true, tooltipText: 'More options', menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ]; } return [ { variant: 'edit', event: action('edit'), tooltipText: 'Edit row', }, ]; }; return { args: isVue2 ? props : _args, rowActions, }; }, }), args: { ...DEFAULT_ARGS, }, parameters: storybookDocsParameter(DataGridRowActionsExample), }; const SummaryCellTemplateCode = ` <template> <bento-data-grid :columns="columns" :data="dataSet" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term="searchTerm" :filter-search-config="searchConfig" @update:filter-values="filterValues = $event" @update:filter-search-term="searchTerm = $event" @filter-search-clear="onSearchCleared" > <template #item-company="{item}"> <bento-data-grid-summary-cell :label="\`\${item.company.length} merchants\`" :items="item.company" :search-term="companyColumnSearchTerm" /> </template> </bento-data-grid> </template> <script setup lang="ts"> import { BentoColumn, BentoDataGrid, BentoDataGridSummaryCell, BentoDatagridData, BentoFilterBarModel, BentoFilterBarSearchBarProps, BentoFilterItemType, BentoFilterValues, BentoCountryCode, } from '@adyen/bento-vue2'; import { computed, ref } from 'vue'; const columns: Array<BentoColumn> = [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ]; /** * Data set with array of companies */ const multiData: BentoDatagridData = [ { id: '03c0a799-1ced-4017-aea3-0cebb0106335', name: 'Shepherd Kane', payment: 160405, company: ['Merchant Helium', 'Merchant A'], }, { id: '8f9e7877-1ca2-4929-9217-f1ec78709291', name: 'Joyce Johns', payment: 78500, company: ['Merchant C', 'Merchant Hedra'], }, { id: '51d2b31d-d577-4fca-8329-a583babbbdda', name: 'Evans Zamora', payment: 1153.4, company: [ 'Merchant A', 'Merchant B', 'Merchant C', 'Merchant D', 'Merchant E', 'Merchant F', 'Merchant G', 'Merchant H', 'Merchant I', 'Merchant J', ], }, ]; const filtersConfig = computed<BentoFilterBarModel>(() => [ { field: 'name', label: 'Name', type: BentoFilterItemType.INPUT, }, { field: 'company', label: 'Company', type: BentoFilterItemType.INPUT, }, ]); /** * The values of applied filters. */ const filterValues = ref<BentoFilterValues>(); /** * The current global search term that is being searched with */ const searchTerm = ref(''); /** * Global search configuration */ const searchConfig = ref<BentoFilterBarSearchBarProps>({ inputFieldAriaLabel: 'Descriptive details about the search bar', debounceTime: 1000, // in ms placeholder: 'Find merchants', }); /** * Concatenated search term using the global search and the company filter value */ const companyColumnSearchTerm = computed( () => searchTerm.value || filterValues.value?.find(filter => filter.field === 'company')?.value?.toString() || '' ); /** * Simplified computed data set that filters data depending on applied filters and global search values */ const dataSet = computed<BentoDatagridData>(() => { let filteredData = multiData; // Apply global search term filter if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); filteredData = filteredData.filter( item => (item.name as string).toLowerCase().includes(lowerCaseSearchTerm) || (item.company as string[]).some(company => company.toLowerCase().includes(lowerCaseSearchTerm) ) ); } // Apply filterValues filter if (filterValues.value?.length) { filteredData = filteredData.filter(item => filterValues.value?.every(filter => { if (filter.field === 'company') { return filter.value ? (item[filter.field] as string[]).some(company => (company as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) ) : true; } return filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true; }) ); } return filteredData; }); const onSearchCleared = () => { searchTerm.value = ''; }; </script> `; export const SummaryCell: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, BentoDashedUnderline, BentoDataGridSummaryCell, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="dataSet" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term="searchTerm" :filter-search-config="searchConfig" @update:filter-values="filterValues = $event" @update:filter-search-term="searchTerm = $event" @filter-search-clear="onSearchCleared" > <template #item-company="{item}"> <bento-data-grid-summary-cell :label="\`\${item.company.length} merchants\`" :items="item.company" :search-term="companyColumnSearchTerm" /> </template> </bento-data-grid> `, setup(props) { /** * Data set with array of companies */ const multiData: BentoDatagridData = [ { id: '03c0a799-1ced-4017-aea3-0cebb0106335', name: 'Shepherd Kane', payment: 160405, company: ['Merchant Helium', 'Merchant A'], }, { id: 'b6d4f1fd-b1f6-40a6-a443-f785c89604ef', name: 'Santos Palmer', payment: 163500, company: ['Merchant B'], }, { id: '8f9e7877-1ca2-4929-9217-f1ec78709291', name: 'Joyce Johns', payment: 78500, company: ['Merchant C', 'Merchant Hedra'], }, { id: '0789a012-85a6-4657-87bb-319141938a61', name: 'Lynnette Melendez', payment: -45000, company: ['Merchant A', 'Merchant B', 'Merchant C'], }, { id: '0b3981b6-c73b-47ad-81d1-b06f9b95d467', name: 'Wagner Mcintosh', payment: 1112, company: ['Merchant A'], }, { id: '51d2b31d-d577-4fca-8329-a583babbbdda', name: 'Evans Zamora', payment: 1153.4, company: [ 'Merchant A', 'Merchant B', 'Merchant C', 'Merchant D', 'Merchant E', 'Merchant F', 'Merchant G', 'Merchant H', 'Merchant I', 'Merchant J', 'Merchant K', 'Merchant L', 'Merchant M', 'Merchant N', 'Merchant O', 'Merchant P', 'Merchant Q', 'Merchant R', 'Merchant S', 'Merchant T', ], }, { id: '9bd59a6e-bd03-4477-813f-b844953b7645', name: 'Anderson Middleton', payment: 181722.23, company: ['Merchant A'], }, ]; const filtersConfig = computed<BentoFilterBarModel>(() => [ { field: 'name', label: 'Name', type: BentoFilterItemType.INPUT, }, { field: 'company', label: 'Company', type: BentoFilterItemType.INPUT, }, ]); /** * The values of applied filters. */ const filterValues = ref<BentoFilterValues>(); /** * The current global search term that is being searched with */ const searchTerm = ref(''); /** * Global search configuration */ const searchConfig = ref<BentoFilterBarSearchBarProps>({ inputFieldAriaLabel: 'Descriptive details about the search bar', debounceTime: 1000, // in ms placeholder: 'Find merchants', }); /** * Concatenated search term using the global search and the company filter value */ const companyColumnSearchTerm = computed( () => searchTerm.value || filterValues.value?.find(filter => filter.field === 'company')?.value?.toString() || '' ); /** * Simplified computed data set that filters data depending on applied filters and global search values */ const dataSet = computed<BentoDatagridData>(() => { let filteredData = multiData; // Apply global search term filter if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); filteredData = filteredData.filter( item => (item.name as string).toLowerCase().includes(lowerCaseSearchTerm) || (item.company as Array<string>).some(company => company.toLowerCase().includes(lowerCaseSearchTerm) ) ); } // Apply filterValues filter if (filterValues.value?.length) { filteredData = filteredData.filter(item => filterValues.value?.every(filter => { if (filter.field === 'company') { return filter.value ? (item[filter.field] as Array<string>).some(company => (company as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) ) : true; } return filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true; }) ); } return filteredData; }); const onSearchCleared = () => { searchTerm.value = ''; }; return { args: isVue2 ? props : _args, filtersConfig, filterValues, searchTerm, searchConfig, onSearchCleared, dataSet, companyColumnSearchTerm, }; }, }), args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ], zebraRows: true, }, parameters: storybookDocsParameter(SummaryCellTemplateCode), }; export const TrendCell: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoDataGridTrendCell, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args"> <template #item-performance="{ item }"> <bento-data-grid-trend-cell :value="item.performance.value" :direction="item.performance.direction" :variant="item.performance.variant" /> </template> </bento-data-grid> `, setup(props) { const columns: Array<BentoColumn> = [ { field: 'name', label: 'Name', flex: 2 }, { field: 'performance', label: 'Performance', flex: 1, numeric: true }, ]; const data: BentoDatagridData = [ { id: '1', name: 'Shepherd Kane', performance: { value: 25.5, variant: 'green', direction: 'up', }, }, { id: '2', name: 'Santos Palmer', performance: { value: 10.2, variant: 'red', direction: 'down', }, }, { id: '3', name: 'Joyce Johns', performance: { value: 0, variant: 'black', direction: 'none', }, }, { id: '4', name: 'Lynnette Melendez', performance: { value: '3.1428571429%', variant: 'green', direction: 'down', }, }, { id: '5', name: 'Wagner Mcintosh', performance: { value: '15.2%', variant: 'red', direction: 'up', }, }, { id: '6', name: 'Evans Zamora', performance: { value: '1.23', direction: 'up', }, }, { id: '7', name: 'Anderson Middleton', performance: { value: '90', direction: 'down', }, }, ]; return { args: { ...(isVue2 ? props : _args), columns, data }, }; }, }), args: { ...DEFAULT_ARGS, zebraRows: true, }, parameters: storybookDocsParameter(DataGridTrendCellExample), }; export const TagWithTextCell: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoDataGridTagWithTextCell, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args"> <template #item-endpoint="{ item }"> <bento-data-grid-tag-with-text-cell :tag="item.endpoint.tag" :value="item.endpoint.value" /> </template> </bento-data-grid> `, setup(props) { const columns = [ { field: 'name', label: 'Name', flex: 2 }, { field: 'endpoint', label: 'Endpoint', flex: 2 }, ]; const data = [ { id: '1', name: 'Get users', endpoint: { tag: { label: 'GET', variant: 'blue' }, value: '/api/users', }, }, { id: '2', name: 'Create user', endpoint: { tag: { label: 'POST', variant: 'green' }, value: '/api/users', }, }, { id: '3', name: 'Update user', endpoint: { tag: { label: 'PUT', variant: 'yellow' }, value: '/api/users/:id', }, }, { id: '4', name: 'Delete user', endpoint: { tag: { label: 'DELETE', variant: 'red' }, value: '/api/users/:id', }, }, ]; return { args: { ...(isVue2 ? props : _args), columns, data }, }; }, }), args: { ...DEFAULT_ARGS, }, parameters: storybookDocsParameter(DataGridTagWithTextCellExample), };
1
+ import { type BentoColumn, BentoColumnOverflow, type BentoDatagridData, type BentoDatagridDataItem, type BentoDatagridDataItemId, BentoDatagridEvent, type BentoDatagridPaginationProps, type BentoDatagridSelection, type BentoDatagridSortByColumn, BentoDatagridSortDirection, } from './data-grid.types'; import { BentoCard } from '@/components/card'; import { action } from '@storybook/addon-actions'; import { type BentoFilterBarModel, type BentoFilterBarSearchBarProps, BentoFilterItemType, type BentoFilterValues, } from '../filter-bar'; import { BentoCurrency } from '@/components/currency'; import { BentoStatus, BentoStatusVariant } from '@/components/status'; import { BentoLink } from '@/components/link'; import { BentoTypography } from '@/components/typography'; import { computed, ref, watch } from 'vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { BentoDate, BentoDateFormat } from '@/components/date'; import { BentoCountry, BentoCountryCode } from '@/components/country'; import type { Meta, StoryObj } from '@storybook/vue'; import BentoDataGrid from './data-grid.vue'; import { isVue2 } from 'vue-demi'; import { BentoButton } from '../button'; import { BentoDashedUnderline } from '../dashed-underline'; import { BentoDataGridSummaryCell, BentoDataGridTagWithTextCell, BentoDataGridTrendCell } from './components'; import type { BentoColumnCellAction, BentoColumnCellActionEvent, BentoColumnCellActionsProp, BentoDataGridRowActionsProp, } from './components/data-grid-cell-actions/data-grid-cell-actions.types'; // Examples import DataGridEverythingBagel from './__tests__/data-grid-everything-bagel.vue?raw'; import DataGridCellActionsExample from './__tests__/data-grid-cell-actions-example.vue?raw'; import DataGridRowActionsExample from './__tests__/data-grid-row-actions-example.vue?raw'; import DataGridFitContentInCardExample from './__tests__/data-grid-fit-content-in-card-example.vue?raw'; import DataGridWithSlotsExample from './__tests__/data-grid-with-slots-example.vue?raw'; import DataGridFilteringExample from './__tests__/data-grid-filtering-example.vue?raw'; import DataGridSelectionExample from './__tests__/data-grid-selection-example.vue?raw'; import DataGridTrendCellExample from './__tests__/data-grid-trend-cell-example.vue?raw'; import DataGridTagWithTextCellExample from './__tests__/data-grid-tag-with-text-cell-example.vue?raw'; import { DEFAULT_DATA } from './__tests__/data-grid-example-data'; const meta: Meta = { title: 'Data Grid', component: BentoDataGrid, argTypes: { // Values lazyLoadType: { control: { type: 'select', }, }, // Filter bar props filters: { table: { type: { summary: 'BentoFilterBarModel' }, }, }, filterSearchTerm: { name: 'filterSearchTerm', defaultValue: undefined, description: "Filter bar search term useful to filter all the data by text. If the value is non-existant (`null` or `undefined`) the search bar won't be enabled.", control: { type: 'text', }, }, // Slots default: { table: { disable: true }, }, header: { name: 'header', defaultValue: '', description: 'header text', table: { type: { summary: 'VNode[]' }, }, control: { type: 'text', }, }, footer: { name: 'footer', defaultValue: '', description: 'footer text', table: { type: { summary: 'VNode[]' }, }, control: { type: 'text', }, }, 'head-{columnKey}': { name: 'head-{columnKey}', defaultValue: '', description: 'the column slot where `{columnKey}` is the name of the column key. Allows the Column header to be customized.', table: { type: { summary: 'VNode[]' }, category: 'slots', }, control: { disable: true }, }, 'item-{columnKey}': { name: 'item-{columnKey}', defaultValue: '', description: 'the data item slot where `{columnKey}` is the name of the data item column key. Allow row cells to be customized.', table: { type: { summary: 'VNode[]' }, category: 'slots', }, control: { disable: true }, }, ['pagination-slot']: { control: { type: 'text' }, description: 'Passes content to the default pagination slot', table: { type: { summary: 'VNode[]' }, }, }, // Events [BentoDatagridEvent.COLUMN_RESIZE]: { name: BentoDatagridEvent.COLUMN_RESIZE, defaultValue: '', description: 'Triggered when a column is resized. Provides a field name and the new resized width in pixels.', table: { category: 'events', type: { summary: '({ field, newResizedWidth }) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.SELECT]: { name: BentoDatagridEvent.SELECT, defaultValue: '', description: 'Triggered when a row is selected. Provides an array of data item ids. `BentoDatagridDataItemId[]`', table: { category: 'events', type: { summary: '(selectedDataItemIds) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.NAVIGATE]: { name: BentoDatagridEvent.NAVIGATE, defaultValue: '', description: `Triggered when the navigation controls in the pagination are clicked (i.e. when the arrow buttons to the rightmost of the screen or the dropdown on the right hand are updated). It indicates to which page it should go next.`, table: { category: 'events', type: { summary: '(pageNumber: number) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.ITEMS_PAGE]: { name: BentoDatagridEvent.ITEMS_PAGE, defaultValue: '', description: `Triggered when the number of items per page is changed from the left hand dropdown of the pagination. It indicates the new number of items that will be displayed.`, table: { category: 'events', type: { summary: '(itemsPerPage: number) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.SORT]: { name: BentoDatagridEvent.SORT, defaultValue: '', description: `Triggered when a column is sorted. Provides an array of \`BentoDatagridSortByColumn\` which includes the \`field\` and the sort \`direction\` (\`BentoDatagridSortDirection\`) of the column:`, table: { category: 'events', type: { summary: '(sortByDirections) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_COLUMNS]: { name: BentoDatagridEvent.UPDATE_COLUMNS, defaultValue: '', description: `Triggered when a the columns have been updated. Provides an updated array of columns \`BentoColumn[]\` which should be passed back into the \`columns\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:columns.sync="columns"\``, table: { category: 'events', type: { summary: '(columns) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_CONDENSED]: { name: 'update:condensed', defaultValue: '', description: `Triggered when the config panel's condensed view toggle is interacted with. Provides the updated condensed \`boolean\`. (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:condensed.sync="condensed"\``, table: { category: 'events', type: { summary: '(condensed) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_FILTER_VALUES]: { name: BentoDatagridEvent.UPDATE_FILTER_VALUES, defaultValue: '', description: `Triggered when a the filters in the filterbar have updated. Provides an array of filters \`BentoFilterBarModel[]\` which should be passed back into the \`filter-values\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:filter-values.sync="filtersValues"\``, table: { category: 'events', type: { summary: '(filterValues) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_PAGINATION]: { name: BentoDatagridEvent.UPDATE_PAGINATION, defaultValue: '', description: `Triggered when a the pagination state has been updated. Provides an updated \`pagination\` properties object which should be passed back into the \`pagination\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:pagination.sync="pagination"\``, table: { category: 'events', type: { summary: '(paginationProps) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_SELECTION]: { name: BentoDatagridEvent.UPDATE_SELECTION, defaultValue: '', description: `Triggered when a the select data rows state has been updated. Provides an array of row IDs \`BentoDatagridDataItemId[]\` which should be passed back into the \`selection\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:selection.sync="selection"\``, table: { category: 'events', type: { summary: '(selection) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_SORT_BY]: { name: 'update:sort-by', defaultValue: '', description: `Triggered when a the sorting state has been updated. Provides an array of sorting directions \`BentoDatagridSortByColumn[]\` which should be passed back into the \`sort-by\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:sort-by.sync="sortBy"\``, table: { category: 'events', type: { summary: '(sortingDirections) => void' }, }, control: { disable: true }, }, // Filter bar search events [BentoDatagridEvent.FILTER_SEARCH_INPUT]: { name: BentoDatagridEvent.FILTER_SEARCH_INPUT, defaultValue: '', description: "Triggered whenever the filter's search is updated.", table: { category: 'events', type: { summary: '(newSearchTerm) => void' } }, control: { disable: true }, }, [BentoDatagridEvent.FILTER_SEARCH_CLEAR]: { name: BentoDatagridEvent.FILTER_SEARCH_CLEAR, defaultValue: '', description: "Triggered whenever the filter's search is cleared.", table: { category: 'events', type: { summary: '() => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM]: { name: BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM, defaultValue: '', description: "Triggered whenever the filter's search is updated to sync the model.", table: { category: 'events', type: { summary: '(newSearchTerm) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.ROW_CLICK]: { name: BentoDatagridEvent.ROW_CLICK, defaultValue: '', description: 'Triggered whenever a row has been clicked.', table: { category: 'events', type: { summary: '(row: BentoDatagridDataItem, event: MouseEvent) => void' }, }, }, [BentoDatagridEvent.NESTED_CONTENT_TOGGLED]: { name: BentoDatagridEvent.NESTED_CONTENT_TOGGLED, defaultValue: '', description: 'Triggered whenever a data item toggled is nested content', table: { category: 'events', type: { summary: '(dataItem: BentoDatagridDataItem) => void' }, }, control: { disable: true }, }, }, }; export default meta; type Story = StoryObj<typeof BentoDataGrid>; const DEFAULT_COLUMNS: Array<BentoColumn> = [ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Company', autoWidth: true }, { field: 'payment', label: 'Payment', minWidth: 350 }, ]; const ADDITIONAL_DATA: BentoDatagridData = [ { id: '03c0a799-1ced-4017-aea3-0cebb0106335', name: 'Additional data fetched here', payment: 160405, company: 'Merchant Helium', registered: '2023-06-30T13:56:09.498Z', tags: ['deserunt', 'et', 'esse'], reference: 'UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn', paymentStatus: 1, countryCode: BentoCountryCode.NL, }, { id: 'b6d4f1fd-b1f6-40a6-a443-f785c89604ef', name: 'Santos Palmer', payment: 163500, company: 'Merchant B', registered: '2023-04-28T13:11:09.498Z', tags: ['et', 'Lorem', 'sint'], reference: 'IxUZ9FsEPCZ9dFFPpI9C', paymentStatus: 2, countryCode: BentoCountryCode.BE, }, { id: '8f9e7877-1ca2-4929-9217-f1ec78709291', name: 'Joyce Johns', payment: 78500, company: 'Merchant C', registered: '2023-06-30T11:01:09.498Z', tags: ['laborum', 'enim', 'ullamco'], reference: 'y0qAQ0WDszRyqLY2WO9f', paymentStatus: 1, countryCode: BentoCountryCode.UA, }, { id: '0789a012-85a6-4657-87bb-319141938a61', name: 'Lynnette Melendez', payment: -45000, company: 'STRALOY', registered: '2019-08-01T13:56:01.498Z', tags: ['sit', 'consectetur', 'amet'], reference: '2vv5pzbRSwx8YsFoGGOC', paymentStatus: 1, countryCode: BentoCountryCode.BR, }, { id: '0b3981b6-c73b-47ad-81d1-b06f9b95d467', name: 'Wagner Mcintosh', payment: 1112, company: 'Merchant A', registered: '2022-12-30T02:12:09.498Z', tags: ['ad', 'ullamco', 'et'], reference: '54KN4jkpBuaRY5xMfe84', paymentStatus: 1, countryCode: BentoCountryCode.US, }, { id: '51d2b31d-d577-4fca-8329-a583babbbdda', name: 'Evans Zamora', payment: 1153.4, company: 'GEEKWAGON', registered: '2023-06-30T13:51:09.498Z', tags: ['consequat', 'magna', 'sit'], reference: 'Sj84kad0MGxpuozTjEk', paymentStatus: 0, countryCode: BentoCountryCode.FR, }, { id: '9bd59a6e-bd03-4477-813f-b844953b7645', name: 'Anderson Middleton', payment: 181722.23, company: 'BALOOBA', registered: '2021-03-02T01:34:09.498Z', tags: ['aliqua', 'adipisicing', 'magna'], reference: 'MtJ5Bnew0MGxpuozTjEk', paymentStatus: 1, countryCode: BentoCountryCode.IT, }, ]; const DEFAULT_ARGS = { data: DEFAULT_DATA, columns: DEFAULT_COLUMNS, allowColumnDragAndDrop: false, pagination: {}, emptyState: { title: 'No results were found', image: 'no-results-found', variant: 'embedded', action: { title: 'Clear filters', event: action('clear'), }, description: 'Try a different term or reset search filters', }, }; // Default const DefaultCode = ` <template> <bento-data-grid :allow-row-clicks="false" :allow-column-drag-and-drop="false" :bulk-actions="undefined" :condensed="false" :columns="columns" :column-panel="false" :data="data" :empty-state="undefined" :filters="undefined" :filters-search-term="undefined" :fit-content="false" :get-data-item-id="undefined" :has-resizeable-columns="false" :has-nested-content="false" :highlighted-rows="undefined" :is-data-item-disabled="undefined" lazy-load-type="none" :loading="false" :outline="false" :no-spacing-top="false" :pagination="undefinded" :selectable-accross-pages="false" :sticky-pagination="false" :sticky-filters="false" :selectable="false" :selection="undefined" :sort-by="undefined" :tab-title="undefined" :truncate-column-headers="false" /> </template> <script setup lang="ts"> import { BentoColumn, BentoDataGrid, BentoDatagridData } from '@adyen/bento-vue2'; import { ref } from 'vue'; const data = ref<BentoDatagridData>(${JSON.stringify(DEFAULT_DATA)}); const columns = ref<BentoColumn[]>([ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]); </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" /> `, setup(props) { return { args: isVue2 ? props : _args }; }, }), args: DEFAULT_ARGS, parameters: storybookDocsParameter(DefaultCode), }; // Fluid Columns const FluidTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" /> </template> <script setup lang="ts"> import { BentoColumn, BentoDataGrid, BentoDatagridData } from '@adyen/bento-vue2'; import { ref } from 'vue'; const data = ref<BentoDatagridData>(${JSON.stringify(DEFAULT_DATA)}); const columns = ref<BentoColumn[]>([ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ]); </script> `; const rowActionsFluid: BentoDataGridRowActionsProp = (item: BentoDatagridDataItem) => { if (item.id === DEFAULT_DATA[3].id) { return [ { variant: 'edit', event: action('edit'), }, ]; } else if (item.id === DEFAULT_DATA[2].id) { return [ { variant: 'edit', event: action('edit') }, { variant: 'open', isShort: true, menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ]; } else if (item.id === DEFAULT_DATA[1].id) { return []; } return [ { variant: 'edit', event: action('edit'), }, { title: 'Delete', critical: true, event: action('Delete'), }, ]; }; export const Fluid: Story = { ...Default, args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ], rowActions: rowActionsFluid, }, parameters: storybookDocsParameter(FluidTemplateCode), }; // Slots export const Slots: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" > <template #header> <bento-typography variant="title" el="h2"> Users </bento-typography> </template> <template #head-name="{column}"> <bento-typography variant="caption"> {{ column.label }} </bento-typography> </template> <template #item-name="{item}"> <bento-typography variant="body"> {{ item.name }} </bento-typography> <template v-if="item.company === 'Merchant Helium'"> <div style="display: inline-flex; gap: 4px;"> <bento-typography v-for="itemTag in item.tags" :key="itemTag" variant="caption" :style="{ color: 'var(--b-color-label-secondary)' }" > { {{ itemTag }} } </bento-typography> </div> </template> </template> <template #item-company="{item}"> <bento-link isNotRouting to="https://www.adyen.com" :title="item.company" > {{ item.company }} </bento-link> <bento-typography v-if="item.company === 'Merchant B'" variant="caption" :style="{ color: 'var(--b-color-label-secondary)' }"> {{ item.registered }} </bento-typography> </template> <template #item-payment="{item}"> $ {{ item.payment }} </template> <template #footer> <bento-typography variant="caption"> Footer </bento-typography> </template> </bento-data-grid> `, setup(props) { return { args: isVue2 ? props : _args }; }, }), args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ], }, parameters: storybookDocsParameter(DataGridWithSlotsExample), }; // Sorting const useSort = () => { const sortBy = ref<Array<BentoDatagridSortByColumn>>([]); const data = ref<BentoDatagridData>(DEFAULT_DATA); const sort = (sortedColumns: Array<BentoDatagridSortByColumn>) => { data.value = sortedColumns[0].direction === BentoDatagridSortDirection.ASCENDING ? data.value.sort((a, b) => (a[sortedColumns[0].field] as string).localeCompare(b[sortedColumns[0].field] as string) ) : data.value .sort((a, b) => (a[sortedColumns[0].field] as string).localeCompare(b[sortedColumns[0].field] as string) ) .reverse(); action('sort')(sortedColumns); }; return { data, sortBy, sort }; }; const SortingTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" @sort="sort" :sort-by.sync="sortBy" /> </template> <script setup lang="ts"> import { BentoDataGrid, type BentoDatagridSortByColumn, type BentoDatagridData, type BentoColumn, } from '@adyen/bento-vue2' // Sortable fields must have "sortable: true" const columns: BentoColumn = [ { field: 'id', label: 'ID', minWidth: 300, sortable: true, mandatory: true }, { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, ] const data = ref<BentoDatagridData>([ { id: 1, name: 'First' }, { id: 2, name: 'Second' }, ]); const sortBy = ref<Array<BentoDatagridSortByColumn>>([]); const sort = (sortedColumns: Array<BentoDatagridSortByColumn>) => { data.value = sortedColumns[0].direction === BentoDatagridSortDirection.ASCENDING ? data.value.sort((a, b) => (a[sortedColumns[0].field] as number) .toString() .localeCompare(b[sortedColumns[0].field] as string) ) : data.value .sort((a, b) => (a[sortedColumns[0].field] as number) .toString() .localeCompare(b[sortedColumns[0].field] as string) ) .reverse(); } </script> `; export const Sorting: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid :data="data" :columns="columns" :sort-by.sync="sortBy" @sort="sort" /> `, setup(props) { const { data, sortBy, sort } = useSort(); return { args: isVue2 ? props : _args, data, sortBy, sort, }; }, }), args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', minWidth: 300, sortable: true, mandatory: true }, { field: 'company', label: 'Company', minWidth: 150, sortable: true }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ], sortBy: [], }, parameters: storybookDocsParameter(SortingTemplateCode), }; export const Selection: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <div> <bento-data-grid v-bind="args" :data="pagedData" :pagination="pagination" :filters="filtersConfig" :filter-values="filterValues" :is-data-item-disabled="isDataItemDisabled" :selection="selection" :filter-search-term="searchTerm" @update:filter-search-term="searchTerm = $event" @update:pagination="pagination = $event" @update:filter-values="filterValues = $event" @select-all-across-pages="onSelectAllAcrossPages" @select-all-current-results="onSelectAllInCurrentResults" @deselect-all-current-results="onDeselectAllInCurrentResults" @filter-search-clear="onSearchCleared" @update:selection="onSelect" @row-click="rowClickAction" /> </div> `, setup(props) { const args = isVue2 ? props : _args; const selection = ref([]); const filtersConfig = computed<BentoFilterBarModel>(() => args?.filters ?? FILTERS); const filterValues = ref<BentoFilterValues>([]); const searchTerm = ref(''); const data = ref( Array.from({ length: 20 }, (_, i) => ({ id: `id-${i + 1}`, name: `Name ${i + 1}`, payment: (i + 1) * 100, company: `Merchant ${i + 1}`, })) ); const isDataItemDisabled = (row: BentoDatagridDataItem) => { return row.id === data.value[3].id; }; const pagination = ref<BentoDatagridPaginationProps>({ page: 1, size: 10, totalCount: 20, hasNext: true, }); const filteredData = computed<BentoDatagridData>(() => { let internalData = data.value; if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); const searchableFields = args.columns.map(column => column.field); internalData = internalData.filter((item: BentoDatagridDataItem) => { return searchableFields.some(field => { const value = String(item[field]); return value.toLowerCase().includes(lowerCaseSearchTerm); }); }); } return internalData.filter(item => filterValues.value.every(filter => filter.value ? String(item[filter.field]).toLowerCase().includes(String(filter.value).toLowerCase()) : true ) ); }); const pagedData = computed<BentoDatagridData>(() => { return filteredData.value.slice( (pagination.value.page - 1) * pagination.value.size, pagination.value.page * pagination.value.size ); }); const enabledIds = computed(() => data.value.filter(item => !isDataItemDisabled(item)).map(x => x.id)); const enabledFilteredIds = computed(() => filteredData.value.filter(item => !isDataItemDisabled(item)).map(x => x.id) ); const onSelect = value => { selection.value = value; action('update:selection')(value); }; const onSelectAllAcrossPages = () => { selection.value = enabledIds.value; action('select-all-across-pages')(); }; const onSelectAllInCurrentResults = () => { selection.value = [...new Set([...selection.value, ...enabledFilteredIds.value])]; action('select-all-current-results')(); }; const onDeselectAllInCurrentResults = () => { selection.value = selection.value.filter(item => !enabledFilteredIds.value.includes(item)); action('deselect-all-current-results')(); }; const onSearchCleared = () => { searchTerm.value = ''; action('search-clear')(); }; watch( filteredData, () => { pagination.value.totalCount = filteredData.value.length; }, { immediate: true } ); return { args, onSelect, onSelectAllAcrossPages, onSelectAllInCurrentResults, onDeselectAllInCurrentResults, pagination, selection, pagedData, filtersConfig, filterValues, searchTerm, rowClickAction: value => action('row-click')(value), isDataItemDisabled, onSearchCleared, }; }, }), args: { ...DEFAULT_ARGS, selectable: true, allowRowClicks: true, selectableAcrossPages: true, bulkActions: [ { title: 'Edit', event: () => true, }, { title: 'Delete', critical: true, event: () => true, }, ], }, parameters: storybookDocsParameter(DataGridSelectionExample), }; // Filtering const FILTERS: BentoFilterBarModel = [ { field: 'name', label: 'Name', type: BentoFilterItemType.INPUT, }, { field: 'company', label: 'Company', type: BentoFilterItemType.INPUT, }, ]; export const Filtering: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const filtersConfig = computed<BentoFilterBarModel>(() => args?.filters ?? FILTERS); const filterValues = ref<BentoFilterValues>([]); const searchTerm = ref(''); const searchConfig = ref<BentoFilterBarSearchBarProps>({ inputFieldAriaLabel: 'Search for items', placeholder: 'Placeholder', hint: 'Hint', }); const dataSet = computed<BentoDatagridData>(() => { let filteredData = args.data; // Apply global search term filter if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); const searchableFields = args.columns.map(column => column.field); filteredData = filteredData.filter((item: BentoDatagridDataItem) => { return searchableFields.some(field => { const value = String(item[field]); return value.toLowerCase().includes(lowerCaseSearchTerm); }); }); } filteredData = filteredData.filter(item => filterValues.value?.every(filter => { return filter.value ? String(item[filter.field]).toLowerCase().includes(String(filter.value).toLowerCase()) : true; }) ); return filteredData; }); watch(filterValues, () => { action('filter-values:update')(filterValues.value); }); const onSearchCleared = action('search-clear'); const onSearchInput = action('search-input'); const onSearchTermUpdated = action('update:search-term'); return { // Values dataSet, filtersConfig, filterValues, searchTerm, searchConfig, // Methods onSearchCleared, onSearchInput, onSearchTermUpdated, // Vue 3 storybook fix args, }; }, template: ` <bento-data-grid v-bind="args" :data="dataSet" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" :filter-search-config="searchConfig" @filter-search-clear="onSearchCleared" @filter-search-input="onSearchInput" @update:filter-search-term="onSearchTermUpdated" /> `, }), args: DEFAULT_ARGS, parameters: storybookDocsParameter(DataGridFilteringExample), }; // No Results const NoResultsTemplateCode = ` <bento-data-grid :data="[]" :columns="[ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]" :empty-state="{ title: 'No results were found', image: 'no-results-found', variant: 'embedded', action: { title: 'Clear filters', event: clearFiltersFunction, }, description: 'Try a different term or reset search filters', }" /> `; export const NoResults: Story = { ...Filtering, args: { ...DEFAULT_ARGS, data: [], filters: FILTERS, filterValues: [{ field: 'company', value: 'CIA' }], }, parameters: storybookDocsParameter(NoResultsTemplateCode), }; // Pagination const PaginationTemplateCode = ` <template> <bento-data-grid :data="dataGridData" :columns="columnsConfig" :pagination.sync="pagination" @update:pagination="onUpdatePagination" /> </template> <script lang="ts" setup> const data="[{ id: '1', name: 'Jane' }, { id: '1', name: 'John' }]" const pagination = ref({ description: 'datagrid navigation', hasNext: true, size: 10 }); // With API call watch( () => pagination.value, () => { makeAPICall(pagination.value); } ); // Without API call const onUpdatePagination = (newPagination: BentoDatagridPaginationProps) => { pagination.value = { ...newPagination }; const newSetStartIndex = pagination.value.page * pagination.value.size; const amountOfItems = newSetStartIndex + pagination.value.size - 1; data.value = arrayWithFullSetOfValues.slice(newSetStartIndex, amountOfItems); }; </script> `; export const Pagination: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="dataGridData" :pagination.sync="pagination" @update:pagination="updatePagination" @items-page="updateItemsPerPage" /> `, setup(props) { const args = isVue2 ? props : _args; const largeData = [ ...DEFAULT_ARGS.data, ...DEFAULT_ARGS.data.map(dataItem => ({ ...dataItem, id: `${dataItem.id}+1` })), ]; const pagination = ref(args.pagination); const dataGridData = computed(() => { const end = pagination.value.size <= largeData.length ? pagination.value.size : largeData.length; return largeData.slice(pagination.value.page - 1, end); }); const updateItemsPerPage = (itemsPerPage: number) => { action('items-page')(itemsPerPage); }; const updatePagination = (newPagination: BentoDatagridPaginationProps) => { action('navigate')(newPagination.page); action('update:pagination')(newPagination); }; return { // Values dataGridData, pagination, // Methods updateItemsPerPage, updatePagination, // Vue 3 storybook fixes args: isVue2 ? props : _args, }; }, }), args: { ...DEFAULT_ARGS, pagination: { hasNext: true, description: 'datagrid navigation', size: 10, page: 1, totalCount: DEFAULT_ARGS.data.length * 2, }, }, parameters: storybookDocsParameter(PaginationTemplateCode), }; // Payment Status (demo use case for datagrid V2) export const EverythingBagelExample: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoStatus, BentoCurrency, BentoDate, BentoCountry, BentoButton }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="filteredAndSortedData" :columns.sync="columns" :condensed="isCondensed" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" :sort-by="sortBy" @update:sort-by="sortBy = $event" @update:condensed="isCondensed = $event" :selection.sync="selection" :is-data-item-disabled="isDataItemDisabledForSelection" :row-actions="rowActions" selectable @row-click="onRowClick" > <template #item-paymentStatus="{item}"> <div style="display: flex"> <bento-status :variant="getPaymentStatus(item.paymentStatus)" /> <span>{{ getPaymentText(item.paymentStatus) }}</span> </div> </template> <template #item-reference="{item}"> <bento-link isNotRouting to="https://www.adyen.com" :title="item.reference" > {{ item.reference }} </bento-link> </template> <template #item-countryCode="{item}"> <bento-country :code="item.countryCode" /> </template> <template #item-payment="{item}"> <bento-currency currency="EUR" :value="item.payment" /> </template> <template #item-date="{item}"> <bento-date :date="new Date(item.registered)" :format='BentoDateFormat.FULL_DATE_TIME_WITH_PERIOD'/> </template> </bento-data-grid> `, setup(props) { const args = isVue2 ? props : _args; const columns = ref<Array<BentoColumn>>(args.columns); const filtersConfig = ref<BentoFilterBarModel>(args.filters); const filterValues = ref<BentoFilterValues>(args?.filterValues); const sortBy = ref<Array<BentoDatagridSortByColumn>>([]); const searchTerm = ref(''); const isCondensed = ref(args.condensed); const getPaymentStatus = (paymentStatus: number) => { switch (paymentStatus) { case 2: return BentoStatusVariant.GREEN; case 1: return BentoStatusVariant.YELLOW; case 0: default: return BentoStatusVariant.RED; } }; const getPaymentText = (paymentStatus: number) => { switch (paymentStatus) { case 2: return 'Paid'; case 1: return 'In progress'; case 0: default: return 'Not paid'; } }; const onRowClick = (item: BentoDatagridDataItem) => { action('row-click')(item); }; // Selection const isDataItemDisabledForSelection = (row: BentoDatagridDataItem) => row.id === DEFAULT_DATA[3].id; const selection = ref<BentoDatagridSelection>([DEFAULT_DATA[1].id]); // Filtering const filteredData = computed(() => // If any filters or search terms have been set filterValues.value?.length === 0 && searchTerm.value === '' ? args.data : args.data.filter(item => { const foundFilteredItem = filterValues.value?.length > 0 ? filterValues.value.every(filter => filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true ) : true; const foundSearchFilteredItem = searchTerm.value ? (item?.company as string) .toLowerCase() .includes((searchTerm.value as string).toLowerCase()) : true; return foundFilteredItem && foundSearchFilteredItem; }) ); const filteredAndSortedData = computed(() => { if (sortBy.value?.length === 0) { return filteredData.value; } return sortBy.value[0].direction === BentoDatagridSortDirection.ASCENDING ? filteredData.value.sort((a, b) => (a[sortBy.value[0].field] as string).localeCompare(b[sortBy.value[0].field] as string) ) : filteredData.value .sort((a, b) => (a[sortBy.value[0].field] as string).localeCompare(b[sortBy.value[0].field] as string) ) .reverse(); }); const rowActions: BentoDataGridRowActionsProp = (item: BentoDatagridDataItem) => { if (item.id === DEFAULT_DATA[3].id) { return [ { variant: 'edit', event: action('edit'), tooltipText: 'Edit payment', }, ]; } else if (item.id === DEFAULT_DATA[2].id) { return [ { variant: 'edit', event: action('edit'), tooltipText: 'Edit payment' }, { variant: 'open', isShort: true, menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ]; } else if (item.id === DEFAULT_DATA[1].id) { return []; } return [ { variant: 'edit', event: action('edit'), tooltipText: 'Edit payment', }, { title: 'Delete', critical: true, event: action('Delete'), tooltipText: 'Delete payment', }, ]; }; return { // Methods getPaymentStatus, getPaymentText, // Functions onRowClick, isDataItemDisabledForSelection, // Values columns, filteredAndSortedData, sortBy, filtersConfig, filterValues, searchTerm, isCondensed, selection, rowActions, // Enums BentoDateFormat, // Vue 3 storybook fix args, }; }, }), args: { ...DEFAULT_ARGS, zebraRows: true, filters: [ { field: 'company', label: 'Company', type: BentoFilterItemType.INPUT, }, { field: 'countryCode', label: 'Country/Region', options: { listboxItems: [ { value: BentoCountryCode.NL, }, { value: BentoCountryCode.BE, }, ], variant: 'name', }, type: BentoFilterItemType.SELECT_COUNTRY, }, ], columns: [ { field: 'company', label: 'Company', sortable: true, mandatory: true, autoWidth: true, frozen: true, minWidth: 200, }, { field: 'paymentStatus', label: 'Status', width: 180, infoIconTooltip: 'For current day' }, { field: 'reference', label: 'Reference', width: 320, overflow: BentoColumnOverflow.ELLIPSIS }, { field: 'countryCode', label: 'Country/Region', width: 320 }, { field: 'payment', label: 'Payment', width: 180, numeric: true }, { field: 'date', label: 'Execution Date', width: 200 }, ], allowRowClicks: true, allowColumnDragAndDrop: true, columnPanel: true, highlightedRows: [DEFAULT_DATA[0].id, DEFAULT_DATA[4].id], }, parameters: storybookDocsParameter(DataGridEverythingBagel), }; export const DisplayOptions = (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoDate }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :columns.sync="columns" :condensed.sync="isCondensed"> <template #item-date="{item}"> <bento-date :date="new Date(item.registered)" :format='BentoDateFormat.FULL_DATE_TIME_WITH_PERIOD'/> </template> </bento-data-grid> `, setup(props) { const args = isVue2 ? props : _args; const isCondensed = ref(args.condensed); const columns = ref([ { field: 'name', label: 'Name', minWidth: 200, mandatory: true }, { field: 'reference', label: 'Reference', width: 320, overflow: BentoColumnOverflow.ELLIPSIS, frozen: true, }, { field: 'company', label: 'Company', minWidth: 150, width: 250, frozen: true }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, { field: 'countryCode', label: 'Country/Region', width: 280 }, { field: 'date', label: 'Execution Date', width: 300 }, ]); return { // Values columns, isCondensed, // Enums BentoDateFormat, // Vue 3 storybook fix args, }; }, }); DisplayOptions.args = { ...DEFAULT_ARGS, allowColumnDragAndDrop: true, columnPanel: true, }; DisplayOptions.parameters = storybookDocsParameter(` <bento-data-grid column-panel :data="dataArray" :columns.sync="columns" :condensed.sync="isCondensed" />`); export const GridInCard: Story = { render: (_args, { argTypes }) => ({ components: { BentoCard, BentoDataGrid, BentoDate }, props: Object.keys(argTypes), template: ` <div style="max-width: 500px;"> <bento-card> Grid in a card <template #content> <div style="height: 300px;"> <bento-data-grid v-bind="args" :columns.sync="columns" :condensed.sync="isCondensed" :pagination="pagination" fit-content > <template #item-date="{item}"> <bento-date :date="new Date(item.registered)" :format='BentoDateFormat.FULL_DATE_TIME_WITH_PERIOD'/> </template> </bento-data-grid> </div> </template> </bento-card> </div> `, setup(props) { const args = isVue2 ? props : _args; const isCondensed = ref(args.condensed); const pagination = ref({ hasNext: true, size: 20, page: 1, description: 'datagrid navigation', }); const columns = ref([ { field: 'name', label: 'Name', minWidth: 200, mandatory: true }, { field: 'reference', label: 'Reference', width: 320, overflow: BentoColumnOverflow.ELLIPSIS, }, { field: 'company', label: 'Company', minWidth: 150, width: 250 }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, { field: 'countryCode', label: 'Country/Region', width: 280 }, { field: 'date', label: 'Execution Date', width: 300 }, ]); return { // Values columns, isCondensed, pagination, // Enums BentoDateFormat, // Vue 3 storybook fix args, }; }, }), args: { ...DEFAULT_ARGS, columnPanel: false, }, parameters: storybookDocsParameter(DataGridFitContentInCardExample), }; // Default const lazyLoadingCode = ` // With infinite scroll <bento-data-grid :data="dataArray" :columns="[ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]" lazy-load-type="automatic" :loading="yourLoadingLogic" @update:load-more="yourInfiniteScrollHandler" /> // With 'load more' button <bento-data-grid :data="dataArray" :columns="[ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]" lazy-load-type="button" :loading="yourLoadingLogic" @update:load-more="yourInfiniteScrollHandler" />`; export const lazyLoading: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <div style="height: 300px;"> <bento-data-grid v-bind="args" :lazy-load-type="lazyLoadType" :loading="loading" @update:show-more="infiniteScrollHandler"/> </div> `, setup(props) { const args = isVue2 ? props : _args; const loading = ref(false); const data = ref(DEFAULT_DATA); const infiniteScrollHandler = () => { loading.value = true; setTimeout(() => { data.value.push(...ADDITIONAL_DATA); loading.value = false; }, 2000); }; return { args, loading, infiniteScrollHandler, }; }, }), args: { ...DEFAULT_ARGS, lazyLoadType: 'automatic', }, parameters: storybookDocsParameter(lazyLoadingCode), }; // Added just to be able to easily play around with the sticky headers/pagination/filters // Not really relevant as a Storybook example, so not adding storybook code example export const LongDataWithStickyFiltersAndPagination: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const filtersConfig = ref<BentoFilterBarModel>([ { field: 'col1', label: 'Column 1', type: BentoFilterItemType.INPUT, }, { field: 'col2', label: 'Column 2', type: BentoFilterItemType.INPUT, }, ]); const filterValues = ref(); const searchTerm = ref(''); const isCondensed = ref(args.condensed); const columns = ref( Array.from({ length: 20 }, (_, index) => ({ field: `col${index + 1}`, label: `Column ${index + 1}`, width: 200, mandatory: index === 0, })) ); const data = ref( Array.from({ length: 50 }, (_, rowIndex) => columns.value.reduce((acc, col, colIndex) => { acc[col.field] = `Row ${rowIndex + 1} - Col ${colIndex + 1}`; return acc; }, {}) ) ); const dataSet = computed(() => // If any filters or search terms have been set filterValues.value?.length === 0 && searchTerm.value === '' ? data.value : data.value.filter(item => { const foundFilteredItem = filterValues.value?.length > 0 ? filterValues.value.every(filter => filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true ) : true; const foundSearchFilteredItem = searchTerm.value ? Object.keys(item).some(key => key ? (item[key] as string) .toLowerCase() .includes((searchTerm.value as string).toLowerCase()) : true ) : true; return foundFilteredItem && foundSearchFilteredItem; }) ); const onSearchCleared = action('search-clear'); const onSearchInput = action('search-input'); const onSearchTermUpdated = action('update:search-term'); return { // Values dataSet, filtersConfig, filterValues, searchTerm, columns, isCondensed, // Methods onSearchCleared, onSearchInput, onSearchTermUpdated, // Vue 3 storybook fix args, }; }, template: ` <bento-data-grid v-bind="args" :data="dataSet" :columns.sync="columns" :condensed.sync="isCondensed" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" @filter-search-clear="onSearchCleared" @filter-search-input="onSearchInput" @update:filter-search-term="onSearchTermUpdated" /> `, }), args: { ...DEFAULT_ARGS, columnPanel: true, stickyPagination: true, stickyFilters: true }, }; // Cell actions export const CellActions: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :columns="columns"> <template #item-custom="{item}"> Value </template> </bento-data-grid> `, setup(props) { // --- Action Handlers --- const editItem: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('edit')(rowDataItem, columnKey); }; const cancelAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('cancel')(rowDataItem, columnKey); }; const transferAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('transfer')(rowDataItem, columnKey); }; const deleteAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('delete')(rowDataItem, columnKey); }; // --- Dynamic Cell Actions Function --- // This function generates actions based on the row's data. const getNameCellActions: BentoColumnCellActionsProp = rowDataItem => { const actions: Array<BentoColumnCellAction> = []; // Conditionally add the 'edit' action only for the first row. if (rowDataItem.id === DEFAULT_DATA[3].id) { actions.push({ variant: 'edit', event: editItem, tooltipText: 'Edit this name' }); } // Add a 'more' menu with dynamic options. actions.push({ variant: 'more', isShort: true, tooltipText: 'Additional actions', menuData: [ { text: 'Cancel payment', event: cancelAction, critical: true, }, { text: 'Transfer payment', event: transferAction, }, { text: 'Delete payment', event: deleteAction, // Example: disable delete for the second row. disabled: rowDataItem.id === DEFAULT_DATA[2].id, }, ], }); return actions; }; // --- Column Definitions --- const columns: Array<BentoColumn> = [ { field: 'name', label: 'Name (Dynamic Actions)', minWidth: 300, mandatory: true, // Use a function for dynamic cell actions cellActions: getNameCellActions, }, { field: 'company', label: 'Reference', autoWidth: true }, { field: 'payment', label: 'Payment (Static Actions)', minWidth: 350, // Use a static array for cell actions cellActions: [ { variant: 'copy', getCopyData: (rowDataItem: BentoDatagridDataItem, columnKey: string) => `${rowDataItem[columnKey]}`, }, { variant: 'open', event: action('open-payment'), tooltipText: 'Open payment details' }, ], }, ]; return { args: isVue2 ? props : _args, columns }; }, }), args: { ...DEFAULT_ARGS, highlightedRows: [DEFAULT_DATA[3].id], columns: [ { field: 'name', label: 'Name (Dynamic Actions)', minWidth: 300, mandatory: true, cellActions: [ () => (rowDataItem: BentoDatagridDataItem) => ({ variant: 'edit', disabled: rowDataItem.id === DEFAULT_DATA[3].id, event: action('edit'), tooltipText: 'Edit this name', }), { variant: 'open', tooltipText: 'Additional actions', menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ], }, { field: 'company', label: 'Reference', autoWidth: true }, { field: 'payment', label: 'Payment (Static Actions)', minWidth: 350, cellActions: [ { variant: 'copy', getCopyData: (rowDataItem: BentoDatagridDataItem, columnKey: string) => `$${rowDataItem[columnKey]}`, }, { variant: 'more', tooltipText: 'Additional actions', menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], isShort: true, }, ], }, { field: 'custom', label: 'Custom template', minWidth: 200, cellActions: [{ variant: 'edit', event: action('edit'), tooltipText: 'Edit custom' }], }, ], }, parameters: storybookDocsParameter(DataGridCellActionsExample), }; // NestContentSlot const NestedContentSlotTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" has-nested-content :should-show-nested-content="filterRowsThatShowNestedContent" > <template #nested-slot="{item}"> <bento-typography>Nested content for {{ item.company }}</bento-typography> </template> </bento-data-grid> </template> <script setup lang="ts"> import { BentoDataGrid, type BentoColumn, type BentoDatagridData, type BentoDatagridShouldShowNestedContent } from '@adyen/bento-vue2' const columns: BentoColumn = [ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'payment', label: 'Payment', minWidth: 300, mandatory: true }, ] const data = ref<BentoDatagridData>([ { id: 1, name: 'First', payment: 20.9 }, { id: 2, name: 'Second', payment: 10.0 }, ]); const filterRowsThatShowNestedContent: BentoDatagridShouldShowNestedContent = (item) => item.id === 2; </script> `; export const NestedContentSlot: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" > <template #nested-slot="{item}"> <div style="height: 80px; text-align: center; display: flex; align-items: center;"> <bento-typography>Nested content slot for {{ item.company }}</bento-typography> </div> </template> </bento-data-grid> `, setup(props) { return { args: isVue2 ? props : _args }; }, }), args: { ...DEFAULT_ARGS, hasNestedContent: true, zebraRows: true, shouldShowNestedContent: (item: BentoDatagridDataItem) => item.id !== DEFAULT_ARGS.data[1].id, }, parameters: storybookDocsParameter(NestedContentSlotTemplateCode), }; export const StickyHorizontalScrollbar: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoButton }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const filtersConfig = ref<BentoFilterBarModel>([ { field: 'col1', label: 'Column 1', type: BentoFilterItemType.INPUT, }, { field: 'col2', label: 'Column 2', type: BentoFilterItemType.INPUT, }, ]); const filterValues = ref(); const searchTerm = ref(''); const isCondensed = ref(args.condensed); const columns = ref( Array.from({ length: 20 }, (_, index) => ({ field: `col${index + 1}`, label: `Column ${index + 1}`, width: 200, mandatory: index === 0, })) ); const data = ref( Array.from({ length: 50 }, (_, rowIndex) => columns.value.reduce((acc, col, colIndex) => { acc[col.field] = `Row ${rowIndex + 1} - Col ${colIndex + 1}`; return acc; }, {}) ) ); const dataSet = computed(() => // If any filters or search terms have been set filterValues.value?.length === 0 && searchTerm.value === '' ? data.value : data.value.filter(item => { const foundFilteredItem = filterValues.value?.length > 0 ? filterValues.value.every(filter => filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true ) : true; const foundSearchFilteredItem = searchTerm.value ? Object.keys(item).some(key => key ? (item[key] as string) .toLowerCase() .includes((searchTerm.value as string).toLowerCase()) : true ) : true; return foundFilteredItem && foundSearchFilteredItem; }) ); const onSearchCleared = action('search-clear'); const onSearchInput = action('search-input'); const onSearchTermUpdated = action('update:search-term'); return { // Values dataSet, filtersConfig, filterValues, searchTerm, columns, isCondensed, // Methods onSearchCleared, onSearchInput, onSearchTermUpdated, // Vue 3 storybook fix args, }; }, template: ` <bento-data-grid v-bind="args" :data="dataSet" :columns.sync="columns" :condensed.sync="isCondensed" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" @filter-search-clear="onSearchCleared" @filter-search-input="onSearchInput" @update:filter-search-term="onSearchTermUpdated" > <template #pagination-slot> <bento-button variant="tertiary"> Pagination slot button </bento-button> </template> </bento-data-grid> `, }), args: { ...DEFAULT_ARGS, pagination: { page: 1, hidePageSize: true }, columnPanel: true, stickyPagination: true, stickyFilters: true, stickyHorizontalScrollbar: true, }, }; const NestedRowsTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" has-nested-rows :should-show-nested-row="filterRowsThatShowNestedContent" @nested-content-toggled="onParentRowToggle" /> </template> <script setup lang="ts"> import { BentoDataGrid, type BentoColumn, type BentoDatagridData, type BentoDatagridShouldShowNestedRowsFn } from '@adyen/bento-vue2' const columns: BentoColumn = [ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'payment', label: 'Payment', minWidth: 300, mandatory: true }, ] const data = ref<BentoDatagridData>([ { id: 1, name: 'First', payment: 20.9 }, { id: 2, name: 'Second', payment: 10.0 }, ]); const filterRowsThatShowNestedRows: BentoDatagridShouldShowNestedRowsFn = (item) => item.id === 2; const onParentRowToggle = async (item: BentoDatagridDataItem) => { const childrenForToggledParentItem = await makeAPICallToGetChildren(item.id); dataState.value = dataState.value.map(itemToChange => { if (itemToChange.id === item.id) { return { ...itemToChange, children: childrenForToggledParentItem } } return itemToChange; }); }; </script> `; export const NestedRows: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="dataState" @nested-content-toggled="onParentRowToggle" /> `, setup(props) { const dataState = ref<Array<BentoDatagridDataItem>>(DEFAULT_ARGS.data); const selection = ref<BentoDatagridDataItemId>(); const onParentRowToggle = (item: BentoDatagridDataItem) => { setTimeout(() => { const children = [ { id: `03c0a799-1ced-4017-aea3-111111${item.id}`, name: `Child of ${item.name} #1`, payment: 160405, company: 'Merchant Helium', registered: '2023-06-30T13:56:09.498Z', tags: ['deserunt', 'et', 'esse'], reference: 'UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn', paymentStatus: 1, countryCode: BentoCountryCode.NL, parentId: DEFAULT_ARGS.data[1].id, }, { id: `03c0a799-1ced-4017-aea3-1111113${item.id}`, name: `Child of ${item.name} #2`, payment: 163500, company: 'Merchant B', registered: '2023-04-28T13:11:09.498Z', tags: ['et', 'Lorem', 'sint'], reference: 'IxUZ9FsEPCZ9dFFPpI9C', paymentStatus: 2, countryCode: BentoCountryCode.BE, parentId: DEFAULT_ARGS.data[1].id, }, { id: `03c0a799-1ced-4017-aea3-1111114${item.id}`, name: `Child of ${item.name} #3`, payment: 78500, company: 'Merchant C', registered: '2023-06-30T11:01:09.498Z', tags: ['laborum', 'enim', 'ullamco'], reference: 'y0qAQ0WDszRyqLY2WO9f', paymentStatus: 1, countryCode: BentoCountryCode.UA, parentId: DEFAULT_ARGS.data[1].id, }, ]; dataState.value = dataState.value.map(itemToChange => { if (itemToChange.id === item.id) { return { ...itemToChange, children, }; } return itemToChange; }); }, 1000); }; return { args: isVue2 ? props : _args, dataState, selection, onParentRowToggle }; }, }), args: { ...DEFAULT_ARGS, hasNestedRows: true, zebraRows: true, shouldShowNestedRows: dataItem => { return dataItem.id !== DEFAULT_ARGS.data[1].id; }, }, parameters: storybookDocsParameter(NestedRowsTemplateCode), }; export const RowActions: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :row-actions="rowActions" /> `, setup(props) { const rowActions: BentoDataGridRowActionsProp = (item: BentoDatagridDataItem) => { if (item.id === DEFAULT_DATA[0].id) { return [ { variant: 'edit', event: action('edit'), tooltipText: 'Edit row' }, { title: 'Delete', critical: true, event: action('Delete'), tooltipText: 'Delete row', }, ]; } else if (item.id === DEFAULT_DATA[1].id) { return []; } else if (item.id === DEFAULT_DATA[2].id) { return [ { variant: 'edit', event: action('edit'), tooltipText: 'Edit row' }, { variant: 'open', isShort: true, tooltipText: 'More options', menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ]; } return [ { variant: 'edit', event: action('edit'), tooltipText: 'Edit row', }, ]; }; return { args: isVue2 ? props : _args, rowActions, }; }, }), args: { ...DEFAULT_ARGS, }, parameters: storybookDocsParameter(DataGridRowActionsExample), }; const SummaryCellTemplateCode = ` <template> <bento-data-grid :columns="columns" :data="dataSet" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term="searchTerm" :filter-search-config="searchConfig" @update:filter-values="filterValues = $event" @update:filter-search-term="searchTerm = $event" @filter-search-clear="onSearchCleared" > <template #item-company="{item}"> <bento-data-grid-summary-cell :label="\`\${item.company.length} merchants\`" :items="item.company" :search-term="companyColumnSearchTerm" /> </template> </bento-data-grid> </template> <script setup lang="ts"> import { BentoColumn, BentoDataGrid, BentoDataGridSummaryCell, BentoDatagridData, BentoFilterBarModel, BentoFilterBarSearchBarProps, BentoFilterItemType, BentoFilterValues, BentoCountryCode, } from '@adyen/bento-vue2'; import { computed, ref } from 'vue'; const columns: Array<BentoColumn> = [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ]; /** * Data set with array of companies */ const multiData: BentoDatagridData = [ { id: '03c0a799-1ced-4017-aea3-0cebb0106335', name: 'Shepherd Kane', payment: 160405, company: ['Merchant Helium', 'Merchant A'], }, { id: '8f9e7877-1ca2-4929-9217-f1ec78709291', name: 'Joyce Johns', payment: 78500, company: ['Merchant C', 'Merchant Hedra'], }, { id: '51d2b31d-d577-4fca-8329-a583babbbdda', name: 'Evans Zamora', payment: 1153.4, company: [ 'Merchant A', 'Merchant B', 'Merchant C', 'Merchant D', 'Merchant E', 'Merchant F', 'Merchant G', 'Merchant H', 'Merchant I', 'Merchant J', ], }, ]; const filtersConfig = computed<BentoFilterBarModel>(() => [ { field: 'name', label: 'Name', type: BentoFilterItemType.INPUT, }, { field: 'company', label: 'Company', type: BentoFilterItemType.INPUT, }, ]); /** * The values of applied filters. */ const filterValues = ref<BentoFilterValues>(); /** * The current global search term that is being searched with */ const searchTerm = ref(''); /** * Global search configuration */ const searchConfig = ref<BentoFilterBarSearchBarProps>({ inputFieldAriaLabel: 'Descriptive details about the search bar', debounceTime: 1000, // in ms placeholder: 'Find merchants', }); /** * Concatenated search term using the global search and the company filter value */ const companyColumnSearchTerm = computed( () => searchTerm.value || filterValues.value?.find(filter => filter.field === 'company')?.value?.toString() || '' ); /** * Simplified computed data set that filters data depending on applied filters and global search values */ const dataSet = computed<BentoDatagridData>(() => { let filteredData = multiData; // Apply global search term filter if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); filteredData = filteredData.filter( item => (item.name as string).toLowerCase().includes(lowerCaseSearchTerm) || (item.company as string[]).some(company => company.toLowerCase().includes(lowerCaseSearchTerm) ) ); } // Apply filterValues filter if (filterValues.value?.length) { filteredData = filteredData.filter(item => filterValues.value?.every(filter => { if (filter.field === 'company') { return filter.value ? (item[filter.field] as string[]).some(company => (company as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) ) : true; } return filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true; }) ); } return filteredData; }); const onSearchCleared = () => { searchTerm.value = ''; }; </script> `; export const SummaryCell: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, BentoDashedUnderline, BentoDataGridSummaryCell, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="dataSet" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term="searchTerm" :filter-search-config="searchConfig" @update:filter-values="filterValues = $event" @update:filter-search-term="searchTerm = $event" @filter-search-clear="onSearchCleared" > <template #item-company="{item}"> <bento-data-grid-summary-cell :label="\`\${item.company.length} merchants\`" :items="item.company" :search-term="companyColumnSearchTerm" /> </template> </bento-data-grid> `, setup(props) { /** * Data set with array of companies */ const multiData: BentoDatagridData = [ { id: '03c0a799-1ced-4017-aea3-0cebb0106335', name: 'Shepherd Kane', payment: 160405, company: ['Merchant Helium', 'Merchant A'], }, { id: 'b6d4f1fd-b1f6-40a6-a443-f785c89604ef', name: 'Santos Palmer', payment: 163500, company: ['Merchant B'], }, { id: '8f9e7877-1ca2-4929-9217-f1ec78709291', name: 'Joyce Johns', payment: 78500, company: ['Merchant C', 'Merchant Hedra'], }, { id: '0789a012-85a6-4657-87bb-319141938a61', name: 'Lynnette Melendez', payment: -45000, company: ['Merchant A', 'Merchant B', 'Merchant C'], }, { id: '0b3981b6-c73b-47ad-81d1-b06f9b95d467', name: 'Wagner Mcintosh', payment: 1112, company: ['Merchant A'], }, { id: '51d2b31d-d577-4fca-8329-a583babbbdda', name: 'Evans Zamora', payment: 1153.4, company: [ 'Merchant A', 'Merchant B', 'Merchant C', 'Merchant D', 'Merchant E', 'Merchant F', 'Merchant G', 'Merchant H', 'Merchant I', 'Merchant J', 'Merchant K', 'Merchant L', 'Merchant M', 'Merchant N', 'Merchant O', 'Merchant P', 'Merchant Q', 'Merchant R', 'Merchant S', 'Merchant T', ], }, { id: '9bd59a6e-bd03-4477-813f-b844953b7645', name: 'Anderson Middleton', payment: 181722.23, company: ['Merchant A'], }, ]; const filtersConfig = computed<BentoFilterBarModel>(() => [ { field: 'name', label: 'Name', type: BentoFilterItemType.INPUT, }, { field: 'company', label: 'Company', type: BentoFilterItemType.INPUT, }, ]); /** * The values of applied filters. */ const filterValues = ref<BentoFilterValues>(); /** * The current global search term that is being searched with */ const searchTerm = ref(''); /** * Global search configuration */ const searchConfig = ref<BentoFilterBarSearchBarProps>({ inputFieldAriaLabel: 'Descriptive details about the search bar', debounceTime: 1000, // in ms placeholder: 'Find merchants', }); /** * Concatenated search term using the global search and the company filter value */ const companyColumnSearchTerm = computed( () => searchTerm.value || filterValues.value?.find(filter => filter.field === 'company')?.value?.toString() || '' ); /** * Simplified computed data set that filters data depending on applied filters and global search values */ const dataSet = computed<BentoDatagridData>(() => { let filteredData = multiData; // Apply global search term filter if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); filteredData = filteredData.filter( item => (item.name as string).toLowerCase().includes(lowerCaseSearchTerm) || (item.company as Array<string>).some(company => company.toLowerCase().includes(lowerCaseSearchTerm) ) ); } // Apply filterValues filter if (filterValues.value?.length) { filteredData = filteredData.filter(item => filterValues.value?.every(filter => { if (filter.field === 'company') { return filter.value ? (item[filter.field] as Array<string>).some(company => (company as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) ) : true; } return filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true; }) ); } return filteredData; }); const onSearchCleared = () => { searchTerm.value = ''; }; return { args: isVue2 ? props : _args, filtersConfig, filterValues, searchTerm, searchConfig, onSearchCleared, dataSet, companyColumnSearchTerm, }; }, }), args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ], zebraRows: true, }, parameters: storybookDocsParameter(SummaryCellTemplateCode), }; export const TrendCell: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoDataGridTrendCell, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args"> <template #item-performance="{ item }"> <bento-data-grid-trend-cell :value="item.performance.value" :direction="item.performance.direction" :variant="item.performance.variant" /> </template> </bento-data-grid> `, setup(props) { const columns: Array<BentoColumn> = [ { field: 'name', label: 'Name', flex: 2 }, { field: 'performance', label: 'Performance', flex: 1, numeric: true }, ]; const data: BentoDatagridData = [ { id: '1', name: 'Shepherd Kane', performance: { value: 25.5, variant: 'green', direction: 'up', }, }, { id: '2', name: 'Santos Palmer', performance: { value: 10.2, variant: 'red', direction: 'down', }, }, { id: '3', name: 'Joyce Johns', performance: { value: 0, variant: 'black', direction: 'none', }, }, { id: '4', name: 'Lynnette Melendez', performance: { value: '3.1428571429%', variant: 'green', direction: 'down', }, }, { id: '5', name: 'Wagner Mcintosh', performance: { value: '15.2%', variant: 'red', direction: 'up', }, }, { id: '6', name: 'Evans Zamora', performance: { value: '1.23', direction: 'up', }, }, { id: '7', name: 'Anderson Middleton', performance: { value: '90', direction: 'down', }, }, ]; return { args: { ...(isVue2 ? props : _args), columns, data }, }; }, }), args: { ...DEFAULT_ARGS, zebraRows: true, }, parameters: storybookDocsParameter(DataGridTrendCellExample), }; export const TagWithTextCell: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoDataGridTagWithTextCell, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args"> <template #item-endpoint="{ item }"> <bento-data-grid-tag-with-text-cell :tag="item.endpoint.tag" :value="item.endpoint.value" /> </template> </bento-data-grid> `, setup(props) { const columns = [ { field: 'name', label: 'Name', flex: 2 }, { field: 'endpoint', label: 'Endpoint', flex: 2 }, ]; const data = [ { id: '1', name: 'Get users', endpoint: { tag: { label: 'GET', variant: 'blue' }, value: '/api/users', }, }, { id: '2', name: 'Create user', endpoint: { tag: { label: 'POST', variant: 'green' }, value: '/api/users', }, }, { id: '3', name: 'Update user', endpoint: { tag: { label: 'PUT', variant: 'yellow' }, value: '/api/users/:id', }, }, { id: '4', name: 'Delete user', endpoint: { tag: { label: 'DELETE', variant: 'red' }, value: '/api/users/:id', }, }, ]; return { args: { ...(isVue2 ? props : _args), columns, data }, }; }, }), args: { ...DEFAULT_ARGS, }, parameters: storybookDocsParameter(DataGridTagWithTextCellExample), };
@@ -1 +1 @@
1
- <template> <div :class="$attrs.class" data-testid="textbox-wrapper" @click="onToggleDropdown" @keydown.escape="onCloseDropdown" > <bento-typography class="b-dropdown-base-textbox" :class="conditionalClasses" :variant="isUnderlineStyle ? 'title' : 'body'" el="div" data-testid="base-textbox" > <input v-if="shouldShowInput" v-bind="ariaAttributes" :id="textboxId" ref="textbox" class="b-dropdown-base-textbox__input" role="combobox" type="text" aria-autocomplete="list" size="1" :value="open ? value : displayValue" :disabled="disabled ? true : null" :readonly="readonly" :required="showRequiredAttribute" @keydown="$emit(DropdownBaseTextboxEvent.KEYDOWN, $event)" @keydown.enter="onToggleDropdown" @input="onInputEvent" /> <div v-else :id="textboxId" v-bind="ariaAttributes" ref="textbox" class="b-dropdown-base-textbox__input" :class="comboboxConditionalClasses" :aria-readonly="readonly ? 'true' : null" role="combobox" :tabindex="disabled ? -1 : 0" > <template v-if="shouldShowSingleSelectSlot"> <slot v-if="hasSlot('display-value')" v-bind="selectedValueItem" name="display-value"></slot> <slot v-else v-bind="selectedValueItem"> {{ displayValue }} </slot> </template> <template v-else-if="shouldShowMultipleSlot"> <div class="b-dropdown-base-textbox__input-multiple-selected"> <div v-for="singleItem of selectedValueItems" :key="singleItem.value"> <div class="b-dropdown-base-textbox__input-selected-element"> <slot v-bind="singleItem"> {{ displayValue }} </slot> </div> </div> </div> </template> <template v-else>{{ displayValue }}</template> </div> <span v-if="additionalItemsSelected" class="b-dropdown-base-textbox__additional-items"> {{ additionalItemsSelected }} </span> <span v-if="hasSlot('icon') && !isFiltering" class="b-dropdown-base-textbox__icon" aria-hidden="true"> <slot name="icon"></slot> </span> <span v-if="isFiltering" class="b-dropdown-base-textbox__clear-search" :tabindex="disabled ? -1 : 0" data-testid="clear-search-button" @click.stop="onClearSearch" @keydown.enter.stop="onClearSearch" @keyup.space="onClearSearch" > <cross-circle-fill-small-icon v-if="size === BentoDropdownSize.SMALL" :svg-title="t('clearSearch')" /> <cross-circle-fill-icon v-else :svg-title="t('clearSearch')" /> </span> </bento-typography> </div> </template> <script setup lang="ts"> import { computed, defineComponent, inject, nextTick, type PropType, ref, useAttrs, useSlots, watch } from 'vue'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { useHasSlot } from '@/composables'; import { BentoTypography } from '@/components/typography'; import { type Booleanish } from '@/types/prop-types'; import { type BentoListboxOptionItem } from '@/types/listbox'; import type { HTMLAttributes } from 'vue/types/jsx.d.ts'; import { DropdownBaseTextboxEvent, DropdownBaseTextboxState } from './dropdown-base-textbox.types'; import CrossCircleFillIcon from '@adyen/ui-assets-icons-16/vue/cross-circle-fill'; import CrossCircleFillSmallIcon from '@adyen/ui-assets-icons-16/vue/cross-circle-fill-small'; import { BentoDropdownSize } from '@/components/dropdown/dropdown.types'; import { DROPDOWN_UNDERLINE_STYLE_INJECTION_KEY } from '../../dropdown.keys'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const attrs = useAttrs(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = defineProps({ /** * The number of additional items that have been selected in the dropdown. */ additionalItemsSelected: { type: [Number, String] as PropType<number | string>, default: 0 }, /** * If the combobox DOM element should be an `input` element. */ alwaysComboboxIsInput: { type: Boolean, default: false, }, /** * Sets the aria-controls attribute on the textbox. This should correspond to the popover's id that the textbox triggers. */ ariaControls: { type: String, required: true }, /** * Indicates whether the content the textbox triggers is visible or not. Needs to be set accordingly when the textbox is used and changed every time the popover is shown/hidden. */ ariaExpanded: { type: [String, Boolean] as PropType<Booleanish>, required: true, }, /** * If no label element is used then the aria-label is be set should be set. */ ariaLabel: { type: String, default: null }, /** * Corresponds to the label's id that labels the textbox element. */ ariaLabelledby: { type: String, default: null }, /** * Disables any textbox functionality */ disabled: { type: Boolean, default: false }, /** * Selected option's label */ displayValue: { type: String, default: null }, /** * Sets the possibility to type into the textbox. */ dynamicFiltering: { type: Boolean, default: false }, /** * Marks the textbox as invalid, rendering an error state */ isInvalid: { type: Boolean, default: false }, /* * If enabled, the user will be able to select multiple items. */ multiple: { type: Boolean, default: false }, /** * Indicates if the dropdown is open so that * the respective event can be emitted */ open: { type: Boolean, default: false }, /** * Sets the dropdown to readonly state */ readonly: { type: Boolean, default: false }, /** * The input value */ value: { type: String, default: '' }, /** * The selected value's list box item. */ selectedValueItem: { type: [Object, Array] as PropType<BentoListboxOptionItem | Array<BentoListboxOptionItem>>, default: undefined, }, /** * Toggles whether to use the slot content select items shown in the textbox in multiple mode. */ showSlotContentInMultiple: { type: Boolean, default: false }, /** * Dropdown size */ size: { type: String as PropType<BentoDropdownSize | `${BentoDropdownSize}`>, default: null, }, }); const emit = defineEmits<{ /** * Emitted when the "clear" button is clicked */ (e: DropdownBaseTextboxEvent.CLEAR); /** * Emitted on click, space, enter and esc user events. It will close the dropdown. */ (e: DropdownBaseTextboxEvent.CLOSE); /** * Emitted when `dynamicFiltering` is enabled and a search input is entered */ (e: DropdownBaseTextboxEvent.INPUT, searchValue: string); /** * Emitted when any key is pressed and the focus is on the input field */ (e: DropdownBaseTextboxEvent.KEYDOWN, event: KeyboardEvent); /** * Emitted on click, space and enter user events. It will open the dropdown. */ (e: DropdownBaseTextboxEvent.OPEN); }>(); const textbox = ref(null); const textboxId = generateUid('textbox'); // Inject underline style key to change the dropdown layout const isUnderlineStyle = inject(DROPDOWN_UNDERLINE_STYLE_INJECTION_KEY, false); const conditionalClasses = computed(() => ({ [`b-dropdown-base-textbox--${DropdownBaseTextboxState.DISABLED}`]: props.disabled, [`b-dropdown-base-textbox--${DropdownBaseTextboxState.READONLY}`]: props.readonly, [`b-dropdown-base-textbox--${DropdownBaseTextboxState.HAS_ITEMS}`]: !!props.additionalItemsSelected, 'b-dropdown-base-textbox--underline': isUnderlineStyle, })); const showRequiredAttribute = computed(() => shouldShowInput.value && attrs['aria-required'] ? ('true' as Booleanish) : null ); const comboboxConditionalClasses = computed(() => ({ [`b-dropdown-base-textbox__input--dynamic-filtering`]: props.dynamicFiltering, })); const isFiltering = computed(() => props.dynamicFiltering && !!props.value && props.open); const ariaAttributes = computed( () => ({ 'aria-haspopup': 'listbox', 'aria-controls': props.ariaControls, 'aria-describedby': attrs['aria-describedby']?.toString() as string, 'aria-expanded': props.ariaExpanded ? 'true' : 'false', 'aria-labelledby': props.ariaLabelledby, 'aria-label': props.ariaLabelledby ? null : props.ariaLabel, 'aria-invalid': props.isInvalid, 'aria-disabled': props.disabled ? true : null, 'aria-required': attrs['aria-required'] as Booleanish, }) as HTMLAttributes ); // Cast selectedValueItem if it is an array of selected items const selectedValueItems = computed(() => props.multiple ? (props.selectedValueItem as Array<BentoListboxOptionItem>) : [] ); const shouldShowInput = computed(() => props.dynamicFiltering && (props.open || props.alwaysComboboxIsInput)); const shouldShowSingleSelectSlot = computed(() => props.selectedValueItem && !props.multiple); const shouldShowMultipleSlot = computed( () => props.multiple && props.selectedValueItem && selectedValueItems.value.length > 0 && props.showSlotContentInMultiple ); watch( () => shouldShowInput.value, () => { if (shouldShowInput.value && !props.alwaysComboboxIsInput) { nextTick(() => { textbox.value.focus(); }); } } ); const onOpenDropdown = async () => { if (!props.disabled && !props.readonly) { emit(DropdownBaseTextboxEvent.OPEN); // wait for popover to open before setting focus await nextTick(); textbox.value.focus(); } }; const onCloseDropdown = () => { if (props.open) { textbox.value.focus(); emit(DropdownBaseTextboxEvent.CLOSE); } }; const onToggleDropdown = (event: Event) => { const isDynamicFilteringInputClickEvent = props.open && (event.target as HTMLDivElement).tagName === 'INPUT'; if (isDynamicFilteringInputClickEvent) { /** * When there's a click even on the input during filtering * and the popover is open, do not toggle the popover. * Allow for click events inside the input field */ return null; } return props.open ? onCloseDropdown() : onOpenDropdown(); }; const onClearSearch = () => { emit(DropdownBaseTextboxEvent.CLEAR); emit(DropdownBaseTextboxEvent.INPUT, ''); textbox.value.focus(); }; const onInputEvent = (event: Event) => { if (!props.readonly && !props.disabled) { emit(DropdownBaseTextboxEvent.INPUT, (event.target as HTMLInputElement).value); } else { event.preventDefault(); event.stopPropagation(); } }; const focus = () => { textbox.value.focus(); }; defineExpose({ focus, }); </script> <script lang="ts"> /** * Only for internal use, implements the logic of the textbox element to be implemented in the combobox. */ export default defineComponent({ i18n: { messages }, name: 'dropdown-base-textbox', inheritAttrs: false, }); </script> <style lang="scss" scoped src="./dropdown-base-textbox.scss" />
1
+ <template> <div :class="$attrs.class" data-testid="textbox-wrapper" @click="onToggleDropdown" @keydown.escape="onCloseDropdown" > <bento-typography class="b-dropdown-base-textbox" :class="conditionalClasses" :variant="isUnderlineStyle ? 'title' : 'body'" el="div" data-testid="base-textbox" > <input v-if="shouldShowInput" v-bind="ariaAttributes" :id="textboxId" ref="textbox" class="b-dropdown-base-textbox__input" role="combobox" type="text" aria-autocomplete="list" size="1" :value="open ? value : displayValue" :disabled="disabled ? true : null" :readonly="readonly" :required="showRequiredAttribute" @keydown="$emit(DropdownBaseTextboxEvent.KEYDOWN, $event)" @keydown.enter="onToggleDropdown" @input="onInputEvent" /> <div v-else :id="textboxId" v-bind="ariaAttributes" ref="textbox" class="b-dropdown-base-textbox__input" :class="comboboxConditionalClasses" :aria-readonly="readonly ? 'true' : null" role="combobox" :tabindex="disabled ? -1 : 0" > <template v-if="shouldShowSingleSelectSlot"> <slot v-if="hasSlot('display-value')" v-bind="selectedValueItem" name="display-value"></slot> <slot v-else v-bind="selectedValueItem"> {{ displayValue }} </slot> </template> <template v-else-if="shouldShowMultipleSlot"> <div class="b-dropdown-base-textbox__input-multiple-selected"> <div v-for="singleItem of selectedValueItems" :key="singleItem.value"> <div class="b-dropdown-base-textbox__input-selected-element"> <slot v-bind="singleItem"> {{ displayValue }} </slot> </div> </div> </div> </template> <template v-else>{{ displayValue }}</template> </div> <span v-if="additionalItemsSelected" class="b-dropdown-base-textbox__additional-items"> {{ additionalItemsSelected }} </span> <span v-if="hasSlot('icon') && !isFiltering" class="b-dropdown-base-textbox__icon" aria-hidden="true"> <slot name="icon"></slot> </span> <span v-if="isFiltering" class="b-dropdown-base-textbox__clear-search" :tabindex="disabled ? -1 : 0" data-testid="clear-search-button" @click.stop="onClearSearch" @keydown.enter.stop="onClearSearch" @keyup.space="onClearSearch" > <cross-circle-fill-small-icon v-if="size === BentoDropdownSize.SMALL" :svg-title="t('clearSearch')" /> <cross-circle-fill-icon v-else :svg-title="t('clearSearch')" /> </span> </bento-typography> </div> </template> <script setup lang="ts"> import { computed, defineComponent, inject, nextTick, type PropType, ref, useAttrs, useSlots, watch } from 'vue'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { stopEventPropagation } from '@/utils/ts/events'; import { useHasSlot } from '@/composables'; import { BentoTypography } from '@/components/typography'; import { type Booleanish } from '@/types/prop-types'; import { type BentoListboxOptionItem } from '@/types/listbox'; import type { HTMLAttributes } from 'vue/types/jsx.d.ts'; import { DropdownBaseTextboxEvent, DropdownBaseTextboxState } from './dropdown-base-textbox.types'; import CrossCircleFillIcon from '@adyen/ui-assets-icons-16/vue/cross-circle-fill'; import CrossCircleFillSmallIcon from '@adyen/ui-assets-icons-16/vue/cross-circle-fill-small'; import { BentoDropdownSize } from '@/components/dropdown/dropdown.types'; import { DROPDOWN_UNDERLINE_STYLE_INJECTION_KEY } from '../../dropdown.keys'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const attrs = useAttrs(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = defineProps({ /** * The number of additional items that have been selected in the dropdown. */ additionalItemsSelected: { type: [Number, String] as PropType<number | string>, default: 0 }, /** * If the combobox DOM element should be an `input` element. */ alwaysComboboxIsInput: { type: Boolean, default: false, }, /** * Sets the aria-controls attribute on the textbox. This should correspond to the popover's id that the textbox triggers. */ ariaControls: { type: String, required: true }, /** * Indicates whether the content the textbox triggers is visible or not. Needs to be set accordingly when the textbox is used and changed every time the popover is shown/hidden. */ ariaExpanded: { type: [String, Boolean] as PropType<Booleanish>, required: true, }, /** * If no label element is used then the aria-label is be set should be set. */ ariaLabel: { type: String, default: null }, /** * Corresponds to the label's id that labels the textbox element. */ ariaLabelledby: { type: String, default: null }, /** * Disables any textbox functionality */ disabled: { type: Boolean, default: false }, /** * Selected option's label */ displayValue: { type: String, default: null }, /** * Sets the possibility to type into the textbox. */ dynamicFiltering: { type: Boolean, default: false }, /** * Marks the textbox as invalid, rendering an error state */ isInvalid: { type: Boolean, default: false }, /* * If enabled, the user will be able to select multiple items. */ multiple: { type: Boolean, default: false }, /** * Indicates if the dropdown is open so that * the respective event can be emitted */ open: { type: Boolean, default: false }, /** * Sets the dropdown to readonly state */ readonly: { type: Boolean, default: false }, /** * The input value */ value: { type: String, default: '' }, /** * The selected value's list box item. */ selectedValueItem: { type: [Object, Array] as PropType<BentoListboxOptionItem | Array<BentoListboxOptionItem>>, default: undefined, }, /** * Toggles whether to use the slot content select items shown in the textbox in multiple mode. */ showSlotContentInMultiple: { type: Boolean, default: false }, /** * Dropdown size */ size: { type: String as PropType<BentoDropdownSize | `${BentoDropdownSize}`>, default: null, }, }); const emit = defineEmits<{ /** * Emitted when the "clear" button is clicked */ (e: DropdownBaseTextboxEvent.CLEAR); /** * Emitted on click, space, enter and esc user events. It will close the dropdown. */ (e: DropdownBaseTextboxEvent.CLOSE); /** * Emitted when `dynamicFiltering` is enabled and a search input is entered */ (e: DropdownBaseTextboxEvent.INPUT, searchValue: string); /** * Emitted when any key is pressed and the focus is on the input field */ (e: DropdownBaseTextboxEvent.KEYDOWN, event: KeyboardEvent); /** * Emitted on click, space and enter user events. It will open the dropdown. */ (e: DropdownBaseTextboxEvent.OPEN); }>(); const textbox = ref(null); const textboxId = generateUid('textbox'); // Inject underline style key to change the dropdown layout const isUnderlineStyle = inject(DROPDOWN_UNDERLINE_STYLE_INJECTION_KEY, false); const conditionalClasses = computed(() => ({ [`b-dropdown-base-textbox--${DropdownBaseTextboxState.DISABLED}`]: props.disabled, [`b-dropdown-base-textbox--${DropdownBaseTextboxState.READONLY}`]: props.readonly, [`b-dropdown-base-textbox--${DropdownBaseTextboxState.HAS_ITEMS}`]: !!props.additionalItemsSelected, 'b-dropdown-base-textbox--underline': isUnderlineStyle, })); const showRequiredAttribute = computed(() => shouldShowInput.value && attrs['aria-required'] ? ('true' as Booleanish) : null ); const comboboxConditionalClasses = computed(() => ({ [`b-dropdown-base-textbox__input--dynamic-filtering`]: props.dynamicFiltering, })); const isFiltering = computed(() => props.dynamicFiltering && !!props.value && props.open); const ariaAttributes = computed( () => ({ 'aria-haspopup': 'listbox', 'aria-controls': props.ariaControls, 'aria-describedby': attrs['aria-describedby']?.toString() as string, 'aria-expanded': props.ariaExpanded ? 'true' : 'false', 'aria-labelledby': props.ariaLabelledby, 'aria-label': props.ariaLabelledby ? null : props.ariaLabel, 'aria-invalid': props.isInvalid, 'aria-disabled': props.disabled ? true : null, 'aria-required': attrs['aria-required'] as Booleanish, }) as HTMLAttributes ); // Cast selectedValueItem if it is an array of selected items const selectedValueItems = computed(() => props.multiple ? (props.selectedValueItem as Array<BentoListboxOptionItem>) : [] ); const shouldShowInput = computed(() => props.dynamicFiltering && (props.open || props.alwaysComboboxIsInput)); const shouldShowSingleSelectSlot = computed(() => props.selectedValueItem && !props.multiple); const shouldShowMultipleSlot = computed( () => props.multiple && props.selectedValueItem && selectedValueItems.value.length > 0 && props.showSlotContentInMultiple ); watch( () => shouldShowInput.value, () => { if (shouldShowInput.value && !props.alwaysComboboxIsInput) { nextTick(() => { textbox.value.focus(); }); } } ); const onOpenDropdown = async () => { if (!props.disabled && !props.readonly) { emit(DropdownBaseTextboxEvent.OPEN); // wait for popover to open before setting focus await nextTick(); textbox.value.focus(); } }; const onCloseDropdown = (event?: Event) => { if (props.open) { if (event) { stopEventPropagation(event); } textbox.value.focus(); emit(DropdownBaseTextboxEvent.CLOSE); } }; const onToggleDropdown = (event: Event) => { const isDynamicFilteringInputClickEvent = props.open && (event.target as HTMLDivElement).tagName === 'INPUT'; if (isDynamicFilteringInputClickEvent) { /** * When there's a click even on the input during filtering * and the popover is open, do not toggle the popover. * Allow for click events inside the input field */ return null; } return props.open ? onCloseDropdown() : onOpenDropdown(); }; const onClearSearch = () => { emit(DropdownBaseTextboxEvent.CLEAR); emit(DropdownBaseTextboxEvent.INPUT, ''); textbox.value.focus(); }; const onInputEvent = (event: Event) => { if (!props.readonly && !props.disabled) { emit(DropdownBaseTextboxEvent.INPUT, (event.target as HTMLInputElement).value); } else { event.preventDefault(); event.stopPropagation(); } }; const focus = () => { textbox.value.focus(); }; defineExpose({ focus, }); </script> <script lang="ts"> /** * Only for internal use, implements the logic of the textbox element to be implemented in the combobox. */ export default defineComponent({ i18n: { messages }, name: 'dropdown-base-textbox', inheritAttrs: false, }); </script> <style lang="scss" scoped src="./dropdown-base-textbox.scss" />
@@ -1 +1 @@
1
- <template> <component :is="popperContainerOrPopoverComponent" v-if="isDropdownOpen" class="b-dropdown-options-container" :class="conditionalClasses" v-bind="popperContainerOrPopoverProps" > <div @keydown.esc="onEscapeKey"> <bento-listbox :id="id" ref="dropdownOptionsListboxRef" :aria-label="ariaLabel" class="b-dropdown-options-container__listbox" :class="conditionalListboxClasses" :component-loading="componentLoading" :empty-state="emptyState" :is-option-disabled="isOptionDisabled" :items="items" :selected-value="internalSelectedValue" :multiple="multiple" :static-categories="staticCategories" :no-results-message="noResultsMessage" :searching="searching" :loading="loading" :lazy-load-type="lazyLoadType" :has-more-items="hasMoreItems" :virtual-scroll="virtualScroll" v-on="listeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox> </div> </component> </template> <script setup lang="ts"> import { computed, defineComponent, type PropType, ref, toRefs, useSlots } from 'vue'; import { BentoListbox } from '@/internal/listbox'; import { type BentoListboxEvent, type BentoListboxIsOptionDisabled, type BentoListboxOptions, } from '@/types/listbox'; import { PopperContainer } from '@/components/popper-container'; import { type BentoDropdownEmptyStateProps, type BentoDropdownEvent, BentoDropdownLazyLoadType, type BentoDropdownVirtulisationOptions, } from '../../dropdown.types'; import { useI18n } from '@/utils/ts/i18n'; import { BentoPopover } from '@/components/popover'; import { useDropdownOptionsListbox } from './composables/use-dropdown-options-listbox'; import { generateUid } from '@/core/utils/ts'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const slots = useSlots(); const emit = defineEmits<{ /** * Emitted when the dropdown is closing. */ (e: BentoDropdownEvent.CLOSE_DROPDOWN); /** * Emitted when the `ENTER` key is pressed */ (e: BentoListboxEvent.ENTER, newSelectedValue: BentoListboxOptions); /** * Emitted when the `ESCAPE` key is pressed */ (e: BentoListboxEvent.ESCAPE); /** * Emitted when an item is selected */ (e: BentoListboxEvent.SELECT, newSelectedValue: BentoListboxOptions); /** * Emitted when: * - `lazyLoad` is `automatic` and the scroll meets the end of the list * - `lazyload` is `button` and the button is clicked */ (e: BentoDropdownEvent.SHOW_MORE); /** * Emitted when the `SPACE` key is pressed */ (e: BentoListboxEvent.SPACE, newSelectedValue: BentoListboxOptions); /** * Emitted when the `TAB` key is pressed */ (e: BentoListboxEvent.TAB, newSelectedValue: BentoListboxOptions); }>(); const props = defineProps({ /** * Defines a string value that labels an interactive element. */ ariaLabel: { type: String, default: null }, /** * Indicates that dropdown items are loading */ componentLoading: { type: Boolean, default: false }, /** * Empty state props used with the inner empty state component which is displayed when no search results are found. */ emptyState: { type: Object as PropType<BentoDropdownEmptyStateProps>, default: undefined, }, /** * Disables dropdown functionality */ disabled: { type: Boolean, default: false }, /** * Indicates whether there are more items to load */ hasMoreItems: { type: Boolean, default: false }, /** * Identifies the listbox whose contents are controlled by the the combobox on which the aria-controls attribute is set. */ id: { type: String, required: true }, /** * Function that allows the options to be disabled * * @type {BentoDropdownIsOptionDisabled} * @param {BentoDropdownOptionItem} option - Dropdown option object * @param {string} option.label - Option's label text * @param {string|number} option.value - Option's value * @param {unknown} option.data - Option's extra data to be passed to the default slot */ isOptionDisabled: { type: Function as PropType<BentoListboxIsOptionDisabled>, default: undefined, }, /** * The option elements to populate the dropdown with. * It must be an array of {@see BentoDropdownOptionItem } * * @property {string} value.label - Text to be displayed in the option * * @property {string,number} value.value - Value of the option */ items: { type: Array as PropType<BentoListboxOptions>, required: true, }, /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button */ lazyLoadType: { type: String as PropType<BentoDropdownLazyLoadType | `${BentoDropdownLazyLoadType}`>, default: BentoDropdownLazyLoadType.AUTOMATIC, validator: (value: BentoDropdownLazyLoadType) => Object.values(BentoDropdownLazyLoadType).includes(value), }, /** * Indicates if new options are lazy loading. */ loading: { type: Boolean, default: false }, /** * If enabled, the user will be able to select multiple items. */ multiple: { type: Boolean, default: false }, /** * Indicates if the dropdown is open so that * the container can be displayed. */ open: { type: Boolean, required: true }, /** * The `input` value. * Providing an empty string or empty array will select no options. * Set to an empty string `''` if you don't want any of the available options to be selected for single select */ selectedValue: { type: Array as PropType<BentoListboxOptions>, default: null, }, /** * Flag that indicates that an external search it's being made * to filter out the items inside the the multi-select listbox * * If enabled, hides the "Select all" checkbox in "multiple" mode. */ searching: BentoListbox.props.searching, /** * If enabled, dropdown will display non-selectable, static categories */ staticCategories: { type: Boolean, default: false }, /** * Reference to the DOM element or Vue component for positioning * the Dropdown container. */ targetElement: { type: PopperContainer.props.targetElement.type, required: true }, /** * 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: { type: [Boolean, Object] as PropType<BentoDropdownVirtulisationOptions>, default: false, }, }); const { selectedValue, multiple } = toRefs(props); const dropdownOptionsListboxRef = ref(null); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const isDropdownOpen = computed(() => props.open && !props.disabled); const noResultsMessage = computed(() => t('noOptionsMatchThisSearch') as string); const conditionalClasses = computed(() => ({ 'b-dropdown-options-container--single': !props?.multiple, })); const conditionalListboxClasses = computed(() => ({ 'b-dropdown-options-container__listbox--single': !props?.multiple, })); const { actions, internalSelectedValue, listeners, onEscapeKey, onOutsideDropdownClick } = useDropdownOptionsListbox(selectedValue, multiple, isDropdownOpen, emit); // Use different components depending on if the dropdown is a single or multi-select const popperContainerOrPopoverComponent = computed(() => (props?.multiple ? BentoPopover : PopperContainer)); const popperContainerOrPopoverProps = computed(() => { const containerMinAndMaxWidth = { 'min-width': `${(props?.targetElement as HTMLElement)?.clientWidth}px`, 'max-width': 'min(500px, 95%)', }; return props?.multiple ? ({ actionsLayout: 'space-between', ariaLabel: props.ariaLabel, actions: actions.value, divider: true, fallbackPosition: ['top-start'], id: generateUid(`dropdown-options-${props.id}`), open: props.open, position: 'bottom-start', style: { width: 'auto', ...containerMinAndMaxWidth, }, targetElement: props.targetElement, } as InstanceType<typeof BentoPopover>['$props']) : ({ fallbackPosition: ['top-start'], offset: [0, 8], position: 'bottom-start', style: { display: isDropdownOpen.value ? 'block' : 'none', ...containerMinAndMaxWidth, }, targetElement: props.targetElement, } as InstanceType<typeof PopperContainer>['$props']); }); defineExpose({ onOutsideDropdownClick, }); </script> <script lang="ts"> /** * Dropdown options container. * It lists all the available options. * * @example * <bento-dropdown-options-container * v-if="inputContainerRef" <!-- Ref to the input to match the width --> * :id="dropdownOptionsContainerId" * :target-element="inputContainerRef" * :aria-label="ariaLabel" * :open="isDropdownOpen" * :disabled="disabled" * :multiple="multiple" * :selected="value" * :isOptionDisabled="option => option.value === 2" * :items="[{ * { label: 'Option 1', value: 1 }, * { label: 'Option 2', value: 2 }, * }]" * @select="onOptionSelected" * /> */ export default defineComponent({ i18n: { messages }, name: 'bento-dropdown-options-container', }); </script> <style lang="scss" scoped src="./dropdown-options-container.scss" />
1
+ <template> <component :is="popperContainerOrPopoverComponent" v-if="isDropdownOpen" class="b-dropdown-options-container" :class="conditionalClasses" v-bind="popperContainerOrPopoverProps" > <div @keydown.stop @keydown.esc="onEscapeKey"> <bento-listbox :id="id" ref="dropdownOptionsListboxRef" :aria-label="ariaLabel" class="b-dropdown-options-container__listbox" :class="conditionalListboxClasses" :component-loading="componentLoading" :empty-state="emptyState" :is-option-disabled="isOptionDisabled" :items="items" :selected-value="internalSelectedValue" :multiple="multiple" :static-categories="staticCategories" :no-results-message="noResultsMessage" :searching="searching" :loading="loading" :lazy-load-type="lazyLoadType" :has-more-items="hasMoreItems" :virtual-scroll="virtualScroll" v-on="listeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox> </div> </component> </template> <script setup lang="ts"> import { computed, defineComponent, type PropType, ref, toRefs, useSlots } from 'vue'; import { BentoListbox } from '@/internal/listbox'; import { type BentoListboxEvent, type BentoListboxIsOptionDisabled, type BentoListboxOptions, } from '@/types/listbox'; import { PopperContainer } from '@/components/popper-container'; import { type BentoDropdownEmptyStateProps, type BentoDropdownEvent, BentoDropdownLazyLoadType, type BentoDropdownVirtulisationOptions, } from '../../dropdown.types'; import { useI18n } from '@/utils/ts/i18n'; import { BentoPopover } from '@/components/popover'; import { useDropdownOptionsListbox } from './composables/use-dropdown-options-listbox'; import { generateUid } from '@/core/utils/ts'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const slots = useSlots(); const emit = defineEmits<{ /** * Emitted when the dropdown is closing. */ (e: BentoDropdownEvent.CLOSE_DROPDOWN); /** * Emitted when the `ENTER` key is pressed */ (e: BentoListboxEvent.ENTER, newSelectedValue: BentoListboxOptions); /** * Emitted when the `ESCAPE` key is pressed */ (e: BentoListboxEvent.ESCAPE); /** * Emitted when an item is selected */ (e: BentoListboxEvent.SELECT, newSelectedValue: BentoListboxOptions); /** * Emitted when: * - `lazyLoad` is `automatic` and the scroll meets the end of the list * - `lazyload` is `button` and the button is clicked */ (e: BentoDropdownEvent.SHOW_MORE); /** * Emitted when the `SPACE` key is pressed */ (e: BentoListboxEvent.SPACE, newSelectedValue: BentoListboxOptions); /** * Emitted when the `TAB` key is pressed */ (e: BentoListboxEvent.TAB, newSelectedValue: BentoListboxOptions); }>(); const props = defineProps({ /** * Defines a string value that labels an interactive element. */ ariaLabel: { type: String, default: null }, /** * Indicates that dropdown items are loading */ componentLoading: { type: Boolean, default: false }, /** * Empty state props used with the inner empty state component which is displayed when no search results are found. */ emptyState: { type: Object as PropType<BentoDropdownEmptyStateProps>, default: undefined, }, /** * Disables dropdown functionality */ disabled: { type: Boolean, default: false }, /** * Indicates whether there are more items to load */ hasMoreItems: { type: Boolean, default: false }, /** * Identifies the listbox whose contents are controlled by the the combobox on which the aria-controls attribute is set. */ id: { type: String, required: true }, /** * Function that allows the options to be disabled * * @type {BentoDropdownIsOptionDisabled} * @param {BentoDropdownOptionItem} option - Dropdown option object * @param {string} option.label - Option's label text * @param {string|number} option.value - Option's value * @param {unknown} option.data - Option's extra data to be passed to the default slot */ isOptionDisabled: { type: Function as PropType<BentoListboxIsOptionDisabled>, default: undefined, }, /** * The option elements to populate the dropdown with. * It must be an array of {@see BentoDropdownOptionItem } * * @property {string} value.label - Text to be displayed in the option * * @property {string,number} value.value - Value of the option */ items: { type: Array as PropType<BentoListboxOptions>, required: true, }, /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button */ lazyLoadType: { type: String as PropType<BentoDropdownLazyLoadType | `${BentoDropdownLazyLoadType}`>, default: BentoDropdownLazyLoadType.AUTOMATIC, validator: (value: BentoDropdownLazyLoadType) => Object.values(BentoDropdownLazyLoadType).includes(value), }, /** * Indicates if new options are lazy loading. */ loading: { type: Boolean, default: false }, /** * If enabled, the user will be able to select multiple items. */ multiple: { type: Boolean, default: false }, /** * Indicates if the dropdown is open so that * the container can be displayed. */ open: { type: Boolean, required: true }, /** * The `input` value. * Providing an empty string or empty array will select no options. * Set to an empty string `''` if you don't want any of the available options to be selected for single select */ selectedValue: { type: Array as PropType<BentoListboxOptions>, default: null, }, /** * Flag that indicates that an external search it's being made * to filter out the items inside the the multi-select listbox * * If enabled, hides the "Select all" checkbox in "multiple" mode. */ searching: BentoListbox.props.searching, /** * If enabled, dropdown will display non-selectable, static categories */ staticCategories: { type: Boolean, default: false }, /** * Reference to the DOM element or Vue component for positioning * the Dropdown container. */ targetElement: { type: PopperContainer.props.targetElement.type, required: true }, /** * 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: { type: [Boolean, Object] as PropType<BentoDropdownVirtulisationOptions>, default: false, }, }); const { selectedValue, multiple } = toRefs(props); const dropdownOptionsListboxRef = ref(null); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const isDropdownOpen = computed(() => props.open && !props.disabled); const noResultsMessage = computed(() => t('noOptionsMatchThisSearch') as string); const conditionalClasses = computed(() => ({ 'b-dropdown-options-container--single': !props?.multiple, })); const conditionalListboxClasses = computed(() => ({ 'b-dropdown-options-container__listbox--single': !props?.multiple, })); const { actions, internalSelectedValue, listeners, onEscapeKey, onOutsideDropdownClick } = useDropdownOptionsListbox(selectedValue, multiple, isDropdownOpen, emit); // Use different components depending on if the dropdown is a single or multi-select const popperContainerOrPopoverComponent = computed(() => (props?.multiple ? BentoPopover : PopperContainer)); const popperContainerOrPopoverProps = computed(() => { const containerMinAndMaxWidth = { 'min-width': `${(props?.targetElement as HTMLElement)?.clientWidth}px`, 'max-width': 'min(500px, 95%)', }; return props?.multiple ? ({ actionsLayout: 'space-between', ariaLabel: props.ariaLabel, actions: actions.value, divider: true, fallbackPosition: ['top-start'], id: generateUid(`dropdown-options-${props.id}`), open: props.open, position: 'bottom-start', style: { width: 'auto', ...containerMinAndMaxWidth, }, targetElement: props.targetElement, } as InstanceType<typeof BentoPopover>['$props']) : ({ fallbackPosition: ['top-start'], offset: [0, 8], position: 'bottom-start', style: { display: isDropdownOpen.value ? 'block' : 'none', ...containerMinAndMaxWidth, }, targetElement: props.targetElement, } as InstanceType<typeof PopperContainer>['$props']); }); defineExpose({ onOutsideDropdownClick, }); </script> <script lang="ts"> /** * Dropdown options container. * It lists all the available options. * * @example * <bento-dropdown-options-container * v-if="inputContainerRef" <!-- Ref to the input to match the width --> * :id="dropdownOptionsContainerId" * :target-element="inputContainerRef" * :aria-label="ariaLabel" * :open="isDropdownOpen" * :disabled="disabled" * :multiple="multiple" * :selected="value" * :isOptionDisabled="option => option.value === 2" * :items="[{ * { label: 'Option 1', value: 1 }, * { label: 'Option 2', value: 2 }, * }]" * @select="onOptionSelected" * /> */ export default defineComponent({ i18n: { messages }, name: 'bento-dropdown-options-container', }); </script> <style lang="scss" scoped src="./dropdown-options-container.scss" />
@@ -1 +1 @@
1
- <template> <base-button ref="filterBarButtonRef" v-bind="$attrs" class="b-filter-bar-button" :class="conditionalClasses" :disabled="disabled" :aria-label="computedAriaLabel" :aria-controls="ariaAttributes['aria-controls']" type="button" @click="onClickButton" > <div class="b-filter-bar-button__container"> <template v-if="hasSlot('iconLeft')"> <slot name="iconLeft" /> </template> <div class="b-filter-bar-button__label-container"> <bento-typography el="span" variant="body" stronger class="b-filter-bar-button__label"> <span> <slot /> </span> <bento-typography v-if="secondaryLabel && !isIconOnly" el="span" variant="body" stronger class="b-filter-bar-button__label" >:</bento-typography > </bento-typography> <bento-typography v-if="secondaryLabel && !isIconOnly" el="span" variant="body" stronger class="b-filter-bar-button__secondary-label" > {{ secondaryLabel }} </bento-typography> </div> </div> <template #iconRight> <bento-typography v-if="appliedFiltersCount && !isIconOnly" class="b-filter-bar-button__counter" el="span" variant="body" stronger > {{ appliedFiltersCount }} </bento-typography> </template> </base-button> </template> <script setup lang="ts"> import { BaseButton } from '@/components/internal/base-button'; import { BentoTypography } from '@/components/typography'; import { useHasSlot } from '@/composables'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { computed, ref, useAttrs, useSlots } from 'vue'; const props = defineProps({ /** * Allows to set the button as active without setting a "secondaryLabel" */ active: { type: Boolean, default: false }, /** * The number of applied values within the filter. * Text can also be applied here such as 'All'. * e.g. if 2 items in a dropdown filter have been applied, the number 2 will be shown here. */ appliedFiltersCount: { type: [String, Number], default: null }, /** * Disables button functionality. */ disabled: { type: Boolean, default: false }, /** * Signifies if the filter is open or closed. */ isFilterOpen: { type: Boolean, default: false }, /** * Signifies if the filter is a persistent filter */ isPersistentFilter: { type: Boolean, default: false }, /** * Renders the button without the applied state styles */ noAppliedStateStyles: { type: Boolean, default: false }, /** * A secondary label to be added next to the primary for additional filtering information. * i.e. Can be used to show the set value for said filter. */ secondaryLabel: { type: String, default: null }, }); const emit = defineEmits<{ /** * Emitted when the button is clicked */ (e: 'click', event: MouseEvent); }>(); const slots = useSlots(); const attrs = useAttrs(); const hasSlot = useHasSlot(slots); const labelSlot = computed((): string => (slots.default ? getSlotText(slots)('default') : null)); const isIconOnly = computed(() => !hasSlot('default') && hasSlot('iconLeft')); const computedAriaLabel = useAriaLabel({ ariaLabel: labelSlot }); const filterBarButtonRef = ref(null); const ariaAttributes = computed(() => ({ 'aria-controls': attrs['aria-controls']?.toString() as string, })); const isAppliedFilter = computed( () => (props.appliedFiltersCount || props.secondaryLabel || props.isPersistentFilter) && !props.noAppliedStateStyles ); const conditionalClasses = computed(() => ({ 'b-filter-bar-button--applied': isAppliedFilter.value || props.active, 'b-filter-bar-button--filter-open': props.isFilterOpen && !isAppliedFilter.value, 'b-filter-bar-button--filter-open-applied': props.isFilterOpen && isAppliedFilter.value, 'b-filter-bar-button--with-secondary-label': props.secondaryLabel, 'b-filter-bar-button--with-counter': props.appliedFiltersCount, 'b-filter-bar-button--icon-only': isIconOnly.value, })); const onClickButton = (event: MouseEvent) => { event.preventDefault(); if (!props.disabled) { emit('click', event); } }; </script> <script lang="ts"> /** * A filter button used as a base to extend all sorts of filter buttons. * Used in the bento-filter-bar and all-filter-modals components. * * @usage * export default { * components: { FilterBarButton }, * template: ` * <filter-bar-button> * Filter * </filter-bar-button> * `, * } */ export default {}; </script> <style lang="scss" scoped src="./filter-bar-button.scss" />
1
+ <template> <base-button ref="filterBarButtonRef" v-bind="$attrs" class="b-filter-bar-button" :class="conditionalClasses" :disabled="disabled" :aria-label="computedAriaLabel" :aria-controls="ariaAttributes['aria-controls']" type="button" @click="onClickButton" > <div class="b-filter-bar-button__container"> <template v-if="hasSlot('iconLeft')"> <slot name="iconLeft" /> </template> <div class="b-filter-bar-button__label-container"> <bento-typography el="span" variant="body" stronger class="b-filter-bar-button__label"> <span> <slot /> </span> <bento-typography v-if="secondaryLabel && !isIconOnly" el="span" variant="body" stronger class="b-filter-bar-button__label" >:</bento-typography > </bento-typography> <bento-typography v-if="appliedFiltersCount && !isIconOnly" class="b-filter-bar-button__counter" el="span" variant="body" stronger > {{ appliedFiltersCount }} </bento-typography> <bento-typography v-if="secondaryLabel && !isIconOnly" el="span" variant="body" stronger class="b-filter-bar-button__secondary-label" > {{ secondaryLabel }} </bento-typography> </div> </div> </base-button> </template> <script setup lang="ts"> import { BaseButton } from '@/components/internal/base-button'; import { BentoTypography } from '@/components/typography'; import { useHasSlot } from '@/composables'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { computed, ref, useAttrs, useSlots } from 'vue'; const props = defineProps({ /** * Allows to set the button as active without setting a "secondaryLabel" */ active: { type: Boolean, default: false }, /** * The number of applied values within the filter. * Text can also be applied here such as 'All'. * e.g. if 2 items in a dropdown filter have been applied, the number 2 will be shown here. */ appliedFiltersCount: { type: [String, Number], default: null }, /** * Disables button functionality. */ disabled: { type: Boolean, default: false }, /** * Signifies if the filter is open or closed. */ isFilterOpen: { type: Boolean, default: false }, /** * Signifies if the filter is a persistent filter */ isPersistentFilter: { type: Boolean, default: false }, /** * Renders the button without the applied state styles */ noAppliedStateStyles: { type: Boolean, default: false }, /** * A secondary label to be added next to the primary for additional filtering information. * i.e. Can be used to show the set value for said filter. */ secondaryLabel: { type: String, default: null }, }); const emit = defineEmits<{ /** * Emitted when the button is clicked */ (e: 'click', event: MouseEvent); }>(); const slots = useSlots(); const attrs = useAttrs(); const hasSlot = useHasSlot(slots); const labelSlot = computed((): string => (slots.default ? getSlotText(slots)('default') : null)); const isIconOnly = computed(() => !hasSlot('default') && hasSlot('iconLeft')); const computedAriaLabel = useAriaLabel({ ariaLabel: labelSlot }); const filterBarButtonRef = ref(null); const ariaAttributes = computed(() => ({ 'aria-controls': attrs['aria-controls']?.toString() as string, })); const isAppliedFilter = computed( () => (props.appliedFiltersCount || props.secondaryLabel || props.isPersistentFilter) && !props.noAppliedStateStyles ); const conditionalClasses = computed(() => ({ 'b-filter-bar-button--applied': isAppliedFilter.value || props.active, 'b-filter-bar-button--filter-open': props.isFilterOpen && !isAppliedFilter.value, 'b-filter-bar-button--filter-open-applied': props.isFilterOpen && isAppliedFilter.value, 'b-filter-bar-button--with-secondary-label': props.secondaryLabel, 'b-filter-bar-button--with-counter': props.appliedFiltersCount, 'b-filter-bar-button--icon-only': isIconOnly.value, })); const onClickButton = (event: MouseEvent) => { event.preventDefault(); if (!props.disabled) { emit('click', event); } }; </script> <script lang="ts"> /** * A filter button used as a base to extend all sorts of filter buttons. * Used in the bento-filter-bar and all-filter-modals components. * * @usage * export default { * components: { FilterBarButton }, * template: ` * <filter-bar-button> * Filter * </filter-bar-button> * `, * } */ export default {}; </script> <style lang="scss" scoped src="./filter-bar-button.scss" />
@@ -1 +1 @@
1
- <template> <bento-base-filter v-bind="$props" :button-label="buttonLabel" :button-secondary-label="buttonSecondaryLabel" :disable-apply-button="shouldDisableApply" :disable-secondary-button="isSecondaryButtonDisabled" :only-slot="onlySlot" :tooltip-position="options?.tooltipPosition" :tooltip-text="options?.tooltipText" @apply="updateFilter" @clear="clearFilter" @open="openFilter" @reset="resetFilter" > <div class="b-input-with-dropdown-filter" :class="conditionalClasses" @keydown.enter="updateFilter" @input="onInput" > <bento-input-field ref="inputTextRef" :dropdown="dropdownOptions" :dropdown-position="options?.dropdownPosition" variant="dropdown" :model-value="internalFilterValue?.inputValue" :disabled="disabled" @update:model-value="setInternalFilterValue" @dropdown-input="onDropdownInput" /> </div> </bento-base-filter> </template> <script setup lang="ts"> import { computed, onBeforeUpdate, provide, ref, toRef, watch } from 'vue'; import { BentoBaseFilter, useBaseFilter } from '../base-filter'; import { BentoFilterEvent, type BentoInputWithDropdownFilterOptions, type BentoInpuWithDropdowntFilterValue, } from '../../filter-bar.types'; import { type BentoInputDropdownProps, BentoInputField } from '@/components/input-field'; import { truncate } from '@/utils/ts/truncate'; import { type BentoListboxSelectedValue } from '@/types/listbox'; import { INPUT_FILTER_WITH_DROPDOWN_POPOVER_INJECTION_KEY } from '@/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.keys'; const props = withDefaults( defineProps<{ /** * Disables the filter from being interacted with. */ disabled?: boolean; /** * Default value of the filter to reset to. */ defaultValue?: BentoInpuWithDropdowntFilterValue; /** * Filter name/key referencing the actual property name in the data set. * Should be unique per filter bar. */ field: string; /** * Label text to be displayed */ label: string; /** * Renders the base filter's slot content only - leaving out the button and the popover. * Used for the allFiltersModal component. */ onlySlot?: boolean; /** * List of options that are needed to setup the filter * Available options: * - dropdown: all the dropdown properties that can be set in the inner dropdown of the input field. @see BentoDropdown for more info. * - dropdownPosition: allow the user to set the dropdown on the left or right side. Accepts the `start` or `end` values. * - tooltipPosition: The position of the tooltip attached to the filter button * - tooltipText: The content of the tooltip attached to the filter button */ options?: BentoInputWithDropdownFilterOptions; /** * Current value of the filter. * If the options.variant is equal to 'dropdown', the value should be of type should be `{ inputValue: string | null; dropdownValue?: string }`. */ value?: BentoInpuWithDropdowntFilterValue; }>(), { defaultValue: undefined, onlySlot: false, options: undefined, value: undefined, } ); const emit = defineEmits([BentoFilterEvent.INPUT, BentoFilterEvent.UPDATE]); const inputTextRef = ref(null); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled } = useBaseFilter<BentoInpuWithDropdowntFilterValue>(toRef(props, 'value'), toRef(props, 'defaultValue')); const buttonLabel = computed(() => props.label); const buttonSecondaryLabel = computed(() => truncate(props.value?.inputValue, 12)); const dropdownSelection = computed( () => internalFilterValue.value?.dropdownValue ?? props.options?.dropdown?.modelValue ?? props.options?.dropdown?.value ); const dropdownOptions = computed<BentoInputDropdownProps>(() => ({ ...((props.options?.dropdown || {}) as BentoInputDropdownProps), modelValue: dropdownSelection.value, })); const conditionalClasses = computed(() => ({ [`b-input-with-dropdown-filter--only-slot`]: props.onlySlot, })); const shouldDisableApply = computed(() => { const allFieldsAreSet = !!internalFilterValue.value?.inputValue && !!dropdownSelection.value; return !allFieldsAreSet || isApplyButtonDisabled.value; }); onBeforeUpdate(() => { // Set value to null when the string is empty if (internalFilterValue.value?.inputValue === '') { internalFilterValue.value = { ...internalFilterValue.value, inputValue: undefined }; } }); watch(inputTextRef, () => { // Focus on the input when it's opened inputTextRef.value?.focusInput(); }); const openFilter = () => { internalFilterValue.value = props.value; }; const updateFilter = () => { if (!internalFilterValue.value?.inputValue && props.defaultValue !== null) { // If value set to empty, reset back to default value internalFilterValue.value = { ...props.defaultValue }; } emit(BentoFilterEvent.UPDATE, { ...props, value: internalFilterValue.value, }); }; const resetFilter = () => emit(BentoFilterEvent.UPDATE, { ...props, value: props.defaultValue }); const clearFilter = () => { internalFilterValue.value = null; updateFilter(); }; const onInput = () => { emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const setInternalFilterValue = (event: string) => { internalFilterValue.value = { dropdownValue: dropdownSelection.value, inputValue: event }; }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { internalFilterValue.value = { ...internalFilterValue.value, dropdownValue: selectedValue }; }; // We need to change the base filter's popover to have overflow visible so the dropdown options is // not hidden inside the filter when opened. provide(INPUT_FILTER_WITH_DROPDOWN_POPOVER_INJECTION_KEY, true); </script> <script lang="ts"> /** * */ export default { name: 'bento-input-with-dropdown-filter', }; </script> <style lang="scss" scoped src="./input-with-dropdown-filter.scss" />
1
+ <template> <bento-base-filter v-bind="$props" :applied-filters-count="selectedDropdownLabel" :button-label="buttonLabel" :button-secondary-label="buttonSecondaryLabel" :disable-apply-button="shouldDisableApply" :disable-secondary-button="isSecondaryButtonDisabled" :only-slot="onlySlot" :tooltip-position="options?.tooltipPosition" :tooltip-text="options?.tooltipText" @apply="updateFilter" @clear="clearFilter" @open="openFilter" @reset="resetFilter" > <div class="b-input-with-dropdown-filter" :class="conditionalClasses" @keydown.enter="updateFilter" @input="onInput" > <bento-input-field ref="inputTextRef" :dropdown="dropdownOptions" :dropdown-position="options?.dropdownPosition" variant="dropdown" :model-value="internalFilterValue?.inputValue" :disabled="disabled" @update:model-value="setInternalFilterValue" @dropdown-input="onDropdownInput" /> </div> </bento-base-filter> </template> <script setup lang="ts"> import { computed, onBeforeUpdate, provide, ref, toRef, watch } from 'vue'; import { BentoBaseFilter, useBaseFilter } from '../base-filter'; import { BentoFilterEvent, type BentoInputWithDropdownFilterOptions, type BentoInpuWithDropdowntFilterValue, } from '../../filter-bar.types'; import { type BentoInputDropdownProps, BentoInputField } from '@/components/input-field'; import { truncate } from '@/utils/ts/truncate'; import { type BentoListboxSelectedValue } from '@/types/listbox'; import { INPUT_FILTER_WITH_DROPDOWN_POPOVER_INJECTION_KEY } from '@/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.keys'; const props = withDefaults( defineProps<{ /** * Disables the filter from being interacted with. */ disabled?: boolean; /** * Default value of the filter to reset to. */ defaultValue?: BentoInpuWithDropdowntFilterValue; /** * Filter name/key referencing the actual property name in the data set. * Should be unique per filter bar. */ field: string; /** * Label text to be displayed */ label: string; /** * Renders the base filter's slot content only - leaving out the button and the popover. * Used for the allFiltersModal component. */ onlySlot?: boolean; /** * List of options that are needed to setup the filter * Available options: * - dropdown: all the dropdown properties that can be set in the inner dropdown of the input field. @see BentoDropdown for more info. * - dropdownPosition: allow the user to set the dropdown on the left or right side. Accepts the `start` or `end` values. * - tooltipPosition: The position of the tooltip attached to the filter button * - tooltipText: The content of the tooltip attached to the filter button */ options?: BentoInputWithDropdownFilterOptions; /** * Current value of the filter. * If the options.variant is equal to 'dropdown', the value should be of type should be `{ inputValue: string | null; dropdownValue?: string }`. */ value?: BentoInpuWithDropdowntFilterValue; }>(), { defaultValue: undefined, onlySlot: false, options: undefined, value: undefined, } ); const emit = defineEmits([BentoFilterEvent.INPUT, BentoFilterEvent.UPDATE]); const inputTextRef = ref(null); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled } = useBaseFilter<BentoInpuWithDropdowntFilterValue>(toRef(props, 'value'), toRef(props, 'defaultValue')); const selectedDropdownLabel = computed(() => { const selectedValue = props.value?.dropdownValue ?? props.options?.dropdown?.modelValue ?? props.options?.dropdown?.value; if ( !buttonSecondaryLabel.value || selectedValue === undefined || selectedValue === null || !props.options?.dropdown?.items ) { return null; } const label = props.options.dropdown.items.find(item => item.value === selectedValue)?.label; return label ? truncate(label, 12) : null; }); const buttonLabel = computed(() => props.label); const buttonSecondaryLabel = computed(() => props.value?.inputValue ? truncate(props.value.inputValue, 12) : undefined ); const dropdownSelection = computed( () => internalFilterValue.value?.dropdownValue ?? props.options?.dropdown?.modelValue ?? props.options?.dropdown?.value ); const dropdownOptions = computed<BentoInputDropdownProps>(() => ({ ...((props.options?.dropdown || {}) as BentoInputDropdownProps), modelValue: dropdownSelection.value, })); const conditionalClasses = computed(() => ({ [`b-input-with-dropdown-filter--only-slot`]: props.onlySlot, })); const shouldDisableApply = computed(() => { const allFieldsAreSet = !!internalFilterValue.value?.inputValue && !!dropdownSelection.value; return !allFieldsAreSet || isApplyButtonDisabled.value; }); onBeforeUpdate(() => { // Set value to null when the string is empty if (internalFilterValue.value?.inputValue === '') { internalFilterValue.value = { ...internalFilterValue.value, inputValue: undefined }; } }); watch(inputTextRef, () => { // Focus on the input when it's opened inputTextRef.value?.focusInput(); }); const openFilter = () => { internalFilterValue.value = props.value; }; const updateFilter = () => { if (!internalFilterValue.value?.inputValue && props.defaultValue !== null) { // If value set to empty, reset back to default value internalFilterValue.value = { ...props.defaultValue }; } emit(BentoFilterEvent.UPDATE, { ...props, value: internalFilterValue.value, }); }; const resetFilter = () => emit(BentoFilterEvent.UPDATE, { ...props, value: props.defaultValue }); const clearFilter = () => { internalFilterValue.value = null; updateFilter(); }; const onInput = () => { emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const setInternalFilterValue = (event: string) => { internalFilterValue.value = { dropdownValue: dropdownSelection.value, inputValue: event }; }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { internalFilterValue.value = { ...internalFilterValue.value, dropdownValue: selectedValue }; }; // We need to change the base filter's popover to have overflow visible so the dropdown options is // not hidden inside the filter when opened. provide(INPUT_FILTER_WITH_DROPDOWN_POPOVER_INJECTION_KEY, true); </script> <script lang="ts"> /** * */ export default { name: 'bento-input-with-dropdown-filter', }; </script> <style lang="scss" scoped src="./input-with-dropdown-filter.scss" />
@@ -24,6 +24,13 @@ another message is dismissed.
24
24
  If user hovers over the stack, if unfolds showing up to last 3 toasts and the rest of the toast stacked (if there are
25
25
  more than three).
26
26
 
27
+ ### Toasts triggered by the page when a modal/dialog is open
28
+
29
+ When a modal, dialog or some other page overlay component is open and a toast is triggered by the page in the background
30
+ (for example, if there’s some asynchronous process happening that fails), the toast won’t be displayed until the overlay
31
+ component is closed, following the principle that modal is a focused environment, so anything happening in the
32
+ background should wait until the user is done with their flow. Once the modal is closed, the toast(s) will appear.
33
+
27
34
  ## Use Cases
28
35
 
29
36
  Due to their temporary nature, use toasts for at-a-glance, non-critical feedback about the app’s processes, i.e. a