@adyen/bento-mcp 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/SKILL.md +56 -6
  3. package/dist/assets/components/data-grid/components/data-grid-cell/data-grid-cell.vue +1 -1
  4. package/dist/assets/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.vue +1 -1
  5. package/dist/assets/components/data-grid/components/data-grid-columns/data-grid-columns.vue +1 -1
  6. package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.vue +1 -1
  7. package/dist/assets/components/data-grid/data-grid.docs.mdx +1 -1
  8. package/dist/assets/components/data-grid/data-grid.vue +1 -1
  9. package/dist/assets/components/date-picker/date-picker.vue +1 -1
  10. package/dist/assets/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.vue +1 -1
  11. package/dist/assets/components/draggable/components/draggable-handle/draggable-handle.stories.ts +1 -0
  12. package/dist/assets/components/draggable/components/draggable-handle/draggable-handle.types.ts +1 -0
  13. package/dist/assets/components/draggable/components/draggable-handle/draggable-handle.vue +1 -0
  14. package/dist/assets/components/draggable/draggable.docs.mdx +178 -0
  15. package/dist/assets/components/draggable/draggable.stories.ts +1 -0
  16. package/dist/assets/components/draggable/draggable.types.ts +1 -0
  17. package/dist/assets/components/draggable/draggable.vue +1 -0
  18. package/dist/assets/components/filter-bar/components/date-filter/date-filter.vue +1 -1
  19. package/dist/assets/components/filter-bar/filter-bar.docs.mdx +1 -0
  20. package/dist/assets/components/filter-bar/filter-bar.stories.ts +1 -1
  21. package/dist/assets/components/filter-bar/filter-bar.types.ts +1 -1
  22. package/dist/assets/components/filter-bar/filter-bar.vue +1 -1
  23. package/dist/assets/components/form-layout/components/form-layout-group/form-layout-group.vue +1 -1
  24. package/dist/assets/components/form-layout/form-layout.docs.mdx +33 -20
  25. package/dist/assets/components/form-layout/form-layout.stories.ts +1 -1
  26. package/dist/assets/components/input-field/input-field.vue +1 -1
  27. package/dist/assets/components/input-time/input-time.docs.mdx +52 -0
  28. package/dist/assets/components/input-time/input-time.stories.ts +1 -0
  29. package/dist/assets/components/input-time/input-time.types.ts +1 -0
  30. package/dist/assets/components/input-time/input-time.vue +1 -0
  31. package/dist/assets/components/internal/listbox/components/listbox-multi-select/listbox-multi-select.vue +1 -1
  32. package/dist/assets/components/secondary-nav/components/secondary-nav-category/secondary-nav-category.vue +1 -1
  33. package/dist/assets/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue +1 -1
  34. package/dist/assets/components/secondary-nav/secondary-nav.docs.mdx +32 -2
  35. package/dist/assets/components/secondary-nav/secondary-nav.stories.ts +1 -1
  36. package/dist/assets/components/secondary-nav/secondary-nav.types.ts +1 -1
  37. package/dist/assets/components/secondary-nav/secondary-nav.vue +1 -1
  38. package/dist/assets/components.json +101 -99
  39. package/dist/assets/deprecations.md +196 -196
  40. package/dist/assets/index.ts +1 -1
  41. package/dist/assets/tailwind/README.md +45 -5
  42. package/dist/main.js +2 -3
  43. package/package.json +2 -3
  44. package/dist/assets/components/draggable-handle/draggable-handle.stories.ts +0 -1
  45. package/dist/assets/components/draggable-handle/draggable-handle.types.ts +0 -1
  46. package/dist/assets/components/draggable-handle/draggable-handle.vue +0 -1
  47. /package/dist/assets/components/{draggable-handle → draggable/components/draggable-handle}/draggable-handle.docs.mdx +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 (2026-04-15)
4
+
5
+ ### Bug Fixes
6
+
7
+ - fixed `bento-mcp` components registry ([1656acaf8](https://github.com/Adyen/bento/commit/1656acaf8))
8
+
9
+ ### Miscellaneous Chores
10
+
11
+ - removed `@swc/cli` from `mcp` package to fix `CVE-2026-31808` and `CVE-2026-32630` ([3c2114a97](https://github.com/Adyen/bento/commit/3c2114a97))
12
+
13
+ ### ❤️ Thank You
14
+
15
+ - daver
16
+ - erice
17
+
18
+
19
+ ## 0.3.0 (2026-04-08)
20
+
21
+ ### Features
22
+
23
+ - amended writing bento comps skill to batch `get_component` mcp tool calls when needed ([5b9100929](https://github.com/Adyen/bento/commit/5b9100929))
24
+
25
+ ### ❤️ Thank You
26
+
27
+ - gerald
28
+
29
+
3
30
  ## 0.2.1 (2026-04-01)
4
31
 
5
32
  This was a version bump only for mcp to align it with other projects, there were no code changes.
package/SKILL.md CHANGED
@@ -35,6 +35,21 @@ Once the component name is known:
35
35
  - Execute `get_component(['component-name'])`.
36
36
  - **If tools are missing:** Provide the user with the setup instructions in Section 2.
37
37
 
38
+ #### ⚠️ CRITICAL: Truncation & Completeness Rules
39
+
40
+ 1. **One-at-a-Time Rule:** If requesting 3+ components, call `get_component` for **each component individually**. Batch
41
+ requests for many components often exceed output limits and result in truncated/incomplete documentation.
42
+
43
+ 2. **Truncation = Failure:** If `get_component` output is truncated or shows "Output too large", treat it as a **failed
44
+ fetch**. You MUST re-run the tool for each missing component individually.
45
+
46
+ 3. **Completeness Gate:** You are **STRICTLY PROHIBITED** from writing any Bento component code until you have
47
+ successfully fetched and read the full documentation (`.docs.mdx`, `.stories.ts`, `.vue`, `.types.ts`) for **EVERY**
48
+ Bento component you intend to use in that file.
49
+
50
+ 4. **Pre-Write Checklist:** Before any `write_file` or code edit, mentally verify: _"Have I called `get_component` for
51
+ every Bento component in this file?"_ If the answer is no, **STOP** and fetch the missing components.
52
+
38
53
  ### Step 3: Constraints First (Read `.docs.mdx`)
39
54
 
40
55
  - **DO NOT SKIM.** Look specifically for **"Do not use"** and **"Guidelines"** sections.
@@ -48,7 +63,7 @@ Once the component name is known:
48
63
 
49
64
  ---
50
65
 
51
- ## 🚫 ANTI-PATTERNS (Forbidden Actions)
66
+ ## 🚫 2. ANTI-PATTERNS (Forbidden Actions)
52
67
 
53
68
  To prevent token waste and errors, the following actions are **STRICTLY FORBIDDEN**:
54
69
 
@@ -56,10 +71,41 @@ To prevent token waste and errors, the following actions are **STRICTLY FORBIDDE
56
71
  - ❌ **Type Inspection:** Trying to read `.d.ts` or `.js` files inside `node_modules`.
57
72
  - ❌ **Icon Guessing:** Searching the filesystem for icons instead of checking `.stories.ts`.
58
73
  - ❌ **Grep-Discovery:** Running `grep` on `node_modules` to find component exports.
74
+ - ❌ **Partial Implementation:** Writing code that uses Bento components you haven't fetched via `get_component`.
75
+ - ❌ **Scavenging Truncated Output:** Using partial information from a truncated `get_component` response instead of
76
+ re-fetching individually.
77
+
78
+ ---
79
+
80
+ ## 🎨 3. Figma MCP Integration (Name Authority)
81
+
82
+ When using Figma MCP alongside Bento MCP:
83
+
84
+ ### ⚠️ Component Name Sovereignty
85
+
86
+ Design-to-code tools (like Figma MCP) often provide **legacy or internal names** that do not match the public API:
87
+
88
+ | Figma MCP Output | Correct Bento Export |
89
+ | :--------------- | :------------------- |
90
+ | `BHeader` | `BentoHeader` |
91
+ | `BModal` | `BentoModal` |
92
+ | `BButton` | `BentoButton` |
93
+ | `BInput` | `BentoInputField` |
94
+
95
+ **Rule:** You MUST **ignore** component names from Figma MCP and use **ONLY** the names discovered via `get_component`
96
+ or `get_all_components`. The Bento MCP is the **sole authority** on component names and exports.
97
+
98
+ ### Figma + Bento Workflow
99
+
100
+ 1. **Figma MCP** identifies visual components in the design (e.g., header, modal, button).
101
+ 2. **Bento MCP** `get_all_components()` to find the correct Bento component names.
102
+ 3. **Bento MCP** `get_component(['component-name'])` for each component individually (following the **One-at-a-Time
103
+ Rule**).
104
+ 4. **Implementation:** Use the names, props, and patterns from Bento MCP, not Figma MCP.
59
105
 
60
106
  ---
61
107
 
62
- ## ⚙️ 2. Bento MCP Setup
108
+ ## ⚙️ 4. Bento MCP Setup
63
109
 
64
110
  If the Bento MCP tools are not available in your environment, add the following configuration to your client:
65
111
 
@@ -82,10 +128,11 @@ If the Bento MCP tools are not available in your environment, add the following
82
128
  - **Gemini:** `.gemini/settings.json` (Local) or `~/.gemini/settings.json` (Global)
83
129
  - **Windsurf:** `~/.codeium/windsurf/mcp_config.json`
84
130
  - **Claude Desktop:** `~/Library/Application Support/Claude/claude_desktop_config.json`
131
+ - **Factory Droid:** Run `droid mcp add bento -- npx @adyen/bento-mcp@latest`
85
132
 
86
133
  ---
87
134
 
88
- ## 📚 3. Immutable Documentation (Tailwind & Tokens)
135
+ ## 📚 5. Immutable Documentation (Tailwind & Tokens)
89
136
 
90
137
  The following rules are NOT available via the MCP and must be followed strictly:
91
138
 
@@ -112,7 +159,7 @@ The `global.css` MUST follow this exact `@layer` order:
112
159
 
113
160
  ---
114
161
 
115
- ## 🎯 4. Tool Prioritization
162
+ ## 🎯 6. Tool Prioritization
116
163
 
117
164
  1. **Primary Tool:** **Bento MCP** (`get_all_components`, `get_component`).
118
165
  2. **Verification Tool:** **Production Code** (e.g., `wrapper.ts`).
@@ -120,7 +167,7 @@ The `global.css` MUST follow this exact `@layer` order:
120
167
 
121
168
  ---
122
169
 
123
- ## 🩺 5. Bento Doctor (Troubleshooting)
170
+ ## 🩺 7. Bento Doctor (Troubleshooting)
124
171
 
125
172
  | Issue | Resolution |
126
173
  | :---------------------- | :------------------------------------------------------------------------------------------- |
@@ -130,9 +177,12 @@ The `global.css` MUST follow this exact `@layer` order:
130
177
 
131
178
  ---
132
179
 
133
- ## ✅ 6. Final Review Checklist
180
+ ## ✅ 8. Final Review Checklist
134
181
 
135
182
  - [ ] **Discovery Check:** Did I use `get_all_components()` instead of `ls node_modules`?
183
+ - [ ] **Completeness Check:** Did I call `get_component` for **EVERY** Bento component I'm using?
184
+ - [ ] **Truncation Check:** Was any `get_component` output truncated? If yes, did I re-fetch individually?
185
+ - [ ] **Name Authority Check:** Am I using component names from Bento MCP (not Figma MCP)?
136
186
  - [ ] **Constraint Check:** Did I verify the "Do not use" section in the `.docs.mdx`?
137
187
  - [ ] **Literal Check:** Are all props using **string literals**?
138
188
  - [ ] **Layer Check:** Is my `global.css` using the correct Tailwind 4 `@layer` order?
@@ -1 +1 @@
1
- <template> <div :key="`cell-${column.field}`" ref="gridcellRef" :aria-hidden="ariaHidden" :data-testid="`cell-${column.field}`" :data-cellindex="getCellIndex(rowIndex, colIndex, columnSetIndex)" class="b-data-grid-cell" :class="conditionalRowCellClasses(column)" role="gridcell" :tabindex="tabIndex" :aria-colindex="getColIndex(colIndex, columnSetIndex) + 1" :style="{ width: column.autoWidth && isMeasuringColumnWidths ? 'auto' : `${column.width}px`, }" v-on="getGridcellEvents(column)" > <template v-if="column.field === DATAGRID_ACTIONS_COLUMN_FIELD_NAME"> <template v-if="selectable"> <bento-checkbox class="b-data-grid-cell__select-cell" :aria-label="t('selectRow')" :disabled="isDataItemDisabled(rowData)" :indeterminate="selectionIsIndeterminate" :model-value="selectedItems" :value="getDataItemId(rowData)" :tabindex="isFocused ? 0 : -1" @update:model-value="$emit('select-row', $event)" /> </template> <template v-if="nestedContent"> <bento-toggle-button ref="menuButtonRef" aria-haspopup="true" :aria-controls="controlledNestedRowElementIds" condensed variant="tertiary" :toggled="isNestedContentExpanded" @click="toggleNestedContent" > <template #iconLeft> <chevron-up-icon v-if="isNestedContentExpanded" class="b-accordion-item__icon" :svg-title="t('collapseNestedContent')" /> <chevron-down-icon v-else class="b-accordion-item__icon" :svg-title="t('expandNestedContent')" /> </template> </bento-toggle-button> </template> </template> <template v-else> <bento-typography el="div" class="b-data-grid-cell__cell-content" :class="computedCellContentClasses" data-testid="cell-content" > <slot :item="rowData" :row-index="rowIndex" :name="`item-${column.field}`"> <span :title="conditionalTitleAttribute(column, rowData[column.field])">{{ getRowData(rowData[column.field]) }}</span> </slot> </bento-typography> <div v-if="column.cellActions?.length > 0" class="b-data-grid-cell__cell-actions"> <data-grid-cell-actions v-if="(shouldShowCellActions || isTouchDevice) && (hasRowData || hasSlot(`item-${column.field}`))" ref="cellActionsRef" :cell-actions="column.cellActions" :column-key="column.field" :row-data-item="rowData" :has-zebra-background="hasZebraBackground" :is-highlighted="isHighlighted" :is-selected="isSelected" teleport-menu @menu-item-click="onMenuClose" /> </div> </template> </div> </template> <script setup lang="ts"> import { computed, type ComputedRef, inject, nextTick, onMounted, type PropType, provide, type Ref, ref, type UnwrapRef, useSlots, watch, } from 'vue'; import { focusOnInteractiveChild, makeAllInteractiveChildrenFocusable, makeAllInteractiveChildrenUnFocusable, } from '../../utils'; import { DATAGRID_ACTIONS_COLUMN_FIELD_NAME } from '../../composables/use-calculate-column-width'; import { BentoCheckbox } from '@/components/checkbox'; import { BentoTypography } from '@/components/typography'; import { BentoToggleButton } from '@/components/button'; import DataGridCellActions from '../data-grid-cell-actions/data-grid-cell-actions.vue'; import { DATA_GRID_SLASHED_ZERO, DATA_GRID_SMALL_CONTAINER, DATA_GRID_TOUCH_DEVICE } from '../../data-grid.keys'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import ChevronUpIcon from '@adyen/ui-assets-icons-16/vue/chevron-up'; import { DATA_GRID_CELL_MENU_REGISTERING_INJECTION_KEY } from '@/components/data-grid/components/data-grid-cell/data-grid-cell.keys'; // Types and enums import { type BentoColumn, BentoColumnOverflow, type BentoDatagridDataItem, type BentoDatagridDataItemId, type BentoDatagridGetDataItemIdFn, type BentoDatagridIsDataItemDisabledFn, } from '@/components/data-grid/data-grid.types'; import { type DataGridCellColumnSet, type DataGridCellDragEventsForColumn, type DataGridCellEvents, type DataGridCellEventsForColumn, DataGridCellPropClass, } from './data-grid-cell.types'; import type { BentoDatagridDraggingColumnState } from '../data-grid-columns/data-grid-columns.types'; import type { DatagridColumnHoveringState } from '@/components/data-grid/composables/use-column-hovering/use-column-hovering.types'; // Utils import { useHasSlot } from '@/composables'; import { useI18n } from '@/utils/ts/i18n'; import { isFocusVisible } from '@/utils/ts/focus-visible'; import { EventKey } from '@/types/utils-events'; // Translations import messages from './messages.json'; const slots = useSlots(); const hasSlot = useHasSlot(slots); type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const props = defineProps({ draggingEventsForRowCell: { type: Function as PropType<DataGridCellDragEventsForColumn>, required: true, }, /** * The columns sets including */ columnSets: { type: Array as PropType<Array<DataGridCellColumnSet>>, required: true, }, /** * The columns set we are iterating over */ column: { type: Object as PropType<BentoColumn>, required: true, }, colIndex: { type: Number, required: true, }, columnSetIndex: { type: Number, required: true, }, /** * Sets the row cells to the condensed padding state. */ condensed: { type: Boolean, default: false, }, /** * Columns being dragged, contains dragging column and a target column */ draggingColumns: { type: Object as PropType<BentoDatagridDraggingColumnState>, default: undefined, }, /** * A customisable method to retrieve a unique identifier for a data item in the array of data supplied. * This is used selecting and performance reasons and must exist in each data item. */ getDataItemId: { type: Function as PropType<BentoDatagridGetDataItemIdFn>, default: (item: BentoDatagridDataItem) => item.id, }, /** * Set to true if the cell has a darker background when zebra rows is enabled. */ hasZebraBackground: { type: Boolean, default: false, }, /** * Set to true if the row is selected. */ isSelected: { type: Boolean, default: false, }, /** * Set to true if the row is highlighted. */ isHighlighted: { type: Boolean, default: false, }, /** * The column hovered over state and whether the hovered over column should show a border right or left. */ hoveredOverColumnState: { type: Object as PropType<DatagridColumnHoveringState>, required: true, }, /** * A customisable method to determine if a row is not selectable. * The default is that every row is selectable. */ isDataItemDisabled: { type: Function as PropType<BentoDatagridIsDataItemDisabledFn>, default: () => false, }, isMeasuringColumnWidths: { type: Boolean, default: false, }, /** * Determines if the nested content is visible. */ isNestedContentExpanded: { type: Boolean, default: false, }, /** * Determines if the nest content slot is enabled for this data row. */ nestedContent: { type: Boolean, default: false, }, /** * The IDs of the rows this cell controls to show / hide them. */ nestedRowsElementIds: { type: Array as PropType<Array<string>>, default: () => [], }, /** * The row data object */ rowData: { type: Object as PropType<BentoDatagridDataItem>, required: true, }, rowIndex: { type: Number, required: true, }, /** * The row selection checkbox indeterminate state. */ selectionIsIndeterminate: { type: Boolean, default: false, }, /** * Determines if the row selection is enabled. */ selectable: { type: Boolean, default: false, }, selectedItems: { type: Array as PropType<Array<BentoDatagridDataItemId>>, required: true, }, }); const emit = defineEmits<{ /** * Emitted when an a frozen column cell fires a key up or down event. */ (e: 'on-frozen-columns-key-press', event: Event); /** * Emitted when a row has been selected. */ (e: 'select-row', value: Array<BentoDatagridDataItemId>); /** * Emitted when a row has been selected. */ (e: 'register-row-cell-element', el: Element, field: string, rowIndex: number); /** * Emitted when a cell has been hovered over. This is used to keep the row in a hovered state when hovering over a menu. */ (e: 'cell-hovered', value: number); /** * Emitted when nested content is being toggled. */ (e: 'nested-content-toggled'); /** * Emitted when a cell gains focus. */ ( e: 'cell-focus', value: { rowData: BentoDatagridDataItem; column: BentoColumn; rowIndex: number; colIndex: number } ); }>(); // References const gridcellRef = ref<HTMLDivElement | null>(null); const cellActionsRef = ref<InstanceType<typeof DataGridCellActions>>(null); const shouldShowCellActions = ref(false); const childrenMenuContainerElements = ref<Array<Ref<UnwrapRef<HTMLDivElement>>>>([]); const isFocused = ref(false); const focusableItems = ref<Array<{ node: HTMLElement }>>(); const hasEverBeenFocused = ref(false); // Computed const hasRowData = computed(() => !!props.rowData?.[props.column.field]); const controlledNestedRowElementIds = computed(() => props.column.field === DATAGRID_ACTIONS_COLUMN_FIELD_NAME && props.nestedRowsElementIds?.length > 0 ? props.nestedRowsElementIds.join(' ') : undefined ); const ariaHidden = computed( () => props.column.field === DATAGRID_ACTIONS_COLUMN_FIELD_NAME && !props.selectable && !props.nestedContent ); const tabIndex = computed(() => { if (!ariaHidden.value) { // The first cell of the grid should be tabbable on initial render to allow keyboard navigation into the grid. if (getCellIndex(props.rowIndex, props.colIndex, props.columnSetIndex) === 0 && !hasEverBeenFocused.value) { return '0'; } // A cell becomes part of the tab order only when it is focused. if (isFocused.value) { return '0'; } } // Action cells are not directly tabbable; they are accessed via cell navigation. if (props.column.field === DATAGRID_ACTIONS_COLUMN_FIELD_NAME) { return '-1'; } // By default, cells are not in the tab order. return '-1'; }); // Row data retrieval methods const conditionalTitleAttribute = (column: BentoColumn, content: unknown) => column?.overflow === BentoColumnOverflow.ELLIPSIS ? `${content}` : undefined; const getRowData = (rowData: unknown) => rowData ?? '-'; // Cell and Columns index calculation const sumColumnAmountsInColumnSets = (limitIndex?: number) => props.columnSets.slice(0, limitIndex).reduce((total, { columnSet }) => total + columnSet.length, 0); // Sums the amount of columns on every previous column set to find // the absolute column index (useful for frozen columns use case) const getColIndex = (colIndex: number, colSetIndex: number) => colIndex + sumColumnAmountsInColumnSets(colSetIndex); const getCellIndex = (rowIndex: number, colIndex: number, colSetIndex: number) => sumColumnAmountsInColumnSets() * rowIndex + getColIndex(colIndex, colSetIndex); /** * We check here if we are leaving the cell but mouse into a child menu * If this is the case we don't want to hide the cell actions. * @param relatedTarget */ const isLeavingCellFocus = (relatedTarget: EventTarget) => { if ( relatedTarget === null || (!gridcellRef.value.contains(relatedTarget as Node) && !childrenMenuContainerElements.value ?.map(registeredMenuRef => registeredMenuRef.value) .some(menuElement => menuElement === relatedTarget || menuElement?.contains(relatedTarget as Node))) ) { shouldShowCellActions.value = false; } }; /** * Hides the cell actions when a menu item is clicked. This is necessary as relying solely on * focus-out events is not reliable, especially when the menu is teleported or an action * causes focus to leave the window. */ const onMenuClose = () => { shouldShowCellActions.value = false; }; // Events const hasInteractiveElFocusedInside = ref(false); const getGridcellEvents: DataGridCellEventsForColumn = (column: BentoColumn) => { const defaultEvents: DataGridCellEvents = { ...props.draggingEventsForRowCell(column), // Focus on the first interactive child in the grid cell if one exists focus: async () => { if (!isFocusVisible()) { return; } isFocused.value = true; hasEverBeenFocused.value = true; emit('cell-focus', { rowData: props.rowData, column: props.column, rowIndex: props.rowIndex, colIndex: getColIndex(props.colIndex, props.columnSetIndex), }); makeAllInteractiveChildrenFocusable(focusableItems.value); await nextTick(); const hasFocusedOnInteractiveChild = focusOnInteractiveChild(gridcellRef.value); if (hasFocusedOnInteractiveChild) { hasInteractiveElFocusedInside.value = true; } }, focusout: () => { if (!isFocusVisible()) { return; } hasInteractiveElFocusedInside.value = false; }, keydown: (event: KeyboardEvent) => { if (event.key !== EventKey.TAB) { hasInteractiveElFocusedInside.value = false; isFocused.value = false; makeAllInteractiveChildrenUnFocusable(gridcellRef.value); } }, }; const gridCellEvents: DataGridCellEvents = { ...defaultEvents }; if (column.frozen) { gridCellEvents.keyup = (event: Event) => emit('on-frozen-columns-key-press', event); gridCellEvents.keydown = (event: KeyboardEvent) => { defaultEvents.keydown(event); emit('on-frozen-columns-key-press', event); }; } if (column.cellActions) { gridCellEvents.mouseenter = () => { shouldShowCellActions.value = true; }; gridCellEvents.mouseleave = (e: MouseEvent) => { isLeavingCellFocus(e?.relatedTarget); }; gridCellEvents.focus = async (e: FocusEvent) => { defaultEvents.focus(e); await nextTick(); shouldShowCellActions.value = true; }; gridCellEvents.focusout = (e: FocusEvent) => { defaultEvents.focusout(e); isLeavingCellFocus(e?.relatedTarget); }; } return gridCellEvents; }; onMounted(() => { focusableItems.value = makeAllInteractiveChildrenUnFocusable(gridcellRef.value); }); const toggleNestedContent = () => { emit('nested-content-toggled'); }; // Each cell knows it's on hover state but this state needs to be emitted up // in order for the DataGrid to know which row to set the hovered state on. watch( () => shouldShowCellActions.value, () => { if (shouldShowCellActions.value === true) { emit('cell-hovered', props.rowIndex); } else { emit('cell-hovered', -1); } } ); const registerMenuContainerElement = (menuContainerElement: Ref<UnwrapRef<HTMLDivElement>>) => { childrenMenuContainerElements.value.push(menuContainerElement); }; const menuMouseLeave = (e: MouseEvent) => { if (!gridcellRef.value.contains(e?.relatedTarget as Node)) { shouldShowCellActions.value = false; } }; provide<{ registerMenuContainerElement: (menuContainerElement: Ref<UnwrapRef<HTMLDivElement>>) => void; menuMouseLeave: (e: MouseEvent) => void; }>(DATA_GRID_CELL_MENU_REGISTERING_INJECTION_KEY, { registerMenuContainerElement, menuMouseLeave, }); const hasSlashedZero = inject(DATA_GRID_SLASHED_ZERO, ref(false)); const isSmallContainer: ComputedRef<boolean> = inject( DATA_GRID_SMALL_CONTAINER, computed(() => false) ); const isTouchDevice: Ref<boolean> = inject(DATA_GRID_TOUCH_DEVICE, ref(false)); // Styling const conditionalRowCellClasses = (column: BentoColumn) => ({ [`b-data-grid-cell--${DataGridCellPropClass.COLUMN_HOVERED}`]: column?.field === props.hoveredOverColumnState.hoveredOverColumnField, [`b-data-grid-cell--${DataGridCellPropClass.COLUMN_HOVERED_NO_BORDER_LEFT}`]: column?.field === props.hoveredOverColumnState.hoveredOverColumnField && props.hoveredOverColumnState.noBorderLeft, [`b-data-grid-cell--${DataGridCellPropClass.COLUMN_HOVERED_NO_BORDER_RIGHT}`]: column?.field === props.hoveredOverColumnState.hoveredOverColumnField && props.hoveredOverColumnState.noBorderRight, [`b-data-grid-cell--${DataGridCellPropClass.PADDING_LEFT_UNSET}`]: column?.padding?.left === false, [`b-data-grid-cell--${DataGridCellPropClass.PADDING_RIGHT_UNSET}`]: column?.padding?.right === false, [`b-data-grid-cell--${DataGridCellPropClass.NUMERIC}`]: column?.numeric, [`b-data-grid-cell--${DataGridCellPropClass.CONDENSED}`]: props.condensed, [`b-data-grid-cell--${DataGridCellPropClass.OVERFLOW_ELLIPSIS}`]: column?.overflow === BentoColumnOverflow.ELLIPSIS, [`b-data-grid-cell--${DataGridCellPropClass.IS_COLUMN_REORDERING}`]: props.draggingColumns.dragging?.field === column.field, [`b-data-grid-cell--${DataGridCellPropClass.IS_COLUMN_REORDERING_TARGET_LEFT}`]: props.draggingColumns.target?.field === column.field && props.draggingColumns?.dragToLeft, [`b-data-grid-cell--${DataGridCellPropClass.IS_COLUMN_REORDERING_TARGET_RIGHT}`]: props.draggingColumns.target?.field === column.field && !props.draggingColumns?.dragToLeft, [`b-data-grid-cell--${DataGridCellPropClass.FROZEN}`]: column?.frozen, [`b-data-grid-cell--${DataGridCellPropClass.NESTED_CONTENT_ENABLED}`]: props.nestedContent, [`b-data-grid-cell--${DataGridCellPropClass.ACTION_COL_NESTED_CONTENT_ENABLED}`]: props.nestedContent && !props.selectable && column.field === DATAGRID_ACTIONS_COLUMN_FIELD_NAME, [`b-data-grid-cell--${DataGridCellPropClass.ACTION_COL_NESTED_CONTENT_AND_SELECTION_ENABLED}`]: props.nestedContent && props.selectable && column.field === DATAGRID_ACTIONS_COLUMN_FIELD_NAME, [`b-data-grid-cell--${DataGridCellPropClass.ACTION_COL_SELECTION_ENABLED}`]: column.field === DATAGRID_ACTIONS_COLUMN_FIELD_NAME && props.selectable, [`b-data-grid-cell--${DataGridCellPropClass.SLASHED_ZERO}`]: hasSlashedZero.value, 'b-data-grid-cell--small-container': isSmallContainer.value, }); const computedCellContentClasses = computed(() => ({ 'b-data-grid-cell__cell-content--is-focused-on-inside-interactive-element': hasInteractiveElFocusedInside.value, })); // Exposed refs defineExpose({ gridcellRef, }); </script> <script lang="ts"> /** * Internal component to display the BentoDataGrid cells. * * The component can also render the selecting row checkbox as well as cell actions. * * @example * <data-grid-cell * * /> */ export default { i18n: { messages }, name: 'data-grid-cell', }; </script> <style lang="scss" scoped src="./data-grid-cell.scss" />
1
+ <template> <div :key="`cell-${column.field}`" ref="gridcellRef" :aria-hidden="ariaHidden" :data-testid="`cell-${column.field}`" :data-cellindex="getCellIndex(rowIndex, colIndex, columnSetIndex)" class="b-data-grid-cell" :class="conditionalRowCellClasses(column)" role="gridcell" :tabindex="tabIndex" :aria-colindex="getColIndex(colIndex, columnSetIndex) + 1" :style="{ width: column.autoWidth && isMeasuringColumnWidths ? 'auto' : `${column.width}px`, }" v-on="getGridcellEvents(column)" > <template v-if="column.field === DATAGRID_ACTIONS_COLUMN_FIELD_NAME"> <template v-if="selectable"> <bento-checkbox class="b-data-grid-cell__select-cell" :aria-label="t('selectRow')" :disabled="isDataItemDisabled(rowData)" :indeterminate="selectionIsIndeterminate" :model-value="selectedItems" :value="getDataItemId(rowData)" :tabindex="isFocused ? 0 : -1" @update:model-value="$emit('select-row', $event)" /> </template> <template v-if="nestedContent"> <bento-toggle-button ref="menuButtonRef" aria-haspopup="true" :aria-controls="controlledNestedRowElementIds" condensed variant="tertiary" :toggled="isNestedContentExpanded" @click="toggleNestedContent" > <template #iconLeft> <chevron-up-icon v-if="isNestedContentExpanded" class="b-accordion-item__icon" :svg-title="t('collapseNestedContent')" /> <chevron-down-icon v-else class="b-accordion-item__icon" :svg-title="t('expandNestedContent')" /> </template> </bento-toggle-button> </template> </template> <template v-else> <bento-typography el="div" class="b-data-grid-cell__cell-content" :class="computedCellContentClasses" data-testid="cell-content" > <slot :item="rowData" :row-index="rowIndex" :name="`item-${column.field}`"> <span :title="conditionalTitleAttribute(column, rowData[column.field])">{{ getRowData(rowData[column.field]) }}</span> </slot> </bento-typography> <div v-if="column.cellActions?.length > 0" class="b-data-grid-cell__cell-actions"> <data-grid-cell-actions v-if="(shouldShowCellActions || isTouchDevice) && (hasRowData || hasSlot(`item-${column.field}`))" ref="cellActionsRef" :cell-actions="column.cellActions" :column-key="column.field" :row-data-item="rowData" :has-zebra-background="hasZebraBackground" :is-highlighted="isHighlighted" :is-selected="isSelected" teleport-menu @menu-item-click="onMenuClose" /> </div> </template> </div> </template> <script setup lang="ts"> import { computed, type ComputedRef, inject, nextTick, onMounted, type PropType, provide, type Ref, ref, type UnwrapRef, useSlots, watch, } from 'vue'; import { makeAllInteractiveChildrenFocusable, makeAllInteractiveChildrenUnFocusable } from '../../utils'; import { useCellKeyboardNavigation } from './composables/use-cell-keyboard-navigation'; import { DATAGRID_ACTIONS_COLUMN_FIELD_NAME } from '../../composables/use-calculate-column-width'; import { BentoCheckbox } from '@/components/checkbox'; import { BentoTypography } from '@/components/typography'; import { BentoToggleButton } from '@/components/button'; import DataGridCellActions from '../data-grid-cell-actions/data-grid-cell-actions.vue'; import { DATA_GRID_SLASHED_ZERO, DATA_GRID_SMALL_CONTAINER, DATA_GRID_TOUCH_DEVICE } from '../../data-grid.keys'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import ChevronUpIcon from '@adyen/ui-assets-icons-16/vue/chevron-up'; import { DATA_GRID_CELL_MENU_REGISTERING_INJECTION_KEY } from '@/components/data-grid/components/data-grid-cell/data-grid-cell.keys'; // Types and enums import { type BentoColumn, BentoColumnOverflow, type BentoDatagridDataItem, type BentoDatagridDataItemId, type BentoDatagridGetDataItemIdFn, type BentoDatagridIsDataItemDisabledFn, } from '@/components/data-grid/data-grid.types'; import { type DataGridCellColumnSet, type DataGridCellDragEventsForColumn, type DataGridCellEvents, type DataGridCellEventsForColumn, DataGridCellPropClass, } from './data-grid-cell.types'; import type { BentoDatagridDraggingColumnState } from '../data-grid-columns/data-grid-columns.types'; import type { DatagridColumnHoveringState } from '@/components/data-grid/composables/use-column-hovering/use-column-hovering.types'; // Utils import { useHasSlot } from '@/composables'; import { useI18n } from '@/utils/ts/i18n'; // Translations import messages from './messages.json'; const slots = useSlots(); const hasSlot = useHasSlot(slots); type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const props = defineProps({ draggingEventsForRowCell: { type: Function as PropType<DataGridCellDragEventsForColumn>, required: true, }, /** * The columns sets including */ columnSets: { type: Array as PropType<Array<DataGridCellColumnSet>>, required: true, }, /** * The columns set we are iterating over */ column: { type: Object as PropType<BentoColumn>, required: true, }, colIndex: { type: Number, required: true, }, columnSetIndex: { type: Number, required: true, }, /** * Sets the row cells to the condensed padding state. */ condensed: { type: Boolean, default: false, }, /** * Columns being dragged, contains dragging column and a target column */ draggingColumns: { type: Object as PropType<BentoDatagridDraggingColumnState>, default: undefined, }, /** * A customisable method to retrieve a unique identifier for a data item in the array of data supplied. * This is used selecting and performance reasons and must exist in each data item. */ getDataItemId: { type: Function as PropType<BentoDatagridGetDataItemIdFn>, default: (item: BentoDatagridDataItem) => item.id, }, /** * Set to true if the cell has a darker background when zebra rows is enabled. */ hasZebraBackground: { type: Boolean, default: false, }, /** * Set to true if the row is selected. */ isSelected: { type: Boolean, default: false, }, /** * Set to true if the row is highlighted. */ isHighlighted: { type: Boolean, default: false, }, /** * The column hovered over state and whether the hovered over column should show a border right or left. */ hoveredOverColumnState: { type: Object as PropType<DatagridColumnHoveringState>, required: true, }, /** * A customisable method to determine if a row is not selectable. * The default is that every row is selectable. */ isDataItemDisabled: { type: Function as PropType<BentoDatagridIsDataItemDisabledFn>, default: () => false, }, isMeasuringColumnWidths: { type: Boolean, default: false, }, /** * Determines if the nested content is visible. */ isNestedContentExpanded: { type: Boolean, default: false, }, /** * Determines if the nest content slot is enabled for this data row. */ nestedContent: { type: Boolean, default: false, }, /** * The IDs of the rows this cell controls to show / hide them. */ nestedRowsElementIds: { type: Array as PropType<Array<string>>, default: () => [], }, /** * The row data object */ rowData: { type: Object as PropType<BentoDatagridDataItem>, required: true, }, rowIndex: { type: Number, required: true, }, /** * The row selection checkbox indeterminate state. */ selectionIsIndeterminate: { type: Boolean, default: false, }, /** * Determines if the row selection is enabled. */ selectable: { type: Boolean, default: false, }, selectedItems: { type: Array as PropType<Array<BentoDatagridDataItemId>>, required: true, }, }); const emit = defineEmits<{ /** * Emitted when an a frozen column cell fires a key up or down event. */ (e: 'on-frozen-columns-key-press', event: Event); /** * Emitted when a row has been selected. */ (e: 'select-row', value: Array<BentoDatagridDataItemId>); /** * Emitted when a row has been selected. */ (e: 'register-row-cell-element', el: Element, field: string, rowIndex: number); /** * Emitted when a cell has been hovered over. This is used to keep the row in a hovered state when hovering over a menu. */ (e: 'cell-hovered', value: number); /** * Emitted when nested content is being toggled. */ (e: 'nested-content-toggled'); /** * Emitted when a cell gains focus. */ ( e: 'cell-focus', value: { rowData: BentoDatagridDataItem; column: BentoColumn; rowIndex: number; colIndex: number } ); }>(); // References const gridcellRef = ref<HTMLDivElement | null>(null); const cellActionsRef = ref<InstanceType<typeof DataGridCellActions>>(null); const hasEverBeenFocused = ref(false); const { shouldShowCellActions, isFocused, focusableItems, childrenMenuContainerElements, hasInteractiveElFocusedInside, onMenuClose, hideCellActionsIfLeft, handleFocus, handleFocusout, handleKeydown, handleCellActionsFocus, handleCellActionsFocusout, } = useCellKeyboardNavigation({ gridcellRef }); // Computed const hasRowData = computed(() => !!props.rowData?.[props.column.field]); const controlledNestedRowElementIds = computed(() => props.column.field === DATAGRID_ACTIONS_COLUMN_FIELD_NAME && props.nestedRowsElementIds?.length > 0 ? props.nestedRowsElementIds.join(' ') : undefined ); const ariaHidden = computed( () => props.column.field === DATAGRID_ACTIONS_COLUMN_FIELD_NAME && !props.selectable && !props.nestedContent ); const tabIndex = computed(() => { if (!ariaHidden.value) { // The first cell of the grid should be tabbable on initial render to allow keyboard navigation into the grid. if (getCellIndex(props.rowIndex, props.colIndex, props.columnSetIndex) === 0 && !hasEverBeenFocused.value) { return '0'; } // A cell becomes part of the tab order only when it is focused. if (isFocused.value) { return '0'; } } // Action cells are not directly tabbable; they are accessed via cell navigation. if (props.column.field === DATAGRID_ACTIONS_COLUMN_FIELD_NAME) { return '-1'; } // By default, cells are not in the tab order. return '-1'; }); // Row data retrieval methods const conditionalTitleAttribute = (column: BentoColumn, content: unknown) => column?.overflow === BentoColumnOverflow.ELLIPSIS ? `${content}` : undefined; const getRowData = (rowData: unknown) => rowData ?? '-'; // Cell and Columns index calculation const sumColumnAmountsInColumnSets = (limitIndex?: number) => props.columnSets.slice(0, limitIndex).reduce((total, { columnSet }) => total + columnSet.length, 0); // Sums the amount of columns on every previous column set to find // the absolute column index (useful for frozen columns use case) const getColIndex = (colIndex: number, colSetIndex: number) => colIndex + sumColumnAmountsInColumnSets(colSetIndex); const getCellIndex = (rowIndex: number, colIndex: number, colSetIndex: number) => sumColumnAmountsInColumnSets() * rowIndex + getColIndex(colIndex, colSetIndex); // Events const getGridcellEvents: DataGridCellEventsForColumn = (column: BentoColumn) => { const defaultEvents: DataGridCellEvents = { ...props.draggingEventsForRowCell(column), focus: async () => { hasEverBeenFocused.value = true; await handleFocus(() => { emit('cell-focus', { rowData: props.rowData, column: props.column, rowIndex: props.rowIndex, colIndex: getColIndex(props.colIndex, props.columnSetIndex), }); }); }, focusout: handleFocusout, keydown: handleKeydown, }; const gridCellEvents: DataGridCellEvents = { ...defaultEvents }; if (column.frozen) { gridCellEvents.keyup = (event: Event) => emit('on-frozen-columns-key-press', event); gridCellEvents.keydown = (event: KeyboardEvent) => { defaultEvents.keydown(event); emit('on-frozen-columns-key-press', event); }; } if (column.cellActions) { gridCellEvents.mouseenter = () => { shouldShowCellActions.value = true; }; gridCellEvents.mouseleave = (e: MouseEvent) => { hideCellActionsIfLeft(e?.relatedTarget); }; gridCellEvents.focus = async (e: FocusEvent) => { await handleCellActionsFocus(defaultEvents.focus, e); }; gridCellEvents.focusout = (e: FocusEvent) => { handleCellActionsFocusout(defaultEvents.focusout, e); }; } return gridCellEvents; }; onMounted(() => { focusableItems.value = makeAllInteractiveChildrenUnFocusable(gridcellRef.value); }); const toggleNestedContent = () => { emit('nested-content-toggled'); }; // Each cell knows it's on hover state but this state needs to be emitted up // in order for the DataGrid to know which row to set the hovered state on. watch( () => shouldShowCellActions.value, async () => { if (shouldShowCellActions.value === true) { emit('cell-hovered', props.rowIndex); // Re-scan for focusable items after cell-actions render into the DOM await nextTick(); focusableItems.value = makeAllInteractiveChildrenUnFocusable(gridcellRef.value); // When the cell has keyboard focus, make children tabbable so Tab // can reach cell-action buttons that just appeared. if (isFocused.value) { makeAllInteractiveChildrenFocusable(focusableItems.value); } } else { emit('cell-hovered', -1); } } ); const registerMenuContainerElement = (menuContainerElement: Ref<UnwrapRef<HTMLDivElement>>) => { childrenMenuContainerElements.value.push(menuContainerElement); }; const menuMouseLeave = (e: MouseEvent) => { if (!gridcellRef.value.contains(e?.relatedTarget as Node)) { shouldShowCellActions.value = false; } }; provide<{ registerMenuContainerElement: (menuContainerElement: Ref<UnwrapRef<HTMLDivElement>>) => void; menuMouseLeave: (e: MouseEvent) => void; }>(DATA_GRID_CELL_MENU_REGISTERING_INJECTION_KEY, { registerMenuContainerElement, menuMouseLeave, }); const hasSlashedZero = inject(DATA_GRID_SLASHED_ZERO, ref(false)); const isSmallContainer: ComputedRef<boolean> = inject( DATA_GRID_SMALL_CONTAINER, computed(() => false) ); const isTouchDevice: Ref<boolean> = inject(DATA_GRID_TOUCH_DEVICE, ref(false)); // Styling const conditionalRowCellClasses = (column: BentoColumn) => ({ [`b-data-grid-cell--${DataGridCellPropClass.COLUMN_HOVERED}`]: column?.field === props.hoveredOverColumnState.hoveredOverColumnField, [`b-data-grid-cell--${DataGridCellPropClass.COLUMN_HOVERED_NO_BORDER_LEFT}`]: column?.field === props.hoveredOverColumnState.hoveredOverColumnField && props.hoveredOverColumnState.noBorderLeft, [`b-data-grid-cell--${DataGridCellPropClass.COLUMN_HOVERED_NO_BORDER_RIGHT}`]: column?.field === props.hoveredOverColumnState.hoveredOverColumnField && props.hoveredOverColumnState.noBorderRight, [`b-data-grid-cell--${DataGridCellPropClass.PADDING_LEFT_UNSET}`]: column?.padding?.left === false, [`b-data-grid-cell--${DataGridCellPropClass.PADDING_RIGHT_UNSET}`]: column?.padding?.right === false, [`b-data-grid-cell--${DataGridCellPropClass.NUMERIC}`]: column?.numeric, [`b-data-grid-cell--${DataGridCellPropClass.CONDENSED}`]: props.condensed, [`b-data-grid-cell--${DataGridCellPropClass.OVERFLOW_ELLIPSIS}`]: column?.overflow === BentoColumnOverflow.ELLIPSIS, [`b-data-grid-cell--${DataGridCellPropClass.IS_COLUMN_REORDERING}`]: props.draggingColumns.dragging?.field === column.field, [`b-data-grid-cell--${DataGridCellPropClass.IS_COLUMN_REORDERING_TARGET_LEFT}`]: props.draggingColumns.target?.field === column.field && props.draggingColumns?.dragToLeft, [`b-data-grid-cell--${DataGridCellPropClass.IS_COLUMN_REORDERING_TARGET_RIGHT}`]: props.draggingColumns.target?.field === column.field && !props.draggingColumns?.dragToLeft, [`b-data-grid-cell--${DataGridCellPropClass.FROZEN}`]: column?.frozen, [`b-data-grid-cell--${DataGridCellPropClass.NESTED_CONTENT_ENABLED}`]: props.nestedContent, [`b-data-grid-cell--${DataGridCellPropClass.ACTION_COL_NESTED_CONTENT_ENABLED}`]: props.nestedContent && !props.selectable && column.field === DATAGRID_ACTIONS_COLUMN_FIELD_NAME, [`b-data-grid-cell--${DataGridCellPropClass.ACTION_COL_NESTED_CONTENT_AND_SELECTION_ENABLED}`]: props.nestedContent && props.selectable && column.field === DATAGRID_ACTIONS_COLUMN_FIELD_NAME, [`b-data-grid-cell--${DataGridCellPropClass.ACTION_COL_SELECTION_ENABLED}`]: column.field === DATAGRID_ACTIONS_COLUMN_FIELD_NAME && props.selectable, [`b-data-grid-cell--${DataGridCellPropClass.SLASHED_ZERO}`]: hasSlashedZero.value, 'b-data-grid-cell--small-container': isSmallContainer.value, }); const computedCellContentClasses = computed(() => ({ 'b-data-grid-cell__cell-content--is-focused-on-inside-interactive-element': hasInteractiveElFocusedInside.value, })); // Exposed refs defineExpose({ gridcellRef, }); </script> <script lang="ts"> /** * Internal component to display the BentoDataGrid cells. * * The component can also render the selecting row checkbox as well as cell actions. * * @example * <data-grid-cell * * /> */ export default { i18n: { messages }, name: 'data-grid-cell', }; </script> <style lang="scss" scoped src="./data-grid-cell.scss" />
@@ -1 +1 @@
1
- <template> <Transition name="b-data-grid-cell-actions__animation" appear> <div class="b-data-grid-cell-actions"> <div v-if="showGradientElement && !isTouchDevice" class="b-data-grid-cell-actions__background-gradient" :class="conditionalCellActionsBackgroundGradientClasses" ></div> <div class="b-data-grid-cell-actions__actions" :class="conditionalCellActionsClasses" role="group"> <div v-for="(button, index) in computedCellActions" :key="index" class="b-data-grid-cell-actions__action" > <bento-menu v-if="button.menuData" v-bento-tooltip-directive:[button.tooltipPosition]="button.tooltipText" :data="transformMenuEvents(button.menuData)" :button="{ condensed: true, variant: 'secondary', disabled: button.disabled, }" :disable-arrow-down-opening-menu="disableArrowDownOpeningMenu" :teleport="teleportMenu" @open="emit('menu-open')" @close="emit('menu-close')" @menu-item-click="emit('menu-item-click')" > <template v-if="shouldShowMenuIconLeft(button)" #iconLeft> <options-vertical-icon v-if="button?.variant === 'more'" :svg-title="getButtonTitle(button, true)" /> <component :is="button.iconLeft" v-else :svg-title="getButtonTitle(button, true)" /> </template> <template v-if="!button?.isShort" #default> {{ getButtonTitle(button) }} </template> </bento-menu> <copy v-else-if="button.variant === 'copy'" :text="getCopyText(button)" :button-text="getButtonTitle(button)" :button="{ condensed: true, variant: 'secondary', }" /> <bento-button v-else v-bento-tooltip-directive:[button.tooltipPosition]="button.tooltipText" condensed variant="secondary" :critical="button.critical" :disabled="button.disabled" :link="button.link" v-on="generateButtonEvents(button)" > <template v-if="button.iconLeft || button.variant === 'edit'" #iconLeft> <edit1-icon v-if="button?.variant === 'edit'" :svg-title="getButtonTitle(button, true)" /> <component :is="button.iconLeft" v-else :svg-title="button.title" /> </template> <template v-if="!button?.isShort" #default> {{ getButtonTitle(button) }} </template> <template v-if="button.iconRight || button.variant === 'open'" #iconRight> <external-link-icon v-if="button?.variant === 'open'" :svg-title="getButtonTitle(button, true)" /> <component :is="button.iconRight" v-else :svg-title="button.title" /> </template> </bento-button> </div> </div> </div> </Transition> </template> <script setup lang="ts"> import { computed, inject, type PropType, ref, type Ref } from 'vue'; import { BentoButton } from '@/components/button'; import { BentoMenu, type BentoMenuItem } from '@/components/menu'; import { Copy } from '@/internal/copy'; import { BentoTooltipDirective as vBentoTooltipDirective } from '@/directives/tooltip'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { type BentoDatagridDataItem } from '@/components/data-grid/data-grid.types'; import { DATA_GRID_TOUCH_DEVICE } from '@/components/data-grid/data-grid.keys'; import Edit1Icon from '@adyen/ui-assets-icons-16/vue/edit-1'; import ExternalLinkIcon from '@adyen/ui-assets-icons-16/vue/external-link'; import OptionsVerticalIcon from '@adyen/ui-assets-icons-16/vue/options-vertical'; // Types import type { BentoColumnCellAction, BentoColumnCellActionMenuItem, BentoColumnCellActions, BentoColumnCellActionsByCellDataFn, BentoColumnCellActionsProp, BentoDataGridRowActionsProp, } from './data-grid-cell-actions.types'; // Translations import messages from './messages.json'; import { useI18n } from '@/utils/ts/i18n'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const props = defineProps({ /** * Column definitions using 'key' to relate to data object. */ cellActions: { type: [Array, Function] as PropType<BentoColumnCellActionsProp | BentoDataGridRowActionsProp>, required: true, validator: (value: BentoColumnCellActions) => value?.length <= 2, }, /** * The column field key. */ columnKey: { type: String, default: undefined, }, /** * Disables any opening of the cell actions menus using the down arrow key. */ disableArrowDownOpeningMenu: { type: Boolean, default: false, }, /** * Set to true if the datagrid has been scrolled and the right side has overflowing space. */ hasRightOverflow: { type: Boolean, default: false, }, /** * Set to true if the row has a darker background when zebra rows is enabled. */ hasZebraBackground: { type: Boolean, default: false, }, /** * Set to true if the row is selected. */ isSelected: { type: Boolean, default: false, }, /** * Set to true if the row is highlighted. */ isHighlighted: { type: Boolean, default: false, }, /** * Set if set to true will teleport the action menu component */ teleportMenu: { type: Boolean, default: false, }, /** * The row data item object. */ rowDataItem: { type: Object as PropType<BentoDatagridDataItem>, required: true, }, }); const emit = defineEmits<{ /** * Emits when a cell actions menu is opened */ (e: 'menu-open'): void; /** * Emits when a cell actions menu is closed */ (e: 'menu-close'): void; /** * Emits when a cell actions menu button closes due to a menu item click */ (e: 'menu-item-click'): void; }>(); const isTouchDevice: Ref<boolean> = inject(DATA_GRID_TOUCH_DEVICE, ref(false)); // Helper to get text for menu items, always returning a string const getMenuItemText = (action: BentoColumnCellAction | BentoColumnCellActionMenuItem) => { // If it's already a menu item with a text property, use it if ('text' in action && action.text !== undefined) { return action.text; } // Otherwise, derive from variant or title switch ((action as BentoColumnCellAction).variant) { case 'copy': return t('copy'); case 'edit': return t('edit'); case 'open': return t('open'); case 'more': return t('more'); default: return (action as BentoColumnCellAction).title; } }; const getMenuItemIcon = (action: BentoColumnCellAction) => { switch (action.variant) { case 'copy': return Copy; case 'edit': return Edit1Icon; case 'open': return ExternalLinkIcon; default: return undefined; } }; const isRowActions = computed(() => props.columnKey === undefined); const conditionalCellActionsBackgroundGradientClasses = computed(() => ({ 'b-data-grid-cell-actions__background-gradient--with-row-actions-and-overflow': isRowActions.value && props.hasRightOverflow, 'b-data-grid-cell-actions__background-gradient--zebra': props.hasZebraBackground, 'b-data-grid-cell-actions__background-gradient--highlighted': props.isHighlighted, 'b-data-grid-cell-actions__background-gradient--selected': props.isSelected, })); const conditionalCellActionsClasses = computed(() => ({ 'b-data-grid-cell-actions__actions--with-row-actions': isRowActions.value, 'b-data-grid-cell-actions__actions--zebra': props.hasZebraBackground, 'b-data-grid-cell-actions__actions--highlighted': props.isHighlighted, 'b-data-grid-cell-actions__actions--selected': props.isSelected, })); const showGradientElement = computed(() => !isRowActions.value || (isRowActions.value && props.hasRightOverflow)); /** * Checks if the passed object is a cell actions function that returns an cell actions array. * @param cellActions - a cell actions array or function */ function isBentoColumnCellActionsByCellValueDataFn( cellActions: BentoColumnCellActionsProp ): cellActions is BentoColumnCellActionsByCellDataFn { return typeof cellActions === 'function'; } const computedCellActions = computed(() => { const cellActions = isBentoColumnCellActionsByCellValueDataFn(props.cellActions) ? props.cellActions?.(props.rowDataItem, props.columnKey) : props.cellActions; if (isTouchDevice.value && !isRowActions.value && cellActions) { const flattenedMenuItems: Array<BentoColumnCellActionMenuItem> = []; cellActions.forEach(action => { if (action.menuData) { action.menuData.forEach(subMenuItem => { flattenedMenuItems.push(subMenuItem); }); } else { flattenedMenuItems.push({ text: getMenuItemText(action), event: action.event, critical: action.critical, disabled: action.disabled, icon: getMenuItemIcon(action), }); } }); return [ { variant: 'more', isShort: true, iconLeft: OptionsVerticalIcon, menuData: transformMenuEvents(flattenedMenuItems), }, ] as BentoColumnCellActions; } // Do not allow users to provide more than 2 cell action buttons if (!isTouchDevice.value && cellActions?.length > 2) { printDevelopmentWarning( 'WARNING: BentoDataGrid Cell actions can only have a maximum of 2 actions, if you need more consider using the menuData property instead.' ); } return cellActions?.slice(0, 2); }); const getButtonTitle = computed(() => (button: BentoColumnCellAction, isForSvgTitle?: boolean) => { if ((!isForSvgTitle && button.isShort) || (isForSvgTitle && !button.isShort)) { return undefined; } switch (button.variant) { case 'copy': return t('copy'); case 'edit': return t('edit'); case 'open': return t('open'); case 'more': return t('more'); default: return button.title; } }); const generateButtonEvents = (button: BentoColumnCellAction) => button?.event !== undefined ? { click: (event: MouseEvent) => { event.stopPropagation(); button.event(props.rowDataItem, props.columnKey); }, } : {}; const generateMenuEvents = (menuDataItem: BentoColumnCellActionMenuItem): BentoMenuItem => ({ ...menuDataItem, handler: () => { if (menuDataItem?.handler && typeof menuDataItem.handler === 'function') { menuDataItem?.handler(); } if (menuDataItem?.event && typeof menuDataItem.event === 'function') { menuDataItem?.event(props.rowDataItem, props.columnKey); } }, }); const transformMenuEvents = (menuData: Array<BentoColumnCellActionMenuItem>) => menuData.map(menuDataItem => { const transformedMenuData = menuDataItem; if (transformedMenuData?.items) { transformedMenuData.items = transformMenuEvents(transformedMenuData.items); } return { ...generateMenuEvents(transformedMenuData) }; }); const getCopyText = (button: BentoColumnCellAction) => button?.getCopyData !== undefined ? button.getCopyData(props.rowDataItem, props.columnKey) : props.columnKey ? (props.rowDataItem?.[props.columnKey]?.toString() as string) : ''; const shouldShowMenuIconLeft = (button: BentoColumnCellAction) => button.iconLeft || button?.variant === 'more'; </script> <script lang="ts"> /** * Internal component to display the data-grid actions. * There are 4 different pre-defined buttons that can be used by using the `variant` property. * * The component renders buttons or a menu depending on the actions provided. * * @example * <data-grid-cell-actions * :cell-actions="[ * { variant: 'edit', event: () => {} }, * { * variant: 'more', * isShort: true, * menuData: [ * { * text: 'Cancel payment', * handler: () => {}), * critical: true, * }, * { * text: 'Transfer payment', * handler: () => {}, * }, * { * text: 'Delete payment', * handler: () => {}, * disabled: true, * }, * ], * }, * ]" * column-key="payment" * :row-data-item="rowData" * /> */ export default { i18n: { messages }, name: 'data-grid-cell-actions', }; </script> <style lang="scss" scoped src="./data-grid-cell-actions.scss" />
1
+ <template> <Transition name="b-data-grid-cell-actions__animation" appear> <div class="b-data-grid-cell-actions"> <div v-if="showGradientElement && !isTouchDevice" class="b-data-grid-cell-actions__background-gradient" :class="conditionalCellActionsBackgroundGradientClasses" ></div> <div class="b-data-grid-cell-actions__actions" :class="conditionalCellActionsClasses" role="group"> <div v-for="(button, index) in computedCellActions" :key="index" class="b-data-grid-cell-actions__action" > <bento-menu v-if="button.menuData" v-bento-tooltip-directive:[button.tooltipPosition]="button.tooltipText" :data="transformMenuEvents(button.menuData)" :button="{ condensed: true, variant: 'secondary', disabled: button.disabled, }" :disable-arrow-down-opening-menu="disableArrowDownOpeningMenu" :teleport="teleportMenu" @open="emit('menu-open')" @close="emit('menu-close')" @menu-item-click="emit('menu-item-click')" > <template v-if="shouldShowMenuIconLeft(button)" #iconLeft> <options-vertical-icon v-if="button?.variant === 'more'" :svg-title="getButtonTitle(button, true)" /> <component :is="button.iconLeft" v-else :svg-title="getButtonTitle(button, true)" /> </template> <template v-if="!button?.isShort" #default> {{ getButtonTitle(button) }} </template> </bento-menu> <copy v-else-if="button.variant === 'copy'" :text="getCopyText(button)" :button-text="getButtonTitle(button)" :button="{ condensed: true, variant: 'secondary', }" /> <bento-button v-else v-bento-tooltip-directive:[button.tooltipPosition]="button.tooltipText" condensed variant="secondary" :critical="button.critical" :disabled="button.disabled" :link="button.link" v-on="generateButtonEvents(button)" > <template v-if="button.iconLeft || button.variant === 'edit'" #iconLeft> <edit1-icon v-if="button?.variant === 'edit'" :svg-title="getButtonTitle(button, true)" /> <component :is="button.iconLeft" v-else :svg-title="button.title" /> </template> <template v-if="!button?.isShort" #default> {{ getButtonTitle(button) }} </template> <template v-if="button.iconRight || button.variant === 'open'" #iconRight> <external-link-icon v-if="button?.variant === 'open'" :svg-title="getButtonTitle(button, true)" /> <component :is="button.iconRight" v-else :svg-title="button.title" /> </template> </bento-button> </div> </div> </div> </Transition> </template> <script setup lang="ts"> import { computed, inject, type PropType, ref, type Ref } from 'vue'; import { BentoButton } from '@/components/button'; import { BentoMenu, type BentoMenuItem } from '@/components/menu'; import { Copy } from '@/internal/copy'; import { BentoTooltipDirective as vBentoTooltipDirective } from '@/directives/tooltip'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { type BentoDatagridDataItem } from '@/components/data-grid/data-grid.types'; import { DATA_GRID_TOUCH_DEVICE } from '@/components/data-grid/data-grid.keys'; import Edit1Icon from '@adyen/ui-assets-icons-16/vue/edit-1'; import ExternalLinkIcon from '@adyen/ui-assets-icons-16/vue/external-link'; import OptionsVerticalIcon from '@adyen/ui-assets-icons-16/vue/options-vertical'; // Types import type { BentoColumnCellAction, BentoColumnCellActionMenuItem, BentoColumnCellActions, BentoColumnCellActionsByCellDataFn, BentoColumnCellActionsProp, BentoDataGridRowActionsProp, } from './data-grid-cell-actions.types'; // Translations import messages from './messages.json'; import { useI18n } from '@/utils/ts/i18n'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const props = defineProps({ /** * Column definitions using 'key' to relate to data object. */ cellActions: { type: [Array, Function] as PropType<BentoColumnCellActionsProp | BentoDataGridRowActionsProp>, required: true, validator: (value: BentoColumnCellActions) => value?.length <= 2, }, /** * The column field key. */ columnKey: { type: String, default: undefined, }, /** * Disables any opening of the cell actions menus using the down arrow key. */ disableArrowDownOpeningMenu: { type: Boolean, default: false, }, /** * Set to true if the datagrid has been scrolled and the right side has overflowing space. */ hasRightOverflow: { type: Boolean, default: false, }, /** * Set to true if the row has a darker background when zebra rows is enabled. */ hasZebraBackground: { type: Boolean, default: false, }, /** * Set to true if the row is selected. */ isSelected: { type: Boolean, default: false, }, /** * Set to true if the row is highlighted. */ isHighlighted: { type: Boolean, default: false, }, /** * Set if set to true will teleport the action menu component */ teleportMenu: { type: Boolean, default: false, }, /** * The row data item object. */ rowDataItem: { type: Object as PropType<BentoDatagridDataItem>, required: true, }, }); const emit = defineEmits<{ /** * Emits when a cell actions menu is opened */ (e: 'menu-open'): void; /** * Emits when a cell actions menu is closed */ (e: 'menu-close'): void; /** * Emits when a cell actions menu button closes due to a menu item click */ (e: 'menu-item-click'): void; }>(); const isTouchDevice: Ref<boolean> = inject(DATA_GRID_TOUCH_DEVICE, ref(false)); // Helper to get text for menu items, always returning a string const getMenuItemText = (action: BentoColumnCellAction | BentoColumnCellActionMenuItem) => { // If it's already a menu item with a text property, use it if ('text' in action && action.text !== undefined) { return action.text; } // Otherwise, derive from variant or title switch ((action as BentoColumnCellAction).variant) { case 'copy': return t('copy'); case 'edit': return t('edit'); case 'open': return t('open'); case 'more': return t('more'); default: return (action as BentoColumnCellAction).title; } }; const getMenuItemIcon = (action: BentoColumnCellAction) => { switch (action.variant) { case 'copy': return Copy; case 'edit': return Edit1Icon; case 'open': return ExternalLinkIcon; default: return undefined; } }; const isRowActions = computed(() => props.columnKey === undefined); const conditionalCellActionsBackgroundGradientClasses = computed(() => ({ 'b-data-grid-cell-actions__background-gradient--with-row-actions-and-overflow': isRowActions.value && props.hasRightOverflow, 'b-data-grid-cell-actions__background-gradient--zebra': props.hasZebraBackground, 'b-data-grid-cell-actions__background-gradient--highlighted': props.isHighlighted, 'b-data-grid-cell-actions__background-gradient--selected': props.isSelected, })); const conditionalCellActionsClasses = computed(() => ({ 'b-data-grid-cell-actions__actions--with-row-actions': isRowActions.value, 'b-data-grid-cell-actions__actions--zebra': props.hasZebraBackground, 'b-data-grid-cell-actions__actions--highlighted': props.isHighlighted, 'b-data-grid-cell-actions__actions--selected': props.isSelected, 'b-data-grid-cell-actions__actions--zebra-hover': !isRowActions.value && props.hasZebraBackground, 'b-data-grid-cell-actions__actions--highlighted-hover': !isRowActions.value && props.isHighlighted, 'b-data-grid-cell-actions__actions--selected-hover': !isRowActions.value && props.isSelected, })); const showGradientElement = computed(() => !isRowActions.value || (isRowActions.value && props.hasRightOverflow)); /** * Checks if the passed object is a cell actions function that returns an cell actions array. * @param cellActions - a cell actions array or function */ function isBentoColumnCellActionsByCellValueDataFn( cellActions: BentoColumnCellActionsProp ): cellActions is BentoColumnCellActionsByCellDataFn { return typeof cellActions === 'function'; } const computedCellActions = computed(() => { const cellActions = isBentoColumnCellActionsByCellValueDataFn(props.cellActions) ? props.cellActions?.(props.rowDataItem, props.columnKey) : props.cellActions; if (isTouchDevice.value && !isRowActions.value && cellActions) { const flattenedMenuItems: Array<BentoColumnCellActionMenuItem> = []; cellActions.forEach(action => { if (action.menuData) { action.menuData.forEach(subMenuItem => { flattenedMenuItems.push(subMenuItem); }); } else { flattenedMenuItems.push({ text: getMenuItemText(action), event: action.event, critical: action.critical, disabled: action.disabled, icon: getMenuItemIcon(action), }); } }); return [ { variant: 'more', isShort: true, iconLeft: OptionsVerticalIcon, menuData: transformMenuEvents(flattenedMenuItems), }, ] as BentoColumnCellActions; } // Do not allow users to provide more than 2 cell action buttons if (!isTouchDevice.value && cellActions?.length > 2) { printDevelopmentWarning( 'WARNING: BentoDataGrid Cell actions can only have a maximum of 2 actions, if you need more consider using the menuData property instead.' ); } return cellActions?.slice(0, 2); }); const getButtonTitle = computed(() => (button: BentoColumnCellAction, isForSvgTitle?: boolean) => { if ((!isForSvgTitle && button.isShort) || (isForSvgTitle && !button.isShort)) { return undefined; } switch (button.variant) { case 'copy': return t('copy'); case 'edit': return t('edit'); case 'open': return t('open'); case 'more': return t('more'); default: return button.title; } }); const generateButtonEvents = (button: BentoColumnCellAction) => button?.event !== undefined ? { click: (event: MouseEvent) => { event.stopPropagation(); button.event(props.rowDataItem, props.columnKey); }, } : {}; const generateMenuEvents = (menuDataItem: BentoColumnCellActionMenuItem): BentoMenuItem => ({ ...menuDataItem, handler: () => { if (menuDataItem?.handler && typeof menuDataItem.handler === 'function') { menuDataItem?.handler(); } if (menuDataItem?.event && typeof menuDataItem.event === 'function') { menuDataItem?.event(props.rowDataItem, props.columnKey); } }, }); const transformMenuEvents = (menuData: Array<BentoColumnCellActionMenuItem>) => menuData.map(menuDataItem => { const transformedMenuData = menuDataItem; if (transformedMenuData?.items) { transformedMenuData.items = transformMenuEvents(transformedMenuData.items); } return { ...generateMenuEvents(transformedMenuData) }; }); const getCopyText = (button: BentoColumnCellAction) => button?.getCopyData !== undefined ? button.getCopyData(props.rowDataItem, props.columnKey) : props.columnKey ? (props.rowDataItem?.[props.columnKey]?.toString() as string) : ''; const shouldShowMenuIconLeft = (button: BentoColumnCellAction) => button.iconLeft || button?.variant === 'more'; </script> <script lang="ts"> /** * Internal component to display the data-grid actions. * There are 4 different pre-defined buttons that can be used by using the `variant` property. * * The component renders buttons or a menu depending on the actions provided. * * @example * <data-grid-cell-actions * :cell-actions="[ * { variant: 'edit', event: () => {} }, * { * variant: 'more', * isShort: true, * menuData: [ * { * text: 'Cancel payment', * handler: () => {}), * critical: true, * }, * { * text: 'Transfer payment', * handler: () => {}, * }, * { * text: 'Delete payment', * handler: () => {}, * disabled: true, * }, * ], * }, * ]" * column-key="payment" * :row-data-item="rowData" * /> */ export default { i18n: { messages }, name: 'data-grid-cell-actions', }; </script> <style lang="scss" scoped src="./data-grid-cell-actions.scss" />
@@ -1 +1 @@
1
- <template> <div ref="allColumnsRef" class="b-data-grid-columns" :class="conditionalColumnsClasses" data-testid="data-grid-columns" > <div v-for="(column, index) in columns" :key="column.field" :ref="el => registerColumnsRefs(el, column.field)" :aria-colindex="index + 1" :aria-label="column.label" :aria-sort="getSortingColumnDirection(column)" :class="conditionalColumnClasses(column, index)" :data-testid="`${column.field}-column-header`" :draggable="hasDraggableColumns && !isResizing && !isActionsColumn(column)" class="b-data-grid-columns__column" role="columnheader" :tabindex="column.sortable || hasColumnPanel || column.infoIconTooltip ? '0' : '-1'" :style="{ width: column.autoWidth && isMeasuringColumnWidths ? 'auto' : `${column.width}px` }" v-on="dataGridColumnEvents(column)" > <div v-if="isActionsColumn(column) && selectable" class="b-data-grid-columns__select-column" :class="conditionalSelectColumnClasses" > <bento-checkbox v-if="!selectableAcrossPages" :aria-label="t('selectAll')" :model-value="allItemsSelectedState.allItemsSelected" data-testid="checkbox-select-all" :indeterminate="allItemsSelectedState.isIndeterminate" value="data-grid-select-all" @update:model-value="onAllRowSelect" /> <template v-if="selectableAcrossPages"> <bento-menu :button="{ variant: 'tertiary', condensed: true, 'aria-label': t('selectAllMenu') }" :data="selectAllMenu" class="b-data-grid-columns__select-column-menu" disable-arrow-down-opening-menu menu-fixed-positioning menu-position="bottom-start" > <template #iconLeft> <div class="b-data-grid-columns__select-column-menu-button"> <decorative-checkbox :indeterminate="allItemsSelectedState.isIndeterminate" :checked="allItemsSelectedState.allItemsSelected" :all-selected="isAllSelectionState" class="b-data-grid-columns__select-column-menu-checkbox" data-testid="checkbox-select-all" /> <triangle-down :svg-title="t('selectionMenu')" aria-hidden="true" /> </div> </template> </bento-menu> </template> </div> <template v-else> <div :aria-label="t('columnActions', { name: column.label })" aria-live="polite" role="region" class="b-data-grid-columns__column-header-and-actions" :class="conditionalColumnHeaderAndActionsClasses(column)" > <bento-typography el="div" strongest class="b-data-grid-columns__column-header" :class="conditionalColumnHeaderClasses" > <slot :column="column" :name="`head-${column.field}`"> <span class="b-data-grid-columns__label">{{ column.label }}</span> </slot> </bento-typography> <div v-if="hasColumnActions(column)" class="b-data-grid-columns__column-actions" :class="conditionalColumnActionsClasses(column)" > <div v-if="column.infoIconTooltip" class="b-data-grid-columns__column-info-icon"> <bento-info-icon :tooltip-text="column.infoIconTooltip" /> </div> <div v-if="column.sortable" class="b-data-grid-columns__column-sort"> <bento-toggle-button condensed :toggled="isSorting(column.field)" variant="tertiary" @click="sortByColumnField(column.field)" > <template #iconLeft> <arrow-up-and-down-sort-icon v-if="!getSortingDirection(column.field)" :svg-title="t('sortColumn', { name: column.label })" /> <sort-ascending-icon v-else-if=" getSortingDirection(column.field) === BentoDatagridSortDirection.ASCENDING " :svg-title="t('sortColumnInDescendingOrder', { name: column.label })" /> <sort-descending-icon v-else-if=" getSortingDirection(column.field) === BentoDatagridSortDirection.DESCENDING " :svg-title="t('sortColumnInAscendingOrder', { name: column.label })" /> </template> </bento-toggle-button> </div> <bento-menu v-if="hasColumnPanel" :ref="el => (contextMenuRef[column.field] = el)" class="b-data-grid-columns__menu" :class="conditionalColumnMenuClasses(column)" disable-arrow-down-opening-menu :data="contextualMenuData(column)" :menu-position="computeMenuPosition(hasFirstColumnPanelRightAligned, index)" :menu-fixed-positioning="true" :button="{ variant: 'tertiary', condensed: true }" @open="$emit(DatagridColumnsEvent.COLUMN_MENU_OPEN)" @close="$emit(DatagridColumnsEvent.COLUMN_MENU_CLOSE)" /> </div> </div> <div :data-testid="`data-grid-column-${column.field}`" class="b-data-grid-columns__column-separator-container" :class="conditionalColumnSeparatorClasses(column)" role="presentation" @mousedown="startResize(hasResizableColumns, column.field, $event)" @click.stop > <div v-if="!isFrozenColumns || (isFrozenColumns && index !== columns.length - 1)" class="b-data-grid-columns__column-separator" ></div> </div> </template> </div> <template v-if="rowActionsMaximumWidth && !isFrozenColumns"> <div role="presentation" class="b-data-grid-columns__row-actions-filler"></div> <div :ref="el => registerColumnsRefs(el, DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME)" :aria-label="t('rowActionsColumn')" :aria-colindex="columns.length + 1" class="b-data-grid-columns__column" role="columnheader" tabindex="0" :style="{ minWidth: `${rowActionsMaximumWidth}px` }" ></div> </template> </div> </template> <script lang="ts" setup> import { computed, type ComputedRef, inject, type PropType, type Ref, ref, toRefs } from 'vue'; import { BentoCheckbox } from '@/components/checkbox'; import { type BentoColumn, type BentoColumnWidthOverridesLookup, type BentoDatagridAllItemsSelectedState, BentoDatagridAriaSortValue, type BentoDatagridSortByColumn, BentoDatagridSortDirection, type BentoDataGridVisibleRowsState, } from '../../data-grid.types'; import { BentoTypography } from '@/components/typography'; import { DATAGRID_ACTIONS_COLUMN_FIELD_NAME, DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME, useResizer, useSorter, } from '../../composables'; import { focusOnInteractiveChild } from '../../utils'; import { BentoInfoIcon } from '@/components/info-icon'; import { type BentoDatagridColumnSelectionState, type BentoDatagridDraggingColumnState, DatagridColumnsEvent, type DatagridColumnsHeaderReferences, } from './data-grid-columns.types'; import { BentoMenu } from '@/components/menu'; import { BentoToggleButton } from '@/components/button'; import { DecorativeCheckbox } from '@/internal/decorative-checkbox'; import { useI18n } from '@/utils/ts/i18n'; import type { BentoMenuItem } from '@/components/menu/components'; import type { DatagridColumnHoveringState } from '@/components/data-grid/composables/use-column-hovering/use-column-hovering.types'; import { BentoPopoverPositions } from '@/components/popover/popover.types'; import ArrowUpAndDownSortIcon from '@adyen/ui-assets-icons-16/vue/arrow-up-down-sort'; import ArrowLeftIcon from '@adyen/ui-assets-icons-16/vue/arrow-left'; import ArrowRightIcon from '@adyen/ui-assets-icons-16/vue/arrow-right'; import FreezeIcon from '@adyen/ui-assets-icons-16/vue/freeze'; import HideIcon from '@adyen/ui-assets-icons-16/vue/hide'; import SortAscendingIcon from '@adyen/ui-assets-icons-16/vue/sort-ascending'; import SortDescendingIcon from '@adyen/ui-assets-icons-16/vue/sort-descending'; import ShowIcon from '@adyen/ui-assets-icons-16/vue/show'; import TriangleDown from '@adyen/ui-assets-icons-16/vue/triangle-down'; import messages from './messages.json'; import { DATA_GRID_SMALL_CONTAINER, DATA_GRID_TOUCH_DEVICE } from '@/components/data-grid/data-grid.keys'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * Column definitions using 'key' to relate to data object. */ columns: { type: Array as PropType<Array<BentoColumn>>, required: true, }, /** * Columns being dragged, contains dragging column and a target column */ draggingColumns: { type: Object as PropType<BentoDatagridDraggingColumnState>, default: undefined, }, /** * Determines the state of the current results: { filtered: boolean, allItemsVisible: boolean } to render the correct select all menu options */ visibleRowsState: { type: Object as PropType<BentoDataGridVisibleRowsState>, default: undefined, }, /** * Toggle the columns draggable events. */ hasDraggableColumns: { type: Boolean, default: true, }, /** * If resize is enabled */ hasResizableColumns: { type: Boolean, default: true, }, /** * Signifies if the column panel is enabled. */ hasColumnPanel: { type: Boolean, default: false, }, /** * If set to true, aligns the first column panel to the right of its button. */ hasFirstColumnPanelRightAligned: { type: Boolean, default: false, }, /** * If the data grid has overflow on the left side */ hasLeftOverflow: { type: Boolean, default: false, }, /** * The column hovered over state and whether the hovered over column should show a border right or left. */ hoveredOverColumnState: { type: Object as PropType<DatagridColumnHoveringState>, required: true, }, /** * Toggled to true when the DataGrid has auto width columns and is measuring the */ isMeasuringColumnWidths: { type: Boolean, default: false, }, /** * Used to disable hiding the last visible column. */ isOnlyOneVisibleColumn: { type: Boolean, default: false, }, /** * If these columns are frozen (pinned) columns */ isFrozenColumns: { type: Boolean, default: false, }, /** * Show frozen columns separator */ showFrozenColumnsSeparator: { type: Boolean, default: false, }, /** * The column all items selected state */ allItemsSelectedState: { type: Object as PropType<BentoDatagridAllItemsSelectedState>, default: undefined, }, /** * Determines if the user has enabled row selection */ selectable: { type: Boolean, default: false, }, /** * Adds an extra option when selecting all, to select items in other pages too */ selectableAcrossPages: { type: Boolean, default: false, }, /** * Determines which state of row selection is currently active. Used to display the "Select all" menu options. */ selectionState: { type: String as PropType<BentoDatagridColumnSelectionState>, default: undefined, }, /** * Sortby handles proper rendering of sort icon in header. Sorting currently is only supported server side. * It does make sure the currently sorted column has a more visible sort icon, and the sort icon points in the right direction. */ sortBy: { type: Array as PropType<Array<BentoDatagridSortByColumn>>, // NOTE: is an array to allow multi-sorting in the future default: () => [], }, /** * Truncates all data grid column headers */ truncateColumnHeaders: { type: Boolean, default: false, }, /** * The maximum width of the row actions in all rows */ rowActionsMaximumWidth: { type: Number, default: 0, }, }); const emit = defineEmits<{ /** * Emitted when a user manually resizes a column width. */ ( e: DatagridColumnsEvent.COLUMN_RESIZE, column: { field: string; width: number; } ); /** * Triggered when a column is resized. */ (e: DatagridColumnsEvent.COLUMN_RESIZING, value: BentoColumnWidthOverridesLookup); /** * Triggered when a column is sorted. */ (e: 'sort', updatedSortBy: Array<BentoDatagridSortByColumn>); /** * Triggered when selecting all rows. */ (e: DatagridColumnsEvent.SELECT_ALL_ROWS, areAllItemsSelected: boolean); /** * Triggered when all rows across all pages are selected. */ (e: DatagridColumnsEvent.SELECT_ALL_ACROSS_PAGES); /** * Triggered when all filtered rows are selected. */ (e: DatagridColumnsEvent.SELECT_ALL_CURRENT_RESULTS); /** * Triggered when deselecting all rows. */ (e: DatagridColumnsEvent.DESELECT_ALL); /** * Triggered when all filtered rows are deselected. */ (e: DatagridColumnsEvent.DESELECT_ALL_CURRENT_RESULTS); /** * Triggered when a columns started to be dragged. */ (e: DatagridColumnsEvent.COLUMN_DRAG_START, column: BentoColumn, event: DragEvent & Event); /** * Triggered when a column is being dragged over another DOM element. */ (e: DatagridColumnsEvent.COLUMN_DRAG_ENTER, column: BentoColumn, event: DragEvent & Event); /** * Triggered when a column is being dragged and is dropped over another DOM element. */ (e: DatagridColumnsEvent.COLUMN_DROP, column: BentoColumn); /** * Triggered when a column drag operation has ended. */ (e: DatagridColumnsEvent.COLUMN_DRAG_END); /** * Triggered when a column is being moved by a certain offset. */ (e: DatagridColumnsEvent.COLUMN_MOVE, movingColumn: BentoColumn, offset: number); /** * Triggered when a column is updated. */ (e: DatagridColumnsEvent.COLUMN_UPDATE, column: BentoColumn); /** * Triggered when the column contextual menu is opened. */ (e: DatagridColumnsEvent.COLUMN_MENU_OPEN); /** * Triggered when the column contextual menu is closed. */ (e: DatagridColumnsEvent.COLUMN_MENU_CLOSE); /** * Emitted when a column header is hovered over. */ (e: 'column-hover', columnKey: BentoColumn); }>(); const CLOSING_CONTEXTUAL_MENU_DELAY_MS = 100; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const { columns, sortBy } = toRefs(props); const allColumnsRef = ref<HTMLDivElement>(); const columnHeaderRefs = ref<DatagridColumnsHeaderReferences>({}); const contextMenuRef = ref({}); const hoveredColumnField = ref(); const closingTimeOutId = ref(); // Selecting const onAllRowSelect = (areAllItemsSelected: boolean) => emit(DatagridColumnsEvent.SELECT_ALL_ROWS, areAllItemsSelected); const isActionsColumn = (column: BentoColumn) => column.field === DATAGRID_ACTIONS_COLUMN_FIELD_NAME; const isAllSelectionState = computed(() => props.selectionState === 'all'); // Select all menu const selectAllMenu = computed(() => [ { text: t('selectPage') as string, handler: () => onAllRowSelect(true), disabled: props.selectionState === 'page' || props.selectionState === 'all', }, props.visibleRowsState?.filtered ? { text: t('selectAllCurrentResults') as string, handler: () => emit(DatagridColumnsEvent.SELECT_ALL_CURRENT_RESULTS), addDivider: props.selectionState !== 'none', disabled: props.selectionState === 'all', } : { text: t('selectAll') as string, handler: () => emit(DatagridColumnsEvent.SELECT_ALL_ACROSS_PAGES), addDivider: props.selectionState !== 'none', disabled: props.selectionState === 'all', }, props.selectionState !== 'none' && { text: t('deselectPage') as string, handler: () => onAllRowSelect(false), addDivider: !props.visibleRowsState?.filtered, // otherwise divider shown in deselect all in current results disabled: props.selectionState === 'unselected-page', }, props.visibleRowsState?.filtered && props.selectionState !== 'none' && { text: t('deselectAllCurrentResults') as string, handler: () => emit(DatagridColumnsEvent.DESELECT_ALL_CURRENT_RESULTS), addDivider: true, disabled: props.selectionState === 'unselected-page' && props.visibleRowsState.allRowsVisible, }, props.selectionState !== 'none' && { text: t('deselectAll') as string, handler: () => emit(DatagridColumnsEvent.DESELECT_ALL), }, ].filter(Boolean) ); // Resizing const { startResize, isResizing, columnHoverEventToEmitAfterResizing } = useResizer(columns, emit); // Sorting const { isSorting, getSortingDirection, sortByColumnField } = useSorter(sortBy, columns, emit); const getSortingColumnDirection = (column: BentoColumn): BentoDatagridAriaSortValue => { if (!column.sortable) { return null; } const sortingDirection = props.sortBy.find(({ field }) => field === column.field)?.direction; if (!sortingDirection) { return BentoDatagridAriaSortValue.NONE; } const ariaSortingDirectionMap: { [key in BentoDatagridSortDirection]: BentoDatagridAriaSortValue } = { [BentoDatagridSortDirection.ASCENDING]: BentoDatagridAriaSortValue.ASCENDING, [BentoDatagridSortDirection.DESCENDING]: BentoDatagridAriaSortValue.DESCENDING, }; return ariaSortingDirectionMap[sortingDirection]; }; // Dragging emit events and methods const draggingEventsForColumn = computed(() => (column: BentoColumn) => { if (!props.hasDraggableColumns) { return {}; } return { dragstart: (e: DragEvent & Event) => { emit(DatagridColumnsEvent.COLUMN_DRAG_START, column, e); }, dragover: (e: DragEvent) => { e.preventDefault(); }, dragenter: (e: DragEvent) => { e.preventDefault(); emit(DatagridColumnsEvent.COLUMN_DRAG_ENTER, column, e); }, drop: () => { emit(DatagridColumnsEvent.COLUMN_DROP, column); }, dragend: () => { emit(DatagridColumnsEvent.COLUMN_DRAG_END); }, }; }); // Contextual menu (moving, visiblity and freezing) const contextualMenuData = computed<(col: BentoColumn) => Array<BentoMenuItem>>( () => (column: BentoColumn) => [ { text: t('moveColumnLeft') as string, handler: () => emit(DatagridColumnsEvent.COLUMN_MOVE, column, -1), icon: ArrowLeftIcon, disabled: props.columns[0].field === column.field, }, { text: t('moveColumnRight') as string, handler: () => emit(DatagridColumnsEvent.COLUMN_MOVE, column, 1), icon: ArrowRightIcon, disabled: props.columns[props.columns.length - 1].field === column.field, }, !isSmallContainer.value && { text: (column?.frozen ? t('unfreezeColumn') : t('freezeColumn')) as string, handler: () => emit(DatagridColumnsEvent.COLUMN_UPDATE, { ...column, frozen: !column?.frozen }), icon: FreezeIcon, }, { text: (column?.visible !== false ? t('hideColumn') : t('showColumn')) as string, handler: () => { emit(DatagridColumnsEvent.COLUMN_UPDATE, { ...column, visible: !(column?.visible !== false), }); }, icon: column?.visible !== false ? HideIcon : ShowIcon, disabled: column?.mandatory || (column?.visible !== false && props.isOnlyOneVisibleColumn), }, ].filter(Boolean) ); const showContextualMenu = (column: BentoColumn) => { hoveredColumnField.value = column.field; if (closingTimeOutId.value) { clearTimeout(closingTimeOutId.value); } }; const hideContextualMenu = (column: BentoColumn) => { // We want to delay closing for a couple of seconds so // it doesn't flicker if a user hovers over the icon and then inside the popover. const timeOutId = setTimeout(() => { if (column.field === hoveredColumnField.value) { hoveredColumnField.value = ''; } }, CLOSING_CONTEXTUAL_MENU_DELAY_MS); closingTimeOutId.value = timeOutId; }; /** * Generates events for a Datagrid Column. * @param column - the BentoColumn we are attaching events onto. */ const dataGridColumnEvents = (column: BentoColumn) => { let events: { [eventName: string]: (e: Event) => void } = { mouseenter: () => { showContextualMenu(column); if (!isResizing.value) { emit('column-hover', column); } else { columnHoverEventToEmitAfterResizing.value = column; } }, mouseleave: () => { hideContextualMenu(column); if (!isResizing.value) { emit('column-hover', null); } else { columnHoverEventToEmitAfterResizing.value = null; } }, focus: () => { showContextualMenu(column); if (isActionsColumn(column)) { focusOnInteractiveChild(columnHeaderRefs.value[column.field]); } }, focusout: (e: FocusEvent) => { if (!allColumnsRef.value?.contains(e?.relatedTarget as Node)) { hideContextualMenu(column); } }, }; if (!isActionsColumn(column)) { events = { ...events, ...draggingEventsForColumn.value(column), }; } return events; }; const hasColumnActions = (column: BentoColumn) => column.infoIconTooltip || column.sortable || props.hasColumnPanel; const registerColumnsRefs = (el: unknown, field: string) => { if (columnHeaderRefs.value?.[field] === undefined) { columnHeaderRefs.value = { ...columnHeaderRefs.value, [field]: null }; } if (el !== null) { columnHeaderRefs.value[field] = el as HTMLDivElement; } }; const isSmallContainer: ComputedRef<boolean> = inject( DATA_GRID_SMALL_CONTAINER, computed(() => false) ); const isTouchDevice: Ref<boolean> = inject(DATA_GRID_TOUCH_DEVICE, ref(false)); // Conditional column styles // Conditional styles const conditionalColumnsClasses = computed(() => { const defaultConditionalClasses = { 'b-data-grid-columns--is-measuring': props.isMeasuringColumnWidths, }; return columns.value[0]?.frozen && !isSmallContainer.value ? { ...defaultConditionalClasses, 'b-data-grid-columns--frozen': true, 'b-data-grid-columns--frozen-separator': props.showFrozenColumnsSeparator, 'b-data-grid-columns--overflow-shadow-left': props.hasLeftOverflow, } : { ...defaultConditionalClasses, 'b-data-grid-columns--unfrozen': true, }; }); const conditionalSelectColumnClasses = computed(() => ({ 'b-data-grid-columns__select-column--all': props.selectableAcrossPages, })); const computeMenuPosition = (hasFirstColumnPanelRightAligned, index) => hasFirstColumnPanelRightAligned && index === 0 ? BentoPopoverPositions.BOTTOM_START : BentoPopoverPositions.BOTTOM_END; const conditionalColumnClasses = (column: BentoColumn, columnIndex: number) => ({ 'b-data-grid-columns__column--column-hovered': column.field === props.hoveredOverColumnState.hoveredOverColumnField, 'b-data-grid-columns__column--column-hovered-no-border-left': column.field === props.hoveredOverColumnState.hoveredOverColumnField && props.hoveredOverColumnState.noBorderLeft, 'b-data-grid-columns__column--column-hovered-no-border-right': column.field === props.hoveredOverColumnState.hoveredOverColumnField && props.hoveredOverColumnState.noBorderRight, 'b-data-grid-columns__column--last-frozen-frozen-column': props.isFrozenColumns && columnIndex === columns.value.length - 1, 'b-data-grid-columns__column--sortable': column.sortable, 'b-data-grid-columns__column--padding-left-unset': column?.padding?.left === false, 'b-data-grid-columns__column--padding-right-unset': column?.padding?.right === false, 'b-data-grid-columns__column--numeric': column?.numeric, 'b-data-grid-columns__column--frozen': column?.frozen, 'b-data-grid-columns__column--is-re-ordering': column?.field === props.draggingColumns?.dragging?.field, 'b-data-grid-columns__column--is-re-ordering-target-left': column?.field === props.draggingColumns?.target?.field && props.draggingColumns?.dragToLeft, 'b-data-grid-columns__column--is-re-ordering-target-right': column?.field === props.draggingColumns?.target?.field && !props.draggingColumns?.dragToLeft, 'b-data-grid-columns__column--with-sorting-or-column-panel': column.sortable || column.infoIconTooltip || props.hasColumnPanel || props.selectableAcrossPages, 'b-data-grid-columns--small-container': isSmallContainer.value, }); const conditionalColumnHeaderAndActionsClasses = (column: BentoColumn) => ({ 'b-data-grid-columns__column-header-and-actions--numeric': column?.numeric, }); const conditionalColumnActionsClasses = (column: BentoColumn) => ({ 'b-data-grid-columns__column-actions--numeric': column?.numeric, }); const conditionalColumnSeparatorClasses = (column: BentoColumn) => ({ 'b-data-grid-columns__column-separator-container--is-resizeable': props.hasResizableColumns, 'b-data-grid-columns__column-separator-container--is-offset': props.hoveredOverColumnState.hoveredOverColumnField === column.field && props.hoveredOverColumnState.isSeparatorOffset, }); const conditionalColumnHeaderClasses = computed(() => ({ 'b-data-grid-columns__column-header--truncated': props.truncateColumnHeaders, })); const conditionalColumnMenuClasses = (column: BentoColumn) => ({ 'b-data-grid-columns__menu--hovered': hoveredColumnField.value === column.field || isTouchDevice.value, }); // Exposed refs defineExpose({ columnHeaderRefs, }); </script> <script lang="ts"> /** * A component to display the BentoDataGrid columns * * @example * * export default { * components: { DataGridColumns }, * template: ` * <data-grid-columns * v-if="columnSet?.length" * :key="`data-grid-columns-${id}`" * :columns="columnSet" * :has-resizable-columns="hasResizableColumns" * :sort-by="sortBy" * :all-items-selected-state="allItemsSelectedState" * :is-frozen-columns="isFrozenColumns" * :show-frozen-columns-separator="showFrozenColumnsSeparator" * :dragging-columns="draggingColumns" * :has-draggable-columns="allowColumnDragAndDrop" * :has-column-panel="columnPanel" * :has-left-overflow="hasLeftOverflow" * :has-first-column-panel-right-aligned="index === computedColumnPanelAlignment" * :is-only-one-visible-column="isOnlyOneVisibleColumn" * :selectable="selectable" * :selectable-across-pages="selectableAcrossPagesComputed" * :truncate-column-headers="truncateColumnHeaders" * v-on="draggingEventsForColumns" * @column-move="onMoveColumnByOffset" * @column-update="onConfigSettingsColumnUpdate" * @column-resize="onColumnResized" * @column-resizing="onColumnWidthOverride" * @select-all-rows="onAllRowSelect" * @select-all-across-pages="emit(BentoDatagridEvent.SELECT_ALL_ACROSS_PAGES)" * @sort="onSort" * > * <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> * <slot :name="innerSlot" v-bind="scope" /> * </template> * </data-grid-columns> * ` * } */ export default { i18n: { messages }, name: 'data-grid-columns', }; </script> <style lang="scss" scoped src="./data-grid-columns.scss" />
1
+ <template> <div ref="allColumnsRef" class="b-data-grid-columns" :class="conditionalColumnsClasses" data-testid="data-grid-columns" > <div v-for="(column, index) in columns" :key="column.field" :ref="el => registerColumnsRefs(el, column.field)" :aria-colindex="index + 1" :aria-label="column.label" :aria-sort="getSortingColumnDirection(column)" :class="conditionalColumnClasses(column, index)" :data-testid="`${column.field}-column-header`" :draggable="hasDraggableColumns && !isResizing && !isActionsColumn(column)" class="b-data-grid-columns__column" role="columnheader" :tabindex="column.sortable || hasColumnPanel || column.infoIconTooltip ? '0' : '-1'" :style="{ width: column.autoWidth && isMeasuringColumnWidths ? 'auto' : `${column.width}px` }" v-on="dataGridColumnEvents(column)" > <div v-if="isActionsColumn(column) && selectable" class="b-data-grid-columns__select-column" :class="conditionalSelectColumnClasses" > <bento-checkbox v-if="!selectableAcrossPages" :aria-label="t('selectAll')" :model-value="allItemsSelectedState.allItemsSelected" data-testid="checkbox-select-all" :indeterminate="allItemsSelectedState.isIndeterminate" value="data-grid-select-all" @update:model-value="onAllRowSelect" /> <template v-if="selectableAcrossPages"> <bento-menu :button="{ variant: 'tertiary', condensed: true, 'aria-label': t('selectAllMenu') }" :data="selectAllMenu" class="b-data-grid-columns__select-column-menu" disable-arrow-down-opening-menu menu-fixed-positioning menu-position="bottom-start" > <template #iconLeft> <div class="b-data-grid-columns__select-column-menu-button"> <decorative-checkbox :indeterminate="allItemsSelectedState.isIndeterminate" :checked="allItemsSelectedState.allItemsSelected" :all-selected="isAllSelectionState" class="b-data-grid-columns__select-column-menu-checkbox" data-testid="checkbox-select-all" /> <triangle-down :svg-title="t('selectionMenu')" aria-hidden="true" /> </div> </template> </bento-menu> </template> </div> <template v-else> <div :aria-label="t('columnActions', { name: column.label })" aria-live="polite" role="region" class="b-data-grid-columns__column-header-and-actions" :class="conditionalColumnHeaderAndActionsClasses(column)" > <bento-typography el="div" strongest class="b-data-grid-columns__column-header" :class="conditionalColumnHeaderClasses" > <slot :column="column" :name="`head-${column.field}`"> <span class="b-data-grid-columns__label">{{ column.label }}</span> </slot> </bento-typography> <div v-if="hasColumnActions(column)" class="b-data-grid-columns__column-actions" :class="conditionalColumnActionsClasses(column)" > <div v-if="column.infoIconTooltip" class="b-data-grid-columns__column-info-icon"> <bento-info-icon :tooltip-text="column.infoIconTooltip" /> </div> <div v-if="column.sortable" class="b-data-grid-columns__column-sort"> <bento-toggle-button condensed :toggled="isSorting(column.field)" variant="tertiary" @click="sortByColumnField(column.field)" > <template #iconLeft> <arrow-up-and-down-sort-icon v-if="!getSortingDirection(column.field)" :svg-title="t('sortColumn', { name: column.label })" /> <sort-ascending-icon v-else-if=" getSortingDirection(column.field) === BentoDatagridSortDirection.ASCENDING " :svg-title="t('sortColumnInDescendingOrder', { name: column.label })" /> <sort-descending-icon v-else-if=" getSortingDirection(column.field) === BentoDatagridSortDirection.DESCENDING " :svg-title="t('sortColumnInAscendingOrder', { name: column.label })" /> </template> </bento-toggle-button> </div> <bento-menu v-if="hasColumnPanel" :ref="el => (contextMenuRef[column.field] = el)" class="b-data-grid-columns__menu" :class="conditionalColumnMenuClasses(column)" disable-arrow-down-opening-menu :data="contextualMenuData(column)" :menu-position="computeMenuPosition(hasFirstColumnPanelRightAligned, index)" :menu-fixed-positioning="true" :button="{ variant: 'tertiary', condensed: true }" @open="$emit(DatagridColumnsEvent.COLUMN_MENU_OPEN)" @close="$emit(DatagridColumnsEvent.COLUMN_MENU_CLOSE)" /> </div> </div> <div :data-testid="`data-grid-column-${column.field}`" class="b-data-grid-columns__column-separator-container" :class="conditionalColumnSeparatorClasses(column)" role="presentation" @mousedown="startResize(hasResizableColumns, column.field, $event)" @click.stop > <div v-if="!isFrozenColumns || (isFrozenColumns && index !== columns.length - 1)" class="b-data-grid-columns__column-separator" ></div> </div> </template> </div> <template v-if="rowActionsMaximumWidth && !isFrozenColumns"> <div role="presentation" class="b-data-grid-columns__row-actions-filler"></div> <div :ref="el => registerColumnsRefs(el, DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME)" :aria-label="t('rowActionsColumn')" :aria-colindex="columns.length + 1" class="b-data-grid-columns__column" role="columnheader" :tabindex="areAnyColumnsFocusable ? '0' : '-1'" :style="{ minWidth: `${rowActionsMaximumWidth}px` }" ></div> </template> </div> </template> <script lang="ts" setup> import { computed, type ComputedRef, inject, type PropType, type Ref, ref, toRefs } from 'vue'; import { BentoCheckbox } from '@/components/checkbox'; import { type BentoColumn, type BentoColumnWidthOverridesLookup, type BentoDatagridAllItemsSelectedState, BentoDatagridAriaSortValue, type BentoDatagridSortByColumn, BentoDatagridSortDirection, type BentoDataGridVisibleRowsState, } from '../../data-grid.types'; import { BentoTypography } from '@/components/typography'; import { DATAGRID_ACTIONS_COLUMN_FIELD_NAME, DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME, useResizer, useSorter, } from '../../composables'; import { focusOnInteractiveChild } from '../../utils'; import { BentoInfoIcon } from '@/components/info-icon'; import { type BentoDatagridColumnSelectionState, type BentoDatagridDraggingColumnState, DatagridColumnsEvent, type DatagridColumnsHeaderReferences, } from './data-grid-columns.types'; import { BentoMenu } from '@/components/menu'; import { BentoToggleButton } from '@/components/button'; import { DecorativeCheckbox } from '@/internal/decorative-checkbox'; import { useI18n } from '@/utils/ts/i18n'; import type { BentoMenuItem } from '@/components/menu/components'; import type { DatagridColumnHoveringState } from '@/components/data-grid/composables/use-column-hovering/use-column-hovering.types'; import { BentoPopoverPositions } from '@/components/popover/popover.types'; import ArrowUpAndDownSortIcon from '@adyen/ui-assets-icons-16/vue/arrow-up-down-sort'; import ArrowLeftIcon from '@adyen/ui-assets-icons-16/vue/arrow-left'; import ArrowRightIcon from '@adyen/ui-assets-icons-16/vue/arrow-right'; import FreezeIcon from '@adyen/ui-assets-icons-16/vue/freeze'; import HideIcon from '@adyen/ui-assets-icons-16/vue/hide'; import SortAscendingIcon from '@adyen/ui-assets-icons-16/vue/sort-ascending'; import SortDescendingIcon from '@adyen/ui-assets-icons-16/vue/sort-descending'; import ShowIcon from '@adyen/ui-assets-icons-16/vue/show'; import TriangleDown from '@adyen/ui-assets-icons-16/vue/triangle-down'; import messages from './messages.json'; import { DATA_GRID_SMALL_CONTAINER, DATA_GRID_TOUCH_DEVICE } from '@/components/data-grid/data-grid.keys'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * Column definitions using 'key' to relate to data object. */ columns: { type: Array as PropType<Array<BentoColumn>>, required: true, }, /** * Columns being dragged, contains dragging column and a target column */ draggingColumns: { type: Object as PropType<BentoDatagridDraggingColumnState>, default: undefined, }, /** * Determines the state of the current results: { filtered: boolean, allItemsVisible: boolean } to render the correct select all menu options */ visibleRowsState: { type: Object as PropType<BentoDataGridVisibleRowsState>, default: undefined, }, /** * Toggle the columns draggable events. */ hasDraggableColumns: { type: Boolean, default: true, }, /** * If resize is enabled */ hasResizableColumns: { type: Boolean, default: true, }, /** * Signifies if the column panel is enabled. */ hasColumnPanel: { type: Boolean, default: false, }, /** * If set to true, aligns the first column panel to the right of its button. */ hasFirstColumnPanelRightAligned: { type: Boolean, default: false, }, /** * If the data grid has overflow on the left side */ hasLeftOverflow: { type: Boolean, default: false, }, /** * The column hovered over state and whether the hovered over column should show a border right or left. */ hoveredOverColumnState: { type: Object as PropType<DatagridColumnHoveringState>, required: true, }, /** * Toggled to true when the DataGrid has auto width columns and is measuring the */ isMeasuringColumnWidths: { type: Boolean, default: false, }, /** * Used to disable hiding the last visible column. */ isOnlyOneVisibleColumn: { type: Boolean, default: false, }, /** * If these columns are frozen (pinned) columns */ isFrozenColumns: { type: Boolean, default: false, }, /** * Show frozen columns separator */ showFrozenColumnsSeparator: { type: Boolean, default: false, }, /** * The column all items selected state */ allItemsSelectedState: { type: Object as PropType<BentoDatagridAllItemsSelectedState>, default: undefined, }, /** * Determines if the user has enabled row selection */ selectable: { type: Boolean, default: false, }, /** * Adds an extra option when selecting all, to select items in other pages too */ selectableAcrossPages: { type: Boolean, default: false, }, /** * Determines which state of row selection is currently active. Used to display the "Select all" menu options. */ selectionState: { type: String as PropType<BentoDatagridColumnSelectionState>, default: undefined, }, /** * Sortby handles proper rendering of sort icon in header. Sorting currently is only supported server side. * It does make sure the currently sorted column has a more visible sort icon, and the sort icon points in the right direction. */ sortBy: { type: Array as PropType<Array<BentoDatagridSortByColumn>>, // NOTE: is an array to allow multi-sorting in the future default: () => [], }, /** * Truncates all data grid column headers */ truncateColumnHeaders: { type: Boolean, default: false, }, /** * The maximum width of the row actions in all rows */ rowActionsMaximumWidth: { type: Number, default: 0, }, }); const emit = defineEmits<{ /** * Emitted when a user manually resizes a column width. */ ( e: DatagridColumnsEvent.COLUMN_RESIZE, column: { field: string; width: number; } ); /** * Triggered when a column is resized. */ (e: DatagridColumnsEvent.COLUMN_RESIZING, value: BentoColumnWidthOverridesLookup); /** * Triggered when a column is sorted. */ (e: 'sort', updatedSortBy: Array<BentoDatagridSortByColumn>); /** * Triggered when selecting all rows. */ (e: DatagridColumnsEvent.SELECT_ALL_ROWS, areAllItemsSelected: boolean); /** * Triggered when all rows across all pages are selected. */ (e: DatagridColumnsEvent.SELECT_ALL_ACROSS_PAGES); /** * Triggered when all filtered rows are selected. */ (e: DatagridColumnsEvent.SELECT_ALL_CURRENT_RESULTS); /** * Triggered when deselecting all rows. */ (e: DatagridColumnsEvent.DESELECT_ALL); /** * Triggered when all filtered rows are deselected. */ (e: DatagridColumnsEvent.DESELECT_ALL_CURRENT_RESULTS); /** * Triggered when a columns started to be dragged. */ (e: DatagridColumnsEvent.COLUMN_DRAG_START, column: BentoColumn, event: DragEvent & Event); /** * Triggered when a column is being dragged over another DOM element. */ (e: DatagridColumnsEvent.COLUMN_DRAG_ENTER, column: BentoColumn, event: DragEvent & Event); /** * Triggered when a column is being dragged and is dropped over another DOM element. */ (e: DatagridColumnsEvent.COLUMN_DROP, column: BentoColumn); /** * Triggered when a column drag operation has ended. */ (e: DatagridColumnsEvent.COLUMN_DRAG_END); /** * Triggered when a column is being moved by a certain offset. */ (e: DatagridColumnsEvent.COLUMN_MOVE, movingColumn: BentoColumn, offset: number); /** * Triggered when a column is updated. */ (e: DatagridColumnsEvent.COLUMN_UPDATE, column: BentoColumn); /** * Triggered when the column contextual menu is opened. */ (e: DatagridColumnsEvent.COLUMN_MENU_OPEN); /** * Triggered when the column contextual menu is closed. */ (e: DatagridColumnsEvent.COLUMN_MENU_CLOSE); /** * Emitted when a column header is hovered over. */ (e: 'column-hover', columnKey: BentoColumn); }>(); const CLOSING_CONTEXTUAL_MENU_DELAY_MS = 100; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const { columns, sortBy } = toRefs(props); const allColumnsRef = ref<HTMLDivElement>(); const columnHeaderRefs = ref<DatagridColumnsHeaderReferences>({}); const contextMenuRef = ref({}); const hoveredColumnField = ref(); const closingTimeOutId = ref(); // Focusing const areAnyColumnsFocusable = computed(() => props.columns.some(column => column.sortable || props.hasColumnPanel || column.infoIconTooltip) ); // Selecting const onAllRowSelect = (areAllItemsSelected: boolean) => emit(DatagridColumnsEvent.SELECT_ALL_ROWS, areAllItemsSelected); const isActionsColumn = (column: BentoColumn) => column.field === DATAGRID_ACTIONS_COLUMN_FIELD_NAME; const isAllSelectionState = computed(() => props.selectionState === 'all'); // Select all menu const selectAllMenu = computed(() => [ { text: t('selectPage') as string, handler: () => onAllRowSelect(true), disabled: props.selectionState === 'page' || props.selectionState === 'all', }, props.visibleRowsState?.filtered ? { text: t('selectAllCurrentResults') as string, handler: () => emit(DatagridColumnsEvent.SELECT_ALL_CURRENT_RESULTS), addDivider: props.selectionState !== 'none', disabled: props.selectionState === 'all', } : { text: t('selectAll') as string, handler: () => emit(DatagridColumnsEvent.SELECT_ALL_ACROSS_PAGES), addDivider: props.selectionState !== 'none', disabled: props.selectionState === 'all', }, props.selectionState !== 'none' && { text: t('deselectPage') as string, handler: () => onAllRowSelect(false), addDivider: !props.visibleRowsState?.filtered, // otherwise divider shown in deselect all in current results disabled: props.selectionState === 'unselected-page', }, props.visibleRowsState?.filtered && props.selectionState !== 'none' && { text: t('deselectAllCurrentResults') as string, handler: () => emit(DatagridColumnsEvent.DESELECT_ALL_CURRENT_RESULTS), addDivider: true, disabled: props.selectionState === 'unselected-page' && props.visibleRowsState.allRowsVisible, }, props.selectionState !== 'none' && { text: t('deselectAll') as string, handler: () => emit(DatagridColumnsEvent.DESELECT_ALL), }, ].filter(Boolean) ); // Resizing const { startResize, isResizing, columnHoverEventToEmitAfterResizing } = useResizer(columns, emit); // Sorting const { isSorting, getSortingDirection, sortByColumnField } = useSorter(sortBy, columns, emit); const getSortingColumnDirection = (column: BentoColumn): BentoDatagridAriaSortValue => { if (!column.sortable) { return null; } const sortingDirection = props.sortBy.find(({ field }) => field === column.field)?.direction; if (!sortingDirection) { return BentoDatagridAriaSortValue.NONE; } const ariaSortingDirectionMap: { [key in BentoDatagridSortDirection]: BentoDatagridAriaSortValue } = { [BentoDatagridSortDirection.ASCENDING]: BentoDatagridAriaSortValue.ASCENDING, [BentoDatagridSortDirection.DESCENDING]: BentoDatagridAriaSortValue.DESCENDING, }; return ariaSortingDirectionMap[sortingDirection]; }; // Dragging emit events and methods const draggingEventsForColumn = computed(() => (column: BentoColumn) => { if (!props.hasDraggableColumns) { return {}; } return { dragstart: (e: DragEvent & Event) => { emit(DatagridColumnsEvent.COLUMN_DRAG_START, column, e); }, dragover: (e: DragEvent) => { e.preventDefault(); }, dragenter: (e: DragEvent) => { e.preventDefault(); emit(DatagridColumnsEvent.COLUMN_DRAG_ENTER, column, e); }, drop: () => { emit(DatagridColumnsEvent.COLUMN_DROP, column); }, dragend: () => { emit(DatagridColumnsEvent.COLUMN_DRAG_END); }, }; }); // Contextual menu (moving, visiblity and freezing) const contextualMenuData = computed<(col: BentoColumn) => Array<BentoMenuItem>>( () => (column: BentoColumn) => [ { text: t('moveColumnLeft') as string, handler: () => emit(DatagridColumnsEvent.COLUMN_MOVE, column, -1), icon: ArrowLeftIcon, disabled: props.columns[0].field === column.field, }, { text: t('moveColumnRight') as string, handler: () => emit(DatagridColumnsEvent.COLUMN_MOVE, column, 1), icon: ArrowRightIcon, disabled: props.columns[props.columns.length - 1].field === column.field, }, !isSmallContainer.value && { text: (column?.frozen ? t('unfreezeColumn') : t('freezeColumn')) as string, handler: () => emit(DatagridColumnsEvent.COLUMN_UPDATE, { ...column, frozen: !column?.frozen }), icon: FreezeIcon, }, { text: (column?.visible !== false ? t('hideColumn') : t('showColumn')) as string, handler: () => { emit(DatagridColumnsEvent.COLUMN_UPDATE, { ...column, visible: !(column?.visible !== false), }); }, icon: column?.visible !== false ? HideIcon : ShowIcon, disabled: column?.mandatory || (column?.visible !== false && props.isOnlyOneVisibleColumn), }, ].filter(Boolean) ); const showContextualMenu = (column: BentoColumn) => { hoveredColumnField.value = column.field; if (closingTimeOutId.value) { clearTimeout(closingTimeOutId.value); } }; const hideContextualMenu = (column: BentoColumn) => { // We want to delay closing for a couple of seconds so // it doesn't flicker if a user hovers over the icon and then inside the popover. const timeOutId = setTimeout(() => { if (column.field === hoveredColumnField.value) { hoveredColumnField.value = ''; } }, CLOSING_CONTEXTUAL_MENU_DELAY_MS); closingTimeOutId.value = timeOutId; }; /** * Generates events for a Datagrid Column. * @param column - the BentoColumn we are attaching events onto. */ const dataGridColumnEvents = (column: BentoColumn) => { let events: { [eventName: string]: (e: Event) => void } = { mouseenter: () => { showContextualMenu(column); if (!isResizing.value) { emit('column-hover', column); } else { columnHoverEventToEmitAfterResizing.value = column; } }, mouseleave: () => { hideContextualMenu(column); if (!isResizing.value) { emit('column-hover', null); } else { columnHoverEventToEmitAfterResizing.value = null; } }, focus: () => { showContextualMenu(column); if (isActionsColumn(column)) { focusOnInteractiveChild(columnHeaderRefs.value[column.field]); } }, focusout: (e: FocusEvent) => { if (!allColumnsRef.value?.contains(e?.relatedTarget as Node)) { hideContextualMenu(column); } }, }; if (!isActionsColumn(column)) { events = { ...events, ...draggingEventsForColumn.value(column), }; } return events; }; const hasColumnActions = (column: BentoColumn) => column.infoIconTooltip || column.sortable || props.hasColumnPanel; const registerColumnsRefs = (el: unknown, field: string) => { if (columnHeaderRefs.value?.[field] === undefined) { columnHeaderRefs.value = { ...columnHeaderRefs.value, [field]: null }; } if (el !== null) { columnHeaderRefs.value[field] = el as HTMLDivElement; } }; const isSmallContainer: ComputedRef<boolean> = inject( DATA_GRID_SMALL_CONTAINER, computed(() => false) ); const isTouchDevice: Ref<boolean> = inject(DATA_GRID_TOUCH_DEVICE, ref(false)); // Conditional column styles // Conditional styles const conditionalColumnsClasses = computed(() => { const defaultConditionalClasses = { 'b-data-grid-columns--is-measuring': props.isMeasuringColumnWidths, }; return columns.value[0]?.frozen && !isSmallContainer.value ? { ...defaultConditionalClasses, 'b-data-grid-columns--frozen': true, 'b-data-grid-columns--frozen-separator': props.showFrozenColumnsSeparator, 'b-data-grid-columns--overflow-shadow-left': props.hasLeftOverflow, } : { ...defaultConditionalClasses, 'b-data-grid-columns--unfrozen': true, }; }); const conditionalSelectColumnClasses = computed(() => ({ 'b-data-grid-columns__select-column--all': props.selectableAcrossPages, })); const computeMenuPosition = (hasFirstColumnPanelRightAligned, index) => hasFirstColumnPanelRightAligned && index === 0 ? BentoPopoverPositions.BOTTOM_START : BentoPopoverPositions.BOTTOM_END; const conditionalColumnClasses = (column: BentoColumn, columnIndex: number) => ({ 'b-data-grid-columns__column--column-hovered': column.field === props.hoveredOverColumnState.hoveredOverColumnField, 'b-data-grid-columns__column--column-hovered-no-border-left': column.field === props.hoveredOverColumnState.hoveredOverColumnField && props.hoveredOverColumnState.noBorderLeft, 'b-data-grid-columns__column--column-hovered-no-border-right': column.field === props.hoveredOverColumnState.hoveredOverColumnField && props.hoveredOverColumnState.noBorderRight, 'b-data-grid-columns__column--last-frozen-frozen-column': props.isFrozenColumns && columnIndex === columns.value.length - 1, 'b-data-grid-columns__column--sortable': column.sortable, 'b-data-grid-columns__column--padding-left-unset': column?.padding?.left === false, 'b-data-grid-columns__column--padding-right-unset': column?.padding?.right === false, 'b-data-grid-columns__column--numeric': column?.numeric, 'b-data-grid-columns__column--frozen': column?.frozen, 'b-data-grid-columns__column--is-re-ordering': column?.field === props.draggingColumns?.dragging?.field, 'b-data-grid-columns__column--is-re-ordering-target-left': column?.field === props.draggingColumns?.target?.field && props.draggingColumns?.dragToLeft, 'b-data-grid-columns__column--is-re-ordering-target-right': column?.field === props.draggingColumns?.target?.field && !props.draggingColumns?.dragToLeft, 'b-data-grid-columns__column--with-sorting-or-column-panel': column.sortable || column.infoIconTooltip || props.hasColumnPanel || props.selectableAcrossPages, 'b-data-grid-columns--small-container': isSmallContainer.value, }); const conditionalColumnHeaderAndActionsClasses = (column: BentoColumn) => ({ 'b-data-grid-columns__column-header-and-actions--numeric': column?.numeric, }); const conditionalColumnActionsClasses = (column: BentoColumn) => ({ 'b-data-grid-columns__column-actions--numeric': column?.numeric, }); const conditionalColumnSeparatorClasses = (column: BentoColumn) => ({ 'b-data-grid-columns__column-separator-container--is-resizeable': props.hasResizableColumns, 'b-data-grid-columns__column-separator-container--is-offset': props.hoveredOverColumnState.hoveredOverColumnField === column.field && props.hoveredOverColumnState.isSeparatorOffset, }); const conditionalColumnHeaderClasses = computed(() => ({ 'b-data-grid-columns__column-header--truncated': props.truncateColumnHeaders, })); const conditionalColumnMenuClasses = (column: BentoColumn) => ({ 'b-data-grid-columns__menu--hovered': hoveredColumnField.value === column.field || isTouchDevice.value, }); // Exposed refs defineExpose({ columnHeaderRefs, }); </script> <script lang="ts"> /** * A component to display the BentoDataGrid columns * * @example * * export default { * components: { DataGridColumns }, * template: ` * <data-grid-columns * v-if="columnSet?.length" * :key="`data-grid-columns-${id}`" * :columns="columnSet" * :has-resizable-columns="hasResizableColumns" * :sort-by="sortBy" * :all-items-selected-state="allItemsSelectedState" * :is-frozen-columns="isFrozenColumns" * :show-frozen-columns-separator="showFrozenColumnsSeparator" * :dragging-columns="draggingColumns" * :has-draggable-columns="allowColumnDragAndDrop" * :has-column-panel="columnPanel" * :has-left-overflow="hasLeftOverflow" * :has-first-column-panel-right-aligned="index === computedColumnPanelAlignment" * :is-only-one-visible-column="isOnlyOneVisibleColumn" * :selectable="selectable" * :selectable-across-pages="selectableAcrossPagesComputed" * :truncate-column-headers="truncateColumnHeaders" * v-on="draggingEventsForColumns" * @column-move="onMoveColumnByOffset" * @column-update="onConfigSettingsColumnUpdate" * @column-resize="onColumnResized" * @column-resizing="onColumnWidthOverride" * @select-all-rows="onAllRowSelect" * @select-all-across-pages="emit(BentoDatagridEvent.SELECT_ALL_ACROSS_PAGES)" * @sort="onSort" * > * <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> * <slot :name="innerSlot" v-bind="scope" /> * </template> * </data-grid-columns> * ` * } */ export default { i18n: { messages }, name: 'data-grid-columns', }; </script> <style lang="scss" scoped src="./data-grid-columns.scss" />
@@ -1 +1 @@
1
- <template> <div class="b-data-grid-row-container" role="presentation"> <div v-bind="ariaRowAttributes" :id="rowId?.toString()" :key="rowIndex" class="b-data-grid-row-container__row" :class="conditionalRowClasses(rowData, rowIndex)" role="row" :aria-selected="getRowAriaSelectedValue(rowData)" @click="onRowClick" @keyup.enter="onRowClick" @keyup.space="onRowClick" > <template v-for="({ columnSet, id, isFrozenColumns }, columnSetIndex) of columnSets"> <div v-if="columnSet?.length" :key="`data-grid-rows-${id}`" class="b-data-grid-row-container__cells" :class="conditionalRowSetClasses(isFrozenColumns, isSmallContainer)" :data-testid="`data-grid-${id}-rows`" > <data-grid-cell v-for="(column, colIndex) in columnSet" :key="`cell-${column.field}`" :ref="el => registerRowCellRef(el, column.field)" class="b-data-grid-row-container__cell" :column="column" :column-set-index="columnSetIndex" :col-index="colIndex" :column-sets="columnSets" :condensed="condensed" :dragging-columns="draggingColumns" :dragging-events-for-row-cell="draggingEventsForRowCell" :get-data-item-id="getDataItemId" :has-zebra-background="isRowEven" :is-highlighted="isHighlightedRow(rowData)" :is-selected="isSelectedRow(rowData)" :hovered-over-column-state="hoveredOverColumnState" :is-data-item-disabled="isDataItemDisabled" :is-measuring-column-widths="isMeasuringColumnWidths" :is-nested-content-expanded="isNestedContentShown" :nested-content="shouldShowNestedContentToggle" :nested-rows-element-ids="allNestedRowIds" :row-data="rowData" :row-index="rowIndex" :selection-is-indeterminate="hasUnSelectedNestedRows" :selectable="selectable" :selected-items="selectedItems" @cell-hovered="emit('cell-hovered', $event)" @cell-focus="emit('cell-focus', $event)" @on-frozen-columns-key-press="emit('on-frozen-columns-key-press', $event)" @select-row="onRowSelect" @nested-content-toggled="nestedContentToggled" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-cell> </div> <div v-if="!isFrozenColumns && rowActions" :key="`data-grid-row-actions-${id}`" ref="rowActionsRef" :aria-colindex="sumColumnAmountsInColumnSets().toString()" class="b-data-grid-row-container__row-actions-container" :class="conditionalRowActionsContainerClasses" :style="computedRowActionsStyles" :data-cellindex="getRowActionsCellIndex()" tabindex="-1" role="gridcell" @focus="onRowActionsCellFocus" @focusOut="onRowActionsCellFocusOut" @keydown="onRowActionsCellOnKeyDown" > <data-grid-cell-actions class="b-data-grid-row-container__row-actions" disable-arrow-down-opening-menu :cell-actions="rowActions" :has-right-overflow="hasRightOverflow" :row-data-item="rowData" teleport-menu @menu-open="onRowActionMenuOpen" @menu-close="onRowActionMenuClose" /> </div> </template> </div> <Transition v-if=" (hasNestedContent && hasSlot('nested-slot') && shouldShowNestedContent(rowData)) || (hasNestedRows && shouldShowNestedRowsBooleanResult) " :key="`${rowIndex}-nest-content`" name="b-data-grid-row-container__animation--nested-content" > <div v-if="isNestedContentShown && shouldShowRenderNestedRows" :class="conditionalNestedRowClasses" data-testid="nested-content-container" :style="nestedContentStyles" role="presentation" > <div ref="nestedContentRef"> <div v-if="isLoadingNestedRows" class="b-data-grid-row-container__row-nested-rows-loading-container" > <bento-loading-indicator /> </div> <data-grid-row v-for="(childRowData, childRowIndex) in rowData.children" v-bind="props" :id="getRowId(childRowIndex)" :key="`child-${childRowIndex}`" :row-data="childRowData" :row-index="childRowIndex" :row-set-size="rowData.children.length" is-nested-row @cell-hovered="emit('cell-hovered', $event)" @cell-focus="emit('cell-focus', $event)" @row-click="emitRowClick" @on-frozen-columns-key-press="emit('on-frozen-columns-key-press', $event)" @select-row="onNestedRowSelect(childRowData, $event)" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-row> </div> </div> <div v-else-if="isNestedContentShown" v-bind="nestedContentContainerDivAriaAttributes" :class="conditionalNestedRowClasses" data-testid="nested-content-container" :style="nestedContentStyles" role="row" tabindex="-1" > <div ref="nestedContentRef" role="gridcell"> <div v-if="isLoadingNestedRows" class="b-data-grid-row-container__row-nested-rows-loading-container" > <bento-loading-indicator /> </div> <div v-else-if="hasSlot('nested-slot') && shouldShowNestedContent(rowData)"> <slot :item="rowData" name="nested-slot" /> </div> </div> </div> </Transition> </div> </template> <script setup lang="ts"> import { type ComponentPublicInstance, computed, type ComputedRef, inject, onMounted, type PropType, ref, useSlots, watch, } from 'vue'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { focusOnInteractiveChild, makeAllInteractiveChildrenFocusable, makeAllInteractiveChildrenUnFocusable, } from '@/components/data-grid/utils'; import DataGridRow from '@/components/data-grid/components/data-grid-row/data-grid-row.vue'; import DataGridCell from '@/components/data-grid/components/data-grid-cell/data-grid-cell.vue'; import DataGridCellActions from '@/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.vue'; // Types and enums import { type BentoColumn, type BentoDatagridDataItem, type BentoDatagridDataItemId, type BentoDatagridGetDataItemIdFn, type BentoDatagridIsDataItemDisabledFn, type BentoDatagridShouldShowNestedContentFn, type BentoDatagridShouldShowNestedRowsFn, BentoDatagridStyleState, } from '@/components/data-grid/data-grid.types'; import type { DataGridCellColumnSet, DataGridCellDragEventsForColumn, } from '../data-grid-cell/data-grid-cell.types'; import type { BentoDatagridDraggingColumnState } from '../data-grid-columns/data-grid-columns.types'; import type { DatagridColumnHoveringState } from '@/components/data-grid/composables/use-column-hovering'; // Translations import { type Booleanish } from '@/types/prop-types'; import { useHasSlot } from '@/composables/use-has-slot/use-has-slot'; import { useExpandableContentHeight } from '@/composables/use-expandable-content-height/use-expandable-content-height'; import { observeSizeOfElement } from '@/utils/ts/resize'; import { DATAGRID_ACTIONS_COLUMN_FIELD_NAME, DATAGRID_CELL_OFFSET_PADDING, } from '@/components/data-grid/composables/use-calculate-column-width/use-calculate-column-width'; import { generateUid } from '@/core/utils/ts'; import { type BentoDataGridRowActionsProp } from '@/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.types'; import type { HTMLAttributes } from 'vue/types/jsx'; import { EventKey } from '@/types/utils-events'; import { DATA_GRID_SMALL_CONTAINER } from '@/components/data-grid/data-grid.keys'; const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = defineProps({ /** * Row actions property that generates which button to show in row actions area */ rowActions: { type: [Function, Array] as PropType<BentoDataGridRowActionsProp>, default: undefined, }, /** * When true, will emit a 'rowClick' event when a row is clicked. Will add cursor: pointer to rows. */ allowRowClicks: { type: Boolean, default: false }, /** * The columns sets including */ columnSets: { type: Array as PropType<Array<DataGridCellColumnSet>>, required: true, }, /** * Sets the row cells to the condensed padding state. */ condensed: { type: Boolean, default: false, }, /** * Columns being dragged, contains dragging column and a target column */ draggingColumns: { type: Object as PropType<BentoDatagridDraggingColumnState>, default: undefined, }, /** * Dragging event handlers for each row cell */ draggingEventsForRowCell: { type: Function as PropType<DataGridCellDragEventsForColumn>, required: true, }, /** * A customisable method to retrieve a unique identifier for a data item in the array of data supplied. * This is used selecting and performance reasons and must exist in each data item. */ getDataItemId: { type: Function as PropType<BentoDatagridGetDataItemIdFn>, default: (item: BentoDatagridDataItem) => item.id, }, /** * Set to true if the datagrid has been scrolled and the left side has overflowing space. */ hasLeftOverflow: { type: Boolean, default: false, }, /** * Enables the nested content slot functionality. */ hasNestedContent: { type: Boolean, default: false, }, /** * Enables the nested content slot functionality. */ hasNestedRows: { type: Boolean, default: false, }, /** * Set to true if the datagrid has been scrolled and the right side has overflowing space. */ hasRightOverflow: { type: Boolean, default: false, }, /** * Highlighted row IDs. */ highlightedRows: { type: Array as PropType<Array<BentoDatagridDataItemId>>, default: () => [], }, /** * The row index of a currently hovered row. */ hoveredRowIndex: { type: Number, default: -1, }, /** * The column hovered over state and whether the hovered over column should show a border right or left. */ hoveredOverColumnState: { type: Object as PropType<DatagridColumnHoveringState>, required: true, }, /** * A customisable method to determine if a row is not selectable. * The default is that every row is selectable. */ isDataItemDisabled: { type: Function as PropType<BentoDatagridIsDataItemDisabledFn>, default: () => false, }, /** * If the row is of an even number. * Rows are 0th based index e.g. index 0 is an odd-numbered row. */ isRowEven: { type: Boolean, default: false, }, /** * If the row is the last child in it's parent container. */ isRowLastChild: { type: Boolean, default: false, }, /** * If columns are being measured for the autoWidth functionality. */ isMeasuringColumnWidths: { type: Boolean, default: false, }, /** * Determines if this row is nested inside another row. */ isNestedRow: { type: Boolean, default: false, }, /** * The maximum width of the row actions in all rows */ rowActionsMaximumWidth: { type: Number, default: 0, }, /** * The row data object */ rowData: { type: Object as PropType<BentoDatagridDataItem>, required: true, }, /** * The row index. */ rowIndex: { type: Number, required: true, }, /** * The row ID to be using for the ID attribute. */ rowId: { type: Number, default: undefined, }, /** * The number of rows that this row is within. */ rowSetSize: { type: Number, default: 0, }, /** * Determines whether to render frozen seperator on the row. */ showFrozenColumnsSeparator: { type: Boolean, default: false, }, /** * Determines if the row selection is enabled. */ selectable: { type: Boolean, default: false, }, /** * The array of IDs for the selected rows. */ selectedItems: { type: Array as PropType<Array<BentoDatagridDataItemId>>, required: true, }, /** * Enables / Disables the nested-content slot */ shouldShowNestedContent: { type: Function as PropType<BentoDatagridShouldShowNestedContentFn>, default: (_item: BentoDatagridDataItem) => true, }, /** * Returns the nested rows for a given BentoDatagridDataItem. */ shouldShowNestedRows: { type: Function as PropType<BentoDatagridShouldShowNestedRowsFn>, default: (_item: BentoDatagridDataItem) => true, }, }); const emit = defineEmits<{ /** * Emitted when a cell has been hovered over. This is used to keep the row in a hovered state when hovering over a menu. */ (e: 'cell-hovered', value: number); /** * Emitted when an a frozen column cell fires a key up or down event. */ (e: 'on-frozen-columns-key-press', event: Event); /** * Emitted when a row has been selected. */ (e: 'select-row', value: Array<BentoDatagridDataItemId>, rowData: BentoDatagridDataItem); /** * Emitted when a nested row has been selected. */ ( e: 'select-nested-row', value: Array<BentoDatagridDataItemId>, parentRowData: BentoDatagridDataItem, rowData: BentoDatagridDataItem ); /** * Emitted when a row has been clicked. */ (e: 'row-click', rowData: BentoDatagridDataItem, event: MouseEvent | KeyboardEvent); /** * Emitted when a row with nested content is toggled */ (e: 'nested-content-toggled', rowData: BentoDatagridDataItem); /** * Emitted when a row cell is focused. */ ( e: 'cell-focus', value: { rowData: BentoDatagridDataItem; column: BentoColumn; rowIndex: number; colIndex: number } ); }>(); // Refs const gridcellRefs = ref<{ [key: string]: HTMLDivElement }>({}); const isNestedContentShown = ref(false); const nestedContentRef = ref(); const rowActionsRef = ref(); const nestedContentSlotId = generateUid('nested-content-slot'); const isRowActionMenuOpen = ref(false); const focusableItems = ref<Array<{ node: HTMLElement }>>(); onMounted(() => { if (rowActionsRef.value) { focusableItems.value = makeAllInteractiveChildrenUnFocusable(rowActionsRef.value); } }); const shouldShowNestedRowsBooleanResult = computed(() => props.shouldShowNestedRows(props.rowData)); const shouldShowRenderNestedRows = computed( () => !props.isNestedRow && props.hasNestedRows && shouldShowNestedRowsBooleanResult.value && props.rowData?.children?.length > 0 ); const isLoadingNestedRows = computed( () => !props.isNestedRow && props.hasNestedRows && shouldShowNestedRowsBooleanResult.value && (props.rowData?.children === undefined || props?.rowData?.children.length === 0) ); const shouldShowNestedContentToggle = computed( () => (props.hasNestedContent && props.shouldShowNestedContent(props.rowData)) || (props.hasNestedRows && shouldShowNestedRowsBooleanResult.value && !props.isNestedRow) ); const ariaRowAttributes = computed<HTMLAttributes>(() => { if (props.hasNestedContent || props.hasNestedRows) { const isNestedContentShownBooleanishVal = isNestedContentShown.value.toString() as Booleanish; return { 'aria-expanded': shouldShowNestedContentToggle.value ? isNestedContentShownBooleanishVal : undefined, 'aria-level': props.isNestedRow ? '2' : '1', 'aria-posinset': props.rowIndex + 1, 'aria-setsize': props.rowSetSize, }; } return { 'aria-rowindex': props.rowIndex + 1 }; }); const nestedContentContainerDivAriaAttributes = computed<HTMLAttributes>(() => { if (isLoadingNestedRows.value) { return { 'aria-busy': 'true', 'aria-live': 'polite', }; } if ( shouldShowRenderNestedRows.value || !props.hasNestedContent || !props.shouldShowNestedContent(props.rowData) ) { return undefined; } return { 'aria-level': props.isNestedRow ? '3' : '2', 'aria-posinset': '1', 'aria-setsize': '1', id: nestedContentSlotId, role: 'row', tabindex: '-1', }; }); const nestedRowIds = computed(() => { if (shouldShowRenderNestedRows.value) { return props.rowData?.children?.reduce((acc, _val, index) => { acc[index] = generateUid('nested-row'); return acc; }, {}); } return []; }); const allNestedRowIds = computed(() => { if (!isNestedContentShown.value) { return undefined; } if (props.hasNestedContent && props.shouldShowNestedContent(props.rowData)) { return [nestedContentSlotId]; } if (shouldShowRenderNestedRows.value && Object.keys(nestedRowIds.value)?.length > 0) { const generatedRowIds = Object.values(nestedRowIds.value); if (generatedRowIds?.length > 0) { return generatedRowIds; } } return []; }); const hasUnSelectedNestedRows = computed(() => { const childrendDataItemIds = props.rowData?.children?.map(nestedItem => props.getDataItemId(nestedItem)); if (childrendDataItemIds?.length > 0) { const everyChildIsSelected = childrendDataItemIds.every(childId => props?.selectedItems?.includes(childId)); const someItemsAreSelected = childrendDataItemIds.some(childId => props?.selectedItems?.includes(childId)); return !everyChildIsSelected && someItemsAreSelected; } return false; }); // Methods const registerRowCellRef = (el: Element | ComponentPublicInstance, field: string) => { // We always know this will be used on a DataGridCell component so we can cast it const gridCellComponent = el as InstanceType<typeof DataGridCell>; if (el !== null && gridCellComponent?.gridcellRef) { gridcellRefs.value[field] = gridCellComponent.gridcellRef as HTMLDivElement; } }; const getRowId = (childRowIndex: number) => { return nestedRowIds.value[childRowIndex]; }; const getRowAriaSelectedValue = (row: BentoDatagridDataItem): Booleanish => { if (!props.selectable) { return null; } const id = props.getDataItemId(row); return props.selectedItems.includes(id).toString() as Booleanish; }; const emitRowClick = (rowData: BentoDatagridDataItem, event: MouseEvent | KeyboardEvent) => { emit('row-click', rowData, event); }; /** * Handles row clicks when the `allowRowClicks` prop is true. * * This method also prevents the `row-click` event from firing if the click * originated from an interactive element (like a button, input, or link) * within the row. This is to avoid unintended behavior when a user * interacts with controls inside a row. * * @param event The mouse or keyboard event from the click or key press. */ const onRowClick = (event: MouseEvent | KeyboardEvent) => { // Only proceed if allowRowClicks is true if (!props.allowRowClicks) { return; } let isEventTargetInteractiveElement = false; const dataGridRowInteractiveTypes = ['button', 'input', 'a']; for (let i = 0; i < dataGridRowInteractiveTypes.length; i++) { if ((event?.target as HTMLElement)?.closest(dataGridRowInteractiveTypes[i])) { isEventTargetInteractiveElement = true; break; } } // Only trigger if element is NOT interactive if (!isEventTargetInteractiveElement) { emitRowClick(props.rowData, event); } }; const onRowSelect = (value: Array<BentoDatagridDataItemId>) => { emit('select-row', value, props.rowData); }; const onNestedRowSelect = (rowData: BentoDatagridDataItem, value: Array<BentoDatagridDataItemId>) => { emit('select-nested-row', value, props.rowData, rowData); }; /** * Sets the state to indicate that a row action menu is open. This is used to apply a higher * z-index to the row's action container, ensuring the menu displays correctly above * subsequent rows. */ const onRowActionMenuOpen = () => { isRowActionMenuOpen.value = true; }; /** * Resets the state when a row action menu is closed, removing the elevated z-index from the * row's action container. */ const onRowActionMenuClose = () => { isRowActionMenuOpen.value = false; }; /** * When the row actions cell is focused, make the first interactive child focusable and focus it. * This allows for keyboard navigation into the cell's actions. */ const onRowActionsCellFocus = () => { makeAllInteractiveChildrenFocusable([focusableItems.value?.[0]]); focusOnInteractiveChild(rowActionsRef.value); }; /** * When focus leaves the row actions cell, make all interactive children unfocusable. * This ensures that tabbing through the grid skips over the individual actions within the cell. */ const onRowActionsCellFocusOut = () => { makeAllInteractiveChildrenUnFocusable(rowActionsRef.value); }; /** * Handles keyboard navigation within the row actions cell. * Allows using left and right arrow keys to move between interactive elements (e.g., buttons) in the cell. * @param e The keyboard event. */ const onRowActionsCellOnKeyDown = (e: KeyboardEvent) => { if (e.key !== EventKey.TAB) { makeAllInteractiveChildrenUnFocusable(rowActionsRef.value); } if ( e.key === EventKey.ARROW_RIGHT && focusableItems.value?.length > 1 && focusableItems.value[0].node === document.activeElement ) { makeAllInteractiveChildrenUnFocusable(rowActionsRef.value); focusableItems.value[1].node.setAttribute('tabindex', '0'); focusableItems.value[1].node.focus(); e.stopPropagation(); e.preventDefault(); } else if ( e.key === EventKey.ARROW_LEFT && focusableItems.value?.length > 1 && focusableItems.value[1].node === document.activeElement ) { makeAllInteractiveChildrenUnFocusable(rowActionsRef.value); focusableItems.value[0].node.setAttribute('tabindex', '0'); focusableItems.value[0].node.focus(); e.stopPropagation(); e.preventDefault(); } }; // Cell and Columns index calculation const sumColumnAmountsInColumnSets = () => props.columnSets.reduce((total, { columnSet }) => total + columnSet.length, 0); const getRowActionsCellIndex = () => sumColumnAmountsInColumnSets() * props.rowIndex + sumColumnAmountsInColumnSets(); // Nested content expanding logic const { updateContentHeight, contentHeight } = useExpandableContentHeight(nestedContentRef, isNestedContentShown); const nestedContentStyles = computed(() => { if (props.hasNestedContent || props.hasNestedRows) { // calculate the padding by getting the action column width and the first column left padding: const paddingLeft = shouldShowRenderNestedRows.value || isLoadingNestedRows.value ? 0 : gridcellRefs.value[DATAGRID_ACTIONS_COLUMN_FIELD_NAME].offsetWidth + DATAGRID_CELL_OFFSET_PADDING; return { paddingLeft: `${paddingLeft}px`, 'max-height': (props.hasNestedContent || props.hasNestedRows) && contentHeight.value && !contentHeight?.value?.includes('null') ? contentHeight.value : 0, }; } return undefined; }); watch( () => nestedContentRef.value, () => { let observer: ResizeObserver | null = null; if (!observer && nestedContentRef.value) { observer = observeSizeOfElement(nestedContentRef.value, () => { if (nestedContentRef.value) { updateContentHeight(); } }); } } ); const nestedContentToggled = () => { isNestedContentShown.value = !isNestedContentShown.value; updateContentHeight(); emit('nested-content-toggled', props.rowData); }; watch( () => props.rowData?.children, (_newChildren, oldChildren) => { if (props?.rowData?.children?.length > 0 && oldChildren === undefined && !isNestedContentShown.value) { updateContentHeight(); } } ); const isSmallContainer: ComputedRef<boolean> = inject( DATA_GRID_SMALL_CONTAINER, computed(() => false) ); const isHighlightedRow = row => props.highlightedRows.includes(props.getDataItemId(row)); const isSelectedRow = row => props.selectedItems.includes(props.getDataItemId(row)); // Styling const conditionalRowClasses = (row: BentoDatagridDataItem, rowIndex: number) => ({ 'b-data-grid-row-container__row--disabled': props.isDataItemDisabled(row), 'b-data-grid-row-container__row--row-clicks-enabled': props.allowRowClicks, 'b-data-grid-row-container__row--highlighted': isHighlightedRow(row), 'b-data-grid-row-container__row--selected': isSelectedRow(row), 'b-data-grid-row-container__row--hovered': props.hoveredRowIndex === rowIndex, 'b-data-grid-row-container__row--is-last-child': props.isRowLastChild && !isNestedContentShown.value, 'b-data-grid-row-container__row--nested-row': props.isNestedRow, 'b-data-grid-row-container__row--zebra': props.isRowEven, }); const conditionalRowSetClasses = (isFrozenRowSet: boolean, isSmallContainer: boolean) => ({ 'b-data-grid-row-container__row-cell-set--frozen': isFrozenRowSet && !isSmallContainer, [`b-data-grid-row-container__row-cell-set--${BentoDatagridStyleState.OVERFLOW_SHADOW_LEFT}`]: !isSmallContainer && isFrozenRowSet && props.hasLeftOverflow, 'b-data-grid-row-container__row-cell-set--frozen-separator': !isSmallContainer && isFrozenRowSet && props.showFrozenColumnsSeparator, 'b-data-grid-row-container__row-cell-set--unfrozen': !isFrozenRowSet || isSmallContainer, 'b-data-grid-row-container__row-cell-set--is-measuring': props.isMeasuringColumnWidths, }); const conditionalNestedRowClasses = computed(() => ({ 'b-data-grid-row-container__row-nested-slot--is-last-child': props.isRowLastChild, 'b-data-grid-row-container__row-nested-slot': !shouldShowRenderNestedRows.value && props.shouldShowNestedContent(props.rowData), })); const conditionalRowActionsContainerClasses = computed(() => ({ 'b-data-grid-row-container__row-actions-container--row-actions-menu-open': isRowActionMenuOpen.value, })); const computedRowActionsStyles = computed(() => ({ minWidth: `${props.rowActionsMaximumWidth}px`, })); // Exposed refs defineExpose({ gridcellRefs, rowActionsRef, }); </script> <script lang="ts"> /** * Internal component to display the BentoDataGrid data rows containig the data row cells. * * @example * * export default { * components: { DataGridRow }, * template: ` * <data-grid-row * :ref="el => registerRowCellRefs(el, rowIndex)" * :style="gridColumnWidthStyle" * :allow-row-clicks="allowRowClicks" * :column-sets="columnSets" * :condensed="condensed" * :dragging-columns="draggingColumns" * :dragging-events-for-row-cell="draggingEventsForRowCell" * :get-data-item-id="getDataItemId" * :has-left-overflow="hasLeftOverflow" * :highlighted-rows="highlightedRows" * :hovered-row-index="hoveredRowIndex" * :hovered-over-column-state="hoveredOverColumnState" * :is-data-item-disabled="isDataItemDisabled" * :is-measuring-column-widths="isMeasuringColumnWidths" * :row-data="rowData" * :row-index="rowIndex" * :show-frozen-columns-separator="showFrozenColumnsSeparator" * :selectable="selectable" * :selected-items="selectedItems" * @cell-hovered="onCellHover" * @row-click="onRowClick" * @on-frozen-columns-key-press="updateScrollPositionOnFrozenColumnsNavigation" * @select-row="onRowSelect" * > * <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> * <slot :name="innerSlot" v-bind="scope" /> * </template> * </data-grid-row> * ` */ export default { name: 'data-grid-row', }; </script> <style lang="scss" scoped src="./data-grid-row.scss" />
1
+ <template> <div class="b-data-grid-row-container" role="presentation"> <div v-bind="ariaRowAttributes" :id="rowId?.toString()" :key="rowIndex" class="b-data-grid-row-container__row" :class="conditionalRowClasses(rowData, rowIndex)" role="row" :aria-selected="getRowAriaSelectedValue(rowData)" @click="onRowClick" @keyup.enter="onRowClick" @keyup.space="onRowClick" > <template v-for="({ columnSet, id, isFrozenColumns }, columnSetIndex) of columnSets"> <div v-if="columnSet?.length" :key="`data-grid-rows-${id}`" class="b-data-grid-row-container__cells" :class="conditionalRowSetClasses(isFrozenColumns, isSmallContainer)" :data-testid="`data-grid-${id}-rows`" > <data-grid-cell v-for="(column, colIndex) in columnSet" :key="`cell-${column.field}`" :ref="el => registerRowCellRef(el, column.field)" class="b-data-grid-row-container__cell" :column="column" :column-set-index="columnSetIndex" :col-index="colIndex" :column-sets="columnSets" :condensed="condensed" :dragging-columns="draggingColumns" :dragging-events-for-row-cell="draggingEventsForRowCell" :get-data-item-id="getDataItemId" :has-zebra-background="isRowEven" :is-highlighted="isHighlightedRow(rowData)" :is-selected="isSelectedRow(rowData)" :hovered-over-column-state="hoveredOverColumnState" :is-data-item-disabled="isDataItemDisabled" :is-measuring-column-widths="isMeasuringColumnWidths" :is-nested-content-expanded="isNestedContentShown" :nested-content="shouldShowNestedContentToggle" :nested-rows-element-ids="allNestedRowIds" :row-data="rowData" :row-index="rowIndex" :selection-is-indeterminate="hasUnSelectedNestedRows" :selectable="selectable" :selected-items="selectedItems" @cell-hovered="emit('cell-hovered', $event)" @cell-focus="emit('cell-focus', $event)" @on-frozen-columns-key-press="emit('on-frozen-columns-key-press', $event)" @select-row="onRowSelect" @nested-content-toggled="nestedContentToggled" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-cell> </div> <div v-if="!isFrozenColumns && rowActions" :key="`data-grid-row-actions-${id}`" ref="rowActionsRef" :aria-colindex="sumColumnAmountsInColumnSets().toString()" class="b-data-grid-row-container__row-actions-container" :class="conditionalRowActionsContainerClasses" :style="computedRowActionsStyles" :data-cellindex="getRowActionsCellIndex()" tabindex="-1" role="gridcell" @focus="onRowActionsCellFocus" @focusOut="onRowActionsCellFocusOut" @keydown="onRowActionsCellOnKeyDown" > <data-grid-cell-actions class="b-data-grid-row-container__row-actions" disable-arrow-down-opening-menu :cell-actions="rowActions" :has-right-overflow="hasRightOverflow" :row-data-item="rowData" :has-zebra-background="isRowEven" :is-highlighted="isHighlightedRow(rowData)" :is-selected="isSelectedRow(rowData)" teleport-menu @menu-open="onRowActionMenuOpen" @menu-close="onRowActionMenuClose" /> </div> </template> </div> <Transition v-if=" (hasNestedContent && hasSlot('nested-slot') && shouldShowNestedContent(rowData)) || (hasNestedRows && shouldShowNestedRowsBooleanResult) " :key="`${rowIndex}-nest-content`" name="b-data-grid-row-container__animation--nested-content" > <div v-if="isNestedContentShown && shouldShowRenderNestedRows" :class="conditionalNestedRowClasses" data-testid="nested-content-container" :style="nestedContentStyles" role="presentation" > <div ref="nestedContentRef"> <div v-if="isLoadingNestedRows" class="b-data-grid-row-container__row-nested-rows-loading-container" > <bento-loading-indicator /> </div> <data-grid-row v-for="(childRowData, childRowIndex) in rowData.children" v-bind="props" :id="getRowId(childRowIndex)" :key="`child-${childRowIndex}`" :ref="el => registerNestedRowRef(el, childRowIndex)" :row-data="childRowData" :row-index="childRowIndex" :row-set-size="rowData.children.length" is-nested-row @cell-hovered="emit('cell-hovered', $event)" @cell-focus="emit('cell-focus', $event)" @row-click="emitRowClick" @on-frozen-columns-key-press="emit('on-frozen-columns-key-press', $event)" @select-row="onNestedRowSelect(childRowData, $event)" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-row> </div> </div> <div v-else-if="isNestedContentShown" v-bind="nestedContentContainerDivAriaAttributes" :class="conditionalNestedRowClasses" data-testid="nested-content-container" :style="nestedContentStyles" role="row" tabindex="-1" > <div ref="nestedContentRef" role="gridcell"> <div v-if="isLoadingNestedRows" class="b-data-grid-row-container__row-nested-rows-loading-container" > <bento-loading-indicator /> </div> <div v-else-if="hasSlot('nested-slot') && shouldShowNestedContent(rowData)"> <slot :item="rowData" name="nested-slot" /> </div> </div> </div> </Transition> </div> </template> <script setup lang="ts"> import { type ComponentPublicInstance, computed, type ComputedRef, inject, onMounted, type PropType, ref, useSlots, watch, } from 'vue'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { focusOnInteractiveChild, makeAllInteractiveChildrenFocusable, makeAllInteractiveChildrenUnFocusable, } from '@/components/data-grid/utils'; import DataGridRow from '@/components/data-grid/components/data-grid-row/data-grid-row.vue'; import DataGridCell from '@/components/data-grid/components/data-grid-cell/data-grid-cell.vue'; import DataGridCellActions from '@/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.vue'; // Types and enums import { type BentoColumn, type BentoDatagridDataItem, type BentoDatagridDataItemId, type BentoDatagridGetDataItemIdFn, type BentoDatagridIsDataItemDisabledFn, type BentoDatagridShouldShowNestedContentFn, type BentoDatagridShouldShowNestedRowsFn, BentoDatagridStyleState, } from '@/components/data-grid/data-grid.types'; import type { DataGridCellColumnSet, DataGridCellDragEventsForColumn, } from '../data-grid-cell/data-grid-cell.types'; import type { BentoDatagridDraggingColumnState } from '../data-grid-columns/data-grid-columns.types'; import type { DatagridColumnHoveringState } from '@/components/data-grid/composables/use-column-hovering'; // Translations import { type Booleanish } from '@/types/prop-types'; import { useHasSlot } from '@/composables/use-has-slot/use-has-slot'; import { useExpandableContentHeight } from '@/composables/use-expandable-content-height/use-expandable-content-height'; import { observeSizeOfElement } from '@/utils/ts/resize'; import { DATAGRID_ACTIONS_COLUMN_FIELD_NAME, DATAGRID_CELL_OFFSET_PADDING, } from '@/components/data-grid/composables/use-calculate-column-width/use-calculate-column-width'; import { generateUid } from '@/core/utils/ts'; import { type BentoDataGridRowActionsProp } from '@/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.types'; import type { HTMLAttributes } from 'vue/types/jsx'; import { EventKey } from '@/types/utils-events'; import { DATA_GRID_SMALL_CONTAINER } from '@/components/data-grid/data-grid.keys'; const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = defineProps({ /** * Row actions property that generates which button to show in row actions area */ rowActions: { type: [Function, Array] as PropType<BentoDataGridRowActionsProp>, default: undefined, }, /** * When true, will emit a 'rowClick' event when a row is clicked. Will add cursor: pointer to rows. */ allowRowClicks: { type: Boolean, default: false }, /** * The columns sets including */ columnSets: { type: Array as PropType<Array<DataGridCellColumnSet>>, required: true, }, /** * Sets the row cells to the condensed padding state. */ condensed: { type: Boolean, default: false, }, /** * Columns being dragged, contains dragging column and a target column */ draggingColumns: { type: Object as PropType<BentoDatagridDraggingColumnState>, default: undefined, }, /** * Dragging event handlers for each row cell */ draggingEventsForRowCell: { type: Function as PropType<DataGridCellDragEventsForColumn>, required: true, }, /** * A customisable method to retrieve a unique identifier for a data item in the array of data supplied. * This is used selecting and performance reasons and must exist in each data item. */ getDataItemId: { type: Function as PropType<BentoDatagridGetDataItemIdFn>, default: (item: BentoDatagridDataItem) => item.id, }, /** * Set to true if the datagrid has been scrolled and the left side has overflowing space. */ hasLeftOverflow: { type: Boolean, default: false, }, /** * Enables the nested content slot functionality. */ hasNestedContent: { type: Boolean, default: false, }, /** * Enables the nested content slot functionality. */ hasNestedRows: { type: Boolean, default: false, }, /** * Set to true if the datagrid has been scrolled and the right side has overflowing space. */ hasRightOverflow: { type: Boolean, default: false, }, /** * Highlighted row IDs. */ highlightedRows: { type: Array as PropType<Array<BentoDatagridDataItemId>>, default: () => [], }, /** * The row index of a currently hovered row. */ hoveredRowIndex: { type: Number, default: -1, }, /** * The column hovered over state and whether the hovered over column should show a border right or left. */ hoveredOverColumnState: { type: Object as PropType<DatagridColumnHoveringState>, required: true, }, /** * A customisable method to determine if a row is not selectable. * The default is that every row is selectable. */ isDataItemDisabled: { type: Function as PropType<BentoDatagridIsDataItemDisabledFn>, default: () => false, }, /** * If the row is of an even number. * Rows are 0th based index e.g. index 0 is an odd-numbered row. */ isRowEven: { type: Boolean, default: false, }, /** * If the row is the last child in it's parent container. */ isRowLastChild: { type: Boolean, default: false, }, /** * If columns are being measured for the autoWidth functionality. */ isMeasuringColumnWidths: { type: Boolean, default: false, }, /** * Determines if this row is nested inside another row. */ isNestedRow: { type: Boolean, default: false, }, /** * The maximum width of the row actions in all rows */ rowActionsMaximumWidth: { type: Number, default: 0, }, /** * The row data object */ rowData: { type: Object as PropType<BentoDatagridDataItem>, required: true, }, /** * The row index. */ rowIndex: { type: Number, required: true, }, /** * The row ID to be using for the ID attribute. */ rowId: { type: Number, default: undefined, }, /** * The number of rows that this row is within. */ rowSetSize: { type: Number, default: 0, }, /** * Determines whether to render frozen seperator on the row. */ showFrozenColumnsSeparator: { type: Boolean, default: false, }, /** * Determines if the row selection is enabled. */ selectable: { type: Boolean, default: false, }, /** * The array of IDs for the selected rows. */ selectedItems: { type: Array as PropType<Array<BentoDatagridDataItemId>>, required: true, }, /** * Enables / Disables the nested-content slot */ shouldShowNestedContent: { type: Function as PropType<BentoDatagridShouldShowNestedContentFn>, default: (_item: BentoDatagridDataItem) => true, }, /** * Returns the nested rows for a given BentoDatagridDataItem. */ shouldShowNestedRows: { type: Function as PropType<BentoDatagridShouldShowNestedRowsFn>, default: (_item: BentoDatagridDataItem) => true, }, }); const emit = defineEmits<{ /** * Emitted when a cell has been hovered over. This is used to keep the row in a hovered state when hovering over a menu. */ (e: 'cell-hovered', value: number); /** * Emitted when an a frozen column cell fires a key up or down event. */ (e: 'on-frozen-columns-key-press', event: Event); /** * Emitted when a row has been selected. */ (e: 'select-row', value: Array<BentoDatagridDataItemId>, rowData: BentoDatagridDataItem); /** * Emitted when a nested row has been selected. */ ( e: 'select-nested-row', value: Array<BentoDatagridDataItemId>, parentRowData: BentoDatagridDataItem, rowData: BentoDatagridDataItem ); /** * Emitted when a row has been clicked. */ (e: 'row-click', rowData: BentoDatagridDataItem, event: MouseEvent | KeyboardEvent); /** * Emitted when a row with nested content is toggled */ (e: 'nested-content-toggled', rowData: BentoDatagridDataItem); /** * Emitted when a row cell is focused. */ ( e: 'cell-focus', value: { rowData: BentoDatagridDataItem; column: BentoColumn; rowIndex: number; colIndex: number } ); }>(); /** * Checks if the relatedTarget element appears after the reference element in DOM order. * Used to detect backward navigation (Shift+Tab). */ const isRelatedTargetAfterInDOM = (relatedTarget: HTMLElement | null, referenceEl: HTMLElement): boolean => { if (!relatedTarget || !referenceEl) { return false; } return !!(referenceEl.compareDocumentPosition(relatedTarget) & Node.DOCUMENT_POSITION_FOLLOWING); }; // Refs const gridcellRefs = ref<{ [key: string]: HTMLDivElement }>({}); const isNestedContentShown = ref(false); const nestedContentRef = ref(); const rowActionsRef = ref(); const nestedRowRefs = ref<Array<InstanceType<typeof DataGridRow>>>([]); const nestedContentSlotId = generateUid('nested-content-slot'); const isRowActionMenuOpen = ref(false); const isExitingRowActionInteraction = ref(false); const focusableItems = ref<Array<{ node: HTMLElement }>>(); /** * Returns the row-action container element, handling the case where Vue 2 * may wrap refs inside a v-for as an array. */ const getRowActionsElement = (): HTMLElement | undefined => { const val = rowActionsRef.value; return Array.isArray(val) ? val[0] : val; }; onMounted(() => { const container = getRowActionsElement(); if (container) { focusableItems.value = makeAllInteractiveChildrenUnFocusable(container); } }); const shouldShowNestedRowsBooleanResult = computed(() => props.shouldShowNestedRows(props.rowData)); const shouldShowRenderNestedRows = computed( () => !props.isNestedRow && props.hasNestedRows && shouldShowNestedRowsBooleanResult.value && props.rowData?.children?.length > 0 ); const isLoadingNestedRows = computed( () => !props.isNestedRow && props.hasNestedRows && shouldShowNestedRowsBooleanResult.value && (props.rowData?.children === undefined || props?.rowData?.children.length === 0) ); const shouldShowNestedContentToggle = computed( () => (props.hasNestedContent && props.shouldShowNestedContent(props.rowData)) || (props.hasNestedRows && shouldShowNestedRowsBooleanResult.value && !props.isNestedRow) ); const ariaRowAttributes = computed<HTMLAttributes>(() => { if (props.hasNestedContent || props.hasNestedRows) { const isNestedContentShownBooleanishVal = isNestedContentShown.value.toString() as Booleanish; return { 'aria-expanded': shouldShowNestedContentToggle.value ? isNestedContentShownBooleanishVal : undefined, 'aria-level': props.isNestedRow ? '2' : '1', 'aria-posinset': props.rowIndex + 1, 'aria-setsize': props.rowSetSize, }; } return { 'aria-rowindex': props.rowIndex + 1 }; }); const nestedContentContainerDivAriaAttributes = computed<HTMLAttributes>(() => { if (isLoadingNestedRows.value) { return { 'aria-busy': 'true', 'aria-live': 'polite', }; } if ( shouldShowRenderNestedRows.value || !props.hasNestedContent || !props.shouldShowNestedContent(props.rowData) ) { return undefined; } return { 'aria-level': props.isNestedRow ? '3' : '2', 'aria-posinset': '1', 'aria-setsize': '1', id: nestedContentSlotId, role: 'row', tabindex: '-1', }; }); const nestedRowIds = computed(() => { if (shouldShowRenderNestedRows.value) { return props.rowData?.children?.reduce((acc, _val, index) => { acc[index] = generateUid('nested-row'); return acc; }, {}); } return []; }); const allNestedRowIds = computed(() => { if (!isNestedContentShown.value) { return undefined; } if (props.hasNestedContent && props.shouldShowNestedContent(props.rowData)) { return [nestedContentSlotId]; } if (shouldShowRenderNestedRows.value && Object.keys(nestedRowIds.value)?.length > 0) { const generatedRowIds = Object.values(nestedRowIds.value); if (generatedRowIds?.length > 0) { return generatedRowIds; } } return []; }); const hasUnSelectedNestedRows = computed(() => { const childrendDataItemIds = props.rowData?.children?.map(nestedItem => props.getDataItemId(nestedItem)); if (childrendDataItemIds?.length > 0) { const everyChildIsSelected = childrendDataItemIds.every(childId => props?.selectedItems?.includes(childId)); const someItemsAreSelected = childrendDataItemIds.some(childId => props?.selectedItems?.includes(childId)); return !everyChildIsSelected && someItemsAreSelected; } return false; }); // Methods const registerRowCellRef = (el: Element | ComponentPublicInstance, field: string) => { // We always know this will be used on a DataGridCell component so we can cast it const gridCellComponent = el as InstanceType<typeof DataGridCell>; if (el !== null && gridCellComponent?.gridcellRef) { gridcellRefs.value[field] = gridCellComponent.gridcellRef as HTMLDivElement; } }; const registerNestedRowRef = (el: Element | ComponentPublicInstance, childRowIndex: number) => { if (el !== null) { nestedRowRefs.value[childRowIndex] = el as InstanceType<typeof DataGridRow>; } }; const getRowId = (childRowIndex: number) => { return nestedRowIds.value[childRowIndex]; }; const getRowAriaSelectedValue = (row: BentoDatagridDataItem): Booleanish => { if (!props.selectable) { return null; } const id = props.getDataItemId(row); return props.selectedItems.includes(id).toString() as Booleanish; }; const emitRowClick = (rowData: BentoDatagridDataItem, event: MouseEvent | KeyboardEvent) => { emit('row-click', rowData, event); }; /** * Handles row clicks when the `allowRowClicks` prop is true. * * This method also prevents the `row-click` event from firing if the click * originated from an interactive element (like a button, input, or link) * within the row. This is to avoid unintended behavior when a user * interacts with controls inside a row. * * @param event The mouse or keyboard event from the click or key press. */ const onRowClick = (event: MouseEvent | KeyboardEvent) => { // Only proceed if allowRowClicks is true if (!props.allowRowClicks) { return; } let isEventTargetInteractiveElement = false; const dataGridRowInteractiveTypes = ['button', 'input', 'a']; for (let i = 0; i < dataGridRowInteractiveTypes.length; i++) { if ((event?.target as HTMLElement)?.closest(dataGridRowInteractiveTypes[i])) { isEventTargetInteractiveElement = true; break; } } // Only trigger if element is NOT interactive if (!isEventTargetInteractiveElement) { emitRowClick(props.rowData, event); } }; const onRowSelect = (value: Array<BentoDatagridDataItemId>) => { emit('select-row', value, props.rowData); }; const onNestedRowSelect = (rowData: BentoDatagridDataItem, value: Array<BentoDatagridDataItemId>) => { emit('select-nested-row', value, props.rowData, rowData); }; /** * Sets the state to indicate that a row action menu is open. This is used to apply a higher * z-index to the row's action container, ensuring the menu displays correctly above * subsequent rows. */ const onRowActionMenuOpen = () => { isRowActionMenuOpen.value = true; }; /** * Resets the state when a row action menu is closed, removing the elevated z-index from the * row's action container. */ const onRowActionMenuClose = () => { isRowActionMenuOpen.value = false; }; /** * When the row actions cell is focused, determine navigation direction and focus * the appropriate interactive child. Backward navigation (Shift+Tab) focuses the * last child; forward navigation focuses the first. */ const onRowActionsCellFocus = (e: FocusEvent) => { if (isExitingRowActionInteraction.value) { isExitingRowActionInteraction.value = false; return; } const container = getRowActionsElement(); if (!container || !focusableItems.value?.length) { return; } const isBackwardNavigation = isRelatedTargetAfterInDOM(e.relatedTarget as HTMLElement, container); const lastIndex = focusableItems.value.length - 1; const targetItem = isBackwardNavigation ? focusableItems.value[lastIndex] : focusableItems.value[0]; makeAllInteractiveChildrenFocusable([targetItem]); focusOnInteractiveChild(container, { last: isBackwardNavigation }); }; /** * When focus leaves the row actions cell, make all interactive children unfocusable. * This ensures that tabbing through the grid skips over the individual actions within the cell. */ const onRowActionsCellFocusOut = () => { const container = getRowActionsElement(); if (container) { makeAllInteractiveChildrenUnFocusable(container); } }; /** * Handles keyboard navigation within the row actions cell. * Allows using left and right arrow keys to move between interactive elements (e.g., buttons) in the cell. * @param e The keyboard event. */ const onRowActionsCellOnKeyDown = (e: KeyboardEvent) => { const container = getRowActionsElement(); // Escape returns focus to the gridcell container if (e.key === EventKey.ESCAPE) { if (container) { makeAllInteractiveChildrenUnFocusable(container); } isExitingRowActionInteraction.value = true; container?.focus(); e.stopPropagation(); e.preventDefault(); return; } if (e.key !== EventKey.TAB && container) { makeAllInteractiveChildrenUnFocusable(container); } if ( e.key === EventKey.ARROW_RIGHT && focusableItems.value?.length > 1 && focusableItems.value[0].node === document.activeElement ) { if (container) { makeAllInteractiveChildrenUnFocusable(container); } focusableItems.value[1].node.setAttribute('tabindex', '0'); focusableItems.value[1].node.focus(); e.stopPropagation(); e.preventDefault(); } else if ( e.key === EventKey.ARROW_LEFT && focusableItems.value?.length > 1 && focusableItems.value[1].node === document.activeElement ) { if (container) { makeAllInteractiveChildrenUnFocusable(container); } focusableItems.value[0].node.setAttribute('tabindex', '0'); focusableItems.value[0].node.focus(); e.stopPropagation(); e.preventDefault(); } }; // Cell and Columns index calculation const sumColumnAmountsInColumnSets = () => props.columnSets.reduce((total, { columnSet }) => total + columnSet.length, 0); const getRowActionsCellIndex = () => sumColumnAmountsInColumnSets() * props.rowIndex + sumColumnAmountsInColumnSets(); // Nested content expanding logic const { updateContentHeight, contentHeight } = useExpandableContentHeight(nestedContentRef, isNestedContentShown); const nestedContentStyles = computed(() => { if (props.hasNestedContent || props.hasNestedRows) { // calculate the padding by getting the action column width and the first column left padding: const paddingLeft = shouldShowRenderNestedRows.value || isLoadingNestedRows.value ? 0 : gridcellRefs.value[DATAGRID_ACTIONS_COLUMN_FIELD_NAME].offsetWidth + DATAGRID_CELL_OFFSET_PADDING; return { paddingLeft: `${paddingLeft}px`, 'max-height': (props.hasNestedContent || props.hasNestedRows) && contentHeight.value && !contentHeight?.value?.includes('null') ? contentHeight.value : 0, }; } return undefined; }); watch( () => nestedContentRef.value, () => { let observer: ResizeObserver | null = null; if (!observer && nestedContentRef.value) { observer = observeSizeOfElement(nestedContentRef.value, () => { if (nestedContentRef.value) { updateContentHeight(); } }); } } ); const nestedContentToggled = () => { isNestedContentShown.value = !isNestedContentShown.value; updateContentHeight(); emit('nested-content-toggled', props.rowData); }; watch( () => props.rowData?.children, (_newChildren, oldChildren) => { if (props?.rowData?.children?.length > 0 && oldChildren === undefined && !isNestedContentShown.value) { updateContentHeight(); } } ); const isSmallContainer: ComputedRef<boolean> = inject( DATA_GRID_SMALL_CONTAINER, computed(() => false) ); const isHighlightedRow = row => props.highlightedRows.includes(props.getDataItemId(row)); const isSelectedRow = row => props.selectedItems.includes(props.getDataItemId(row)); // Styling const conditionalRowClasses = (row: BentoDatagridDataItem, rowIndex: number) => ({ 'b-data-grid-row-container__row--disabled': props.isDataItemDisabled(row), 'b-data-grid-row-container__row--row-clicks-enabled': props.allowRowClicks, 'b-data-grid-row-container__row--highlighted': isHighlightedRow(row), 'b-data-grid-row-container__row--selected': isSelectedRow(row), 'b-data-grid-row-container__row--hovered': props.hoveredRowIndex === rowIndex, 'b-data-grid-row-container__row--is-last-child': props.isRowLastChild && !isNestedContentShown.value, 'b-data-grid-row-container__row--nested-row': props.isNestedRow, 'b-data-grid-row-container__row--zebra': props.isRowEven, }); const conditionalRowSetClasses = (isFrozenRowSet: boolean, isSmallContainer: boolean) => ({ 'b-data-grid-row-container__row-cell-set--frozen': isFrozenRowSet && !isSmallContainer, [`b-data-grid-row-container__row-cell-set--${BentoDatagridStyleState.OVERFLOW_SHADOW_LEFT}`]: !isSmallContainer && isFrozenRowSet && props.hasLeftOverflow, 'b-data-grid-row-container__row-cell-set--frozen-separator': !isSmallContainer && isFrozenRowSet && props.showFrozenColumnsSeparator, 'b-data-grid-row-container__row-cell-set--unfrozen': !isFrozenRowSet || isSmallContainer, 'b-data-grid-row-container__row-cell-set--is-measuring': props.isMeasuringColumnWidths, }); const conditionalNestedRowClasses = computed(() => ({ 'b-data-grid-row-container__row-nested-slot--is-last-child': props.isRowLastChild, 'b-data-grid-row-container__row-nested-slot': !shouldShowRenderNestedRows.value && props.shouldShowNestedContent(props.rowData), })); const conditionalRowActionsContainerClasses = computed(() => ({ 'b-data-grid-row-container__row-actions-container--row-actions-menu-open': isRowActionMenuOpen.value, })); const computedRowActionsStyles = computed(() => ({ minWidth: `${props.rowActionsMaximumWidth}px`, })); // Exposed refs defineExpose({ gridcellRefs, isNestedContentShown, nestedRowRefs, rowActionsRef, }); </script> <script lang="ts"> /** * Internal component to display the BentoDataGrid data rows containig the data row cells. * * @example * * export default { * components: { DataGridRow }, * template: ` * <data-grid-row * :ref="el => registerRowCellRefs(el, rowIndex)" * :style="gridColumnWidthStyle" * :allow-row-clicks="allowRowClicks" * :column-sets="columnSets" * :condensed="condensed" * :dragging-columns="draggingColumns" * :dragging-events-for-row-cell="draggingEventsForRowCell" * :get-data-item-id="getDataItemId" * :has-left-overflow="hasLeftOverflow" * :highlighted-rows="highlightedRows" * :hovered-row-index="hoveredRowIndex" * :hovered-over-column-state="hoveredOverColumnState" * :is-data-item-disabled="isDataItemDisabled" * :is-measuring-column-widths="isMeasuringColumnWidths" * :row-data="rowData" * :row-index="rowIndex" * :show-frozen-columns-separator="showFrozenColumnsSeparator" * :selectable="selectable" * :selected-items="selectedItems" * @cell-hovered="onCellHover" * @row-click="onRowClick" * @on-frozen-columns-key-press="updateScrollPositionOnFrozenColumnsNavigation" * @select-row="onRowSelect" * > * <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> * <slot :name="innerSlot" v-bind="scope" /> * </template> * </data-grid-row> * ` */ export default { name: 'data-grid-row', }; </script> <style lang="scss" scoped src="./data-grid-row.scss" />
@@ -305,7 +305,7 @@ You can automatically update `filter-values` using the
305
305
  | `filters` | Array of filter objects. Each one of the elements in the array corresponds to a filter that will be created and rendered in the FilterBar. |
306
306
  | `filterValues` | Array of filter values. Each one of the elements in the array corresponds to a filter's value to be set in the filter bar. |
307
307
  | `filterSearchTerm` | String that contains the search term used to filter the data with, it is displayed in the search input of the filter bar. |
308
- | `filterSearchConfig` | Sets the configuration options (`hint`, `placeholder`, `debounceTime` and `inputFieldAriaLabel`) for the filter's search bar. |
308
+ | `filterSearchConfig` | Sets the configuration options (`hint`, `placeholder`, `debounceTime`, `inputFieldAriaLabel` and `width`) for the filter's search bar. |
309
309
 
310
310
  <Canvas of={ComponentStories.Filtering} />
311
311