@adyen/bento-mcp 0.12.1 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.vue +1 -1
  3. package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.internal.types.ts +1 -1
  4. package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue +1 -1
  5. package/dist/assets/components/dropdown/components/dropdown-default-textbox/dropdown-default-textbox.vue +1 -1
  6. package/dist/assets/components/dropdown/components/dropdown-small-textbox/dropdown-small-textbox.vue +1 -1
  7. package/dist/assets/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.stories.ts +1 -1
  8. package/dist/assets/components/dropdown/components/variants/dropdown-country/dropdown-country.stories.ts +1 -1
  9. package/dist/assets/components/dropdown/components/variants/dropdown-currency/dropdown-currency.stories.ts +1 -1
  10. package/dist/assets/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.stories.ts +1 -1
  11. package/dist/assets/components/dropdown/components/variants/dropdown-tag/dropdown-tag.stories.ts +1 -1
  12. package/dist/assets/components/dropdown/dropdown.docs.mdx +11 -0
  13. package/dist/assets/components/dropdown/dropdown.stories.ts +1 -1
  14. package/dist/assets/components/dropdown/dropdown.types.ts +1 -1
  15. package/dist/assets/components/dropdown/dropdown.vue +1 -1
  16. package/dist/assets/components/filter-bar/components/all-filters-modal/components/all-filters-modal-selected-filter/all-filters-modal-selected-filter.vue +1 -1
  17. package/dist/assets/components/filter-bar/filter-bar-filter-components.types.ts +1 -0
  18. package/dist/assets/components/filter-bar/filter-bar.vue +1 -1
  19. package/dist/assets/components/menu/menu.types.ts +1 -1
  20. package/dist/assets/components/pagination/components/pagination-context/pagination-context.vue +1 -1
  21. package/dist/assets/components/pagination/components/pagination-controls/pagination-controls.vue +1 -1
  22. package/dist/assets/components/promo-banner/promo-banner.types.ts +1 -1
  23. package/dist/assets/components/sidepanel/sidepanel.vue +1 -1
  24. package/dist/assets/components/summary-grid/summary-grid.docs.mdx +60 -13
  25. package/dist/assets/components/summary-grid/summary-grid.stories.ts +1 -1
  26. package/dist/assets/components/timeline/components/timeline-item/timeline-item.stories.ts +1 -1
  27. package/dist/assets/components/timeline/components/timeline-item/timeline-item.vue +1 -1
  28. package/dist/assets/components/timeline/timeline.docs.mdx +20 -4
  29. package/dist/assets/components/timeline/timeline.types.ts +1 -1
  30. package/dist/assets/components/typography/typography.types.ts +1 -1
  31. package/dist/assets/deprecations.md +8 -0
  32. package/dist/assets/templates/dashboard-template/dashboard-template.vue +1 -1
  33. package/dist/assets/usage.json +7 -7
  34. package/dist/main.js +1 -1
  35. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.2 (2026-09-15)
4
+
5
+ This was a version bump only for mcp to align it with other projects, there were no code changes.
6
+
7
+
3
8
  ## 0.12.1 (2026-09-09)
4
9
 
5
10
  This was a version bump only for mcp to align it with other projects, there were no code changes.
@@ -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> </template> <div v-if="rowActions" 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> </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, } from 'vue'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { focusOnInteractiveChild, makeAllInteractiveChildrenFocusable, makeAllInteractiveChildrenUnFocusable, } from '@/components/data-grid/utils'; 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 { BentoDataGridRowInstance } from './data-grid-row.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 { 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 '@/utils/ts/generate-uid'; 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<BentoDataGridRowInstance>>([]); 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 BentoDataGridRowInstance; } }; 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 { 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; }); const nestedContentToggled = () => { isNestedContentShown.value = !isNestedContentShown.value; emit('nested-content-toggled', props.rowData); }; 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" />
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> </template> <div v-if="rowActions" 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> </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, } from 'vue'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { focusOnInteractiveChild, makeAllInteractiveChildrenFocusable, makeAllInteractiveChildrenUnFocusable, } from '@/components/data-grid/utils'; 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 { BentoDataGridRowInstance } from './data-grid-row.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 { 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 '@/utils/ts/generate-uid'; 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 rows 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<BentoDataGridRowInstance>>([]); 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 BentoDataGridRowInstance; } }; 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 { 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; }); const nestedContentToggled = () => { isNestedContentShown.value = !isNestedContentShown.value; emit('nested-content-toggled', props.rowData); }; 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" />
@@ -1 +1 @@
1
- import { type PropType } from 'vue'; import { type Booleanish } from '@/types/prop-types'; // TODO: Change this to script setup + Typescript interface export const DropdownBaseTextboxProps = { /** * The number of additional items that have been selected in the dropdown. */ additionalItemsSelected: { type: [Number, String] as PropType<number | string>, default: 0 }, /** * Sets the possibility to type into the textbox */ dynamicFiltering: { type: Boolean, default: false, }, /** * Sets the aria-controls attribute on the textbox. This should correspond to the popover's id that the textbox triggers. */ ariaControls: { type: String, required: true, }, /** * Indicates whether the content the textbox triggers is visible or not. Needs to be set accordingly when the textbox is used and changed every time the popover is shown/hidden. */ ariaExpanded: { type: [String, Boolean] as PropType<Booleanish>, required: true, }, /** * Corresponds to the label's id that labels the textbox element. */ ariaLabelledby: { type: String, default: null, }, /** * Disables any textbox functionality */ disabled: { type: Boolean, default: false }, /** * Marks the textbox as invalid, rendering an error state */ isInvalid: { type: Boolean, default: false }, /** * Selected option's label */ displayValue: { type: String, default: null }, /** * Indicates if the dropdown is open so that * the respective event can be emitted */ open: { type: Boolean, default: false }, /** * The input value */ value: { type: String, default: '' }, /** * Custom debounce time. Default: 300 */ debounceTime: { type: Number, default: 300 }, }; export enum DropdownBaseTextboxEvent { OPEN = 'dropdown-base-open', KEYDOWN = 'dropdown-base-keydown', CLOSE = 'dropdown-base-close', INPUT = 'dropdown-base-input', CLEAR = 'dropdown-base-clear', } export default DropdownBaseTextboxProps;
1
+ import { type PropType } from 'vue'; import { type Booleanish } from '@/types/prop-types'; /** * Elements in the base textbox that can receive focus programmatically. */ export type DropdownBaseTextboxFocus = 'textbox' | 'clear-action'; // TODO: Change this to script setup + Typescript interface export const DropdownBaseTextboxProps = { /** * The number of additional items that have been selected in the dropdown. */ additionalItemsSelected: { type: [Number, String] as PropType<number | string>, default: 0 }, /** * Sets the possibility to type into the textbox */ dynamicFiltering: { type: Boolean, default: false, }, /** * Sets the aria-controls attribute on the textbox. This should correspond to the popover's id that the textbox triggers. */ ariaControls: { type: String, required: true, }, /** * Indicates whether the content the textbox triggers is visible or not. Needs to be set accordingly when the textbox is used and changed every time the popover is shown/hidden. */ ariaExpanded: { type: [String, Boolean] as PropType<Booleanish>, required: true, }, /** * Corresponds to the label's id that labels the textbox element. */ ariaLabelledby: { type: String, default: null, }, /** * Disables any textbox functionality */ disabled: { type: Boolean, default: false }, /** * Marks the textbox as invalid, rendering an error state */ isInvalid: { type: Boolean, default: false }, /** * Selected option's label */ displayValue: { type: String, default: null }, /** * Indicates if the dropdown is open so that * the respective event can be emitted */ open: { type: Boolean, default: false }, /** * The input value */ value: { type: String, default: '' }, /** * Custom debounce time. Default: 300 */ debounceTime: { type: Number, default: 300 }, }; export enum DropdownBaseTextboxEvent { OPEN = 'dropdown-base-open', KEYDOWN = 'dropdown-base-keydown', CLOSE = 'dropdown-base-close', INPUT = 'dropdown-base-input', CLEAR = 'dropdown-base-clear', CLEAR_SELECTION = 'dropdown-base-clear-selection', } export default DropdownBaseTextboxProps;
@@ -1 +1 @@
1
- <template> <div :class="$attrs.class" data-testid="textbox-wrapper" @click="onToggleDropdown" @keydown.escape="onCloseDropdown" > <bento-typography class="b-dropdown-base-textbox" :class="conditionalClasses" :variant="isUnderlineStyle ? 'title' : 'body'" el="div" data-testid="base-textbox" > <span v-if="shouldShowAutocompleteSuggestion(hasSlot('autocomplete-suggestion'))" class="b-dropdown-base-textbox__autocomplete-suggestion" aria-hidden="true" data-testid="autocomplete-suggestion" > <slot name="autocomplete-suggestion"></slot> </span> <input v-if="shouldShowInput" v-bind="ariaAttributes" :id="textboxId" ref="textbox" class="b-dropdown-base-textbox__input" role="combobox" type="text" :aria-autocomplete=" shouldShowAutocompleteSuggestion(hasSlot('autocomplete-suggestion')) ? 'both' : 'list' " size="1" :value="inputValue" :disabled="disabled ? true : null" :readonly="readonly" :required="showRequiredAttribute" @keydown="$emit(DropdownBaseTextboxEvent.KEYDOWN, $event)" @keydown.enter="onTextboxEnterKey" @focus="onTextboxFocus" @blur="onTextboxBlur" @input="onInputEvent" /> <div v-else :id="textboxId" v-bind="ariaAttributes" ref="textbox" class="b-dropdown-base-textbox__input" :class="comboboxConditionalClasses" :aria-readonly="readonly ? 'true' : null" role="combobox" :tabindex="disabled ? -1 : 0" @focus="onTextboxFocus" @mousedown="onClosedComboboxMouseDown" > <template v-if="shouldShowSingleSelectSlot"> <slot v-if="hasSlot('display-value')" v-bind="selectedValueItem" name="display-value"></slot> <slot v-else v-bind="selectedValueItem"> {{ displayValue }} </slot> </template> <template v-else-if="shouldShowMultipleSlot"> <div class="b-dropdown-base-textbox__input-multiple-selected"> <div v-for="singleItem of selectedValueItems" :key="singleItem.value"> <div class="b-dropdown-base-textbox__input-selected-element"> <slot v-bind="singleItem"> {{ displayValue }} </slot> </div> </div> </div> </template> <template v-else>{{ displayValue }}</template> </div> <span v-if="additionalItemsSelected" class="b-dropdown-base-textbox__additional-items"> {{ additionalItemsSelected }} </span> <span v-if="hasSlot('icon') && !isFiltering" class="b-dropdown-base-textbox__icon" aria-hidden="true"> <slot name="icon"></slot> </span> <span v-if="isFiltering" class="b-dropdown-base-textbox__clear-search" :tabindex="disabled ? -1 : 0" data-testid="clear-search-button" @click.stop="onClearSearch" @keydown.enter.stop="onClearSearch" @keyup.space="onClearSearch" > <cross-circle-fill-small-icon v-if="size === BentoDropdownSize.SMALL" :svg-title="t('clearSearch')" /> <cross-circle-fill-icon v-else :svg-title="t('clearSearch')" /> </span> </bento-typography> </div> </template> <script setup lang="ts"> import { computed, defineComponent, inject, nextTick, type PropType, ref, useAttrs, useSlots, watch } from 'vue'; import { generateUid } from '@/utils/ts/generate-uid'; import { useI18n } from '@/utils/ts/i18n'; import { stopEventPropagation } from '@/utils/ts/events'; import { useHasSlot } from '@/composables'; import { BentoTypography } from '@/components/typography'; import { type Booleanish } from '@/types/prop-types'; import { type BentoListboxOptionItem } from '@/types/listbox'; import type { HTMLAttributes } from 'vue/types/jsx.d.ts'; import { DropdownBaseTextboxEvent } from './dropdown-base-textbox.internal.types'; import { DropdownBaseTextboxState } from './dropdown-base-textbox.types'; import CrossCircleFillIcon from '@adyen/ui-assets-icons-16/vue/cross-circle-fill'; import CrossCircleFillSmallIcon from '@adyen/ui-assets-icons-16/vue/cross-circle-fill-small'; import { BentoDropdownSize } from '@/components/dropdown/dropdown.internal.types'; import { DROPDOWN_UNDERLINE_STYLE_INJECTION_KEY } from '../../dropdown.keys'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const attrs = useAttrs(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = defineProps({ /** * The number of additional items that have been selected in the dropdown. */ additionalItemsSelected: { type: [Number, String] as PropType<number | string>, default: 0 }, /** * If the combobox DOM element should be an `input` element. */ alwaysComboboxIsInput: { type: Boolean, default: false, }, /** * Sets the aria-controls attribute on the textbox. This should correspond to the popover's id that the textbox triggers. */ ariaControls: { type: String, required: true }, /** * Indicates whether the content the textbox triggers is visible or not. Needs to be set accordingly when the textbox is used and changed every time the popover is shown/hidden. */ ariaExpanded: { type: [String, Boolean] as PropType<Booleanish>, required: true, }, /** * If no label element is used then the aria-label is be set should be set. */ ariaLabel: { type: String, default: null }, /** * Identifies the currently active option in the listbox. */ ariaActiveDescendant: { type: String, default: null }, /** * Corresponds to the label's id that labels the textbox element. */ ariaLabelledby: { type: String, default: null }, /** * Disables any textbox functionality */ disabled: { type: Boolean, default: false }, /** * Selected option's label */ displayValue: { type: String, default: null }, /** * Sets the possibility to type into the textbox. */ dynamicFiltering: { type: Boolean, default: false }, /** * Indicates whether the dropdown currently has a selected value. */ hasSelectedValue: { type: Boolean, default: false }, /** * Marks the textbox as invalid, rendering an error state */ isInvalid: { type: Boolean, default: false }, /* * If enabled, the user will be able to select multiple items. */ multiple: { type: Boolean, default: false }, /** * Indicates if the dropdown is open so that * the respective event can be emitted */ open: { type: Boolean, default: false }, /** * Sets the dropdown to readonly state */ readonly: { type: Boolean, default: false }, /** * The input value */ value: { type: String, default: '' }, /** * The selected value's list box item. */ selectedValueItem: { type: [Object, Array] as PropType<BentoListboxOptionItem | Array<BentoListboxOptionItem>>, default: undefined, }, /** * Toggles whether to use the slot content select items shown in the textbox in multiple mode. */ showSlotContentInMultiple: { type: Boolean, default: false }, /** * Dropdown size */ size: { type: String as PropType<BentoDropdownSize | `${BentoDropdownSize}`>, default: null, }, }); const emit = defineEmits<{ /** * Emitted when the "clear" button is clicked */ (e: DropdownBaseTextboxEvent.CLEAR); /** * Emitted on click, space, enter and esc user events. It will close the dropdown. */ (e: DropdownBaseTextboxEvent.CLOSE); /** * Emitted when `dynamicFiltering` is enabled and a search input is entered */ (e: DropdownBaseTextboxEvent.INPUT, searchValue: string); /** * Emitted when any key is pressed and the focus is on the input field */ (e: DropdownBaseTextboxEvent.KEYDOWN, event: KeyboardEvent); /** * Emitted on click, space and enter user events. It will open the dropdown. */ (e: DropdownBaseTextboxEvent.OPEN); }>(); const textbox = ref(null); const textboxId = generateUid('textbox'); const isTextboxFocused = ref(false); const localInputValue = ref(props.value); // Inject underline style key to change the dropdown layout const isUnderlineStyle = inject(DROPDOWN_UNDERLINE_STYLE_INJECTION_KEY, false); const conditionalClasses = computed(() => ({ [`b-dropdown-base-textbox--${DropdownBaseTextboxState.DISABLED}`]: props.disabled, [`b-dropdown-base-textbox--${DropdownBaseTextboxState.READONLY}`]: props.readonly, [`b-dropdown-base-textbox--${DropdownBaseTextboxState.HAS_ITEMS}`]: !!props.additionalItemsSelected, 'b-dropdown-base-textbox--underline': isUnderlineStyle, })); const showRequiredAttribute = computed(() => shouldShowInput.value && attrs['aria-required'] ? ('true' as Booleanish) : null ); const comboboxConditionalClasses = computed(() => ({ [`b-dropdown-base-textbox__input--dynamic-filtering`]: props.dynamicFiltering, })); const isFiltering = computed(() => props.dynamicFiltering && !!props.value && props.open); const shouldShowAutocompleteSuggestion = (hasAutocompleteSuggestionSlot: boolean) => shouldShowInput.value && !props.multiple && !!props.value && hasAutocompleteSuggestionSlot; const ariaAttributes = computed( () => ({ 'aria-haspopup': 'listbox', 'aria-controls': props.ariaControls, 'aria-describedby': attrs['aria-describedby']?.toString() as string, 'aria-expanded': props.ariaExpanded ? 'true' : 'false', 'aria-activedescendant': props.ariaActiveDescendant, 'aria-labelledby': props.ariaLabelledby, 'aria-label': props.ariaLabelledby ? null : props.ariaLabel, 'aria-invalid': props.isInvalid, 'aria-disabled': props.disabled ? true : null, 'aria-required': attrs['aria-required'] as Booleanish, }) as HTMLAttributes ); // Cast selectedValueItem if it is an array of selected items const selectedValueItems = computed(() => props.multiple ? (props.selectedValueItem as Array<BentoListboxOptionItem>) : [] ); const shouldUseDynamicFilteringInputMode = computed( () => props.dynamicFiltering && !props.multiple && !props.alwaysComboboxIsInput ); const shouldShowSearchValue = computed( () => props.open || props.alwaysComboboxIsInput || (shouldUseDynamicFilteringInputMode.value && isTextboxFocused.value) ); const shouldShowInput = computed(() => props.dynamicFiltering && shouldShowSearchValue.value); const getInitialInputValue = () => { if (!props.value && props.hasSelectedValue && shouldUseDynamicFilteringInputMode.value) { return props.displayValue ?? ''; } return props.value; }; const inputValue = computed(() => { if (!shouldShowSearchValue.value || (props.alwaysComboboxIsInput && !props.open)) { return props.displayValue; } if (shouldUseDynamicFilteringInputMode.value) { return localInputValue.value; } return props.value; }); const shouldShowSingleSelectSlot = computed(() => props.selectedValueItem && !props.multiple); const shouldShowMultipleSlot = computed( () => props.multiple && props.selectedValueItem && selectedValueItems.value.length > 0 && props.showSlotContentInMultiple ); watch( () => shouldShowInput.value, (shouldShowInput, wasShowingInput) => { if (!shouldShowInput) { localInputValue.value = props.value; } else if (!wasShowingInput && shouldUseDynamicFilteringInputMode.value) { localInputValue.value = getInitialInputValue(); } if (shouldShowInput && !props.alwaysComboboxIsInput) { nextTick(() => { textbox.value?.focus(); textbox.value?.select(); }); } } ); watch( () => props.value, newValue => { if (!shouldUseDynamicFilteringInputMode.value || newValue !== localInputValue.value) { localInputValue.value = newValue; } } ); watch( () => props.displayValue, () => { if (shouldUseDynamicFilteringInputMode.value && props.hasSelectedValue) { localInputValue.value = getInitialInputValue(); } } ); watch( () => props.open, open => { if (!open) { isTextboxFocused.value = false; } } ); const onOpenDropdown = async () => { if (!props.disabled && !props.readonly) { emit(DropdownBaseTextboxEvent.OPEN); // wait for popover to open before setting focus await nextTick(); textbox.value.focus(); } }; const onCloseDropdown = (event?: Event) => { if (props.open) { if (event) { stopEventPropagation(event); } textbox.value.focus(); emit(DropdownBaseTextboxEvent.CLOSE); } }; const onToggleDropdown = (event: Event) => { const isDynamicFilteringInputClickEvent = props.open && (event.target as HTMLDivElement).tagName === 'INPUT'; if (isDynamicFilteringInputClickEvent) { /** * When there's a click even on the input during filtering * and the popover is open, do not toggle the popover. * Allow for click events inside the input field */ return null; } return props.open ? onCloseDropdown() : onOpenDropdown(); }; const onTextboxEnterKey = (event: KeyboardEvent) => { if (props.dynamicFiltering) { return null; } return onToggleDropdown(event); }; const onClearSearch = () => { emit(DropdownBaseTextboxEvent.CLEAR); emit(DropdownBaseTextboxEvent.INPUT, ''); textbox.value.focus(); }; const onTextboxFocus = () => { isTextboxFocused.value = true; }; const onTextboxBlur = () => { isTextboxFocused.value = false; }; const onClosedComboboxMouseDown = (event: MouseEvent) => { if (!shouldUseDynamicFilteringInputMode.value || props.open || props.readonly || props.disabled) { return; } event.preventDefault(); event.stopPropagation(); }; const onInputEvent = (event: Event) => { if (!props.readonly && !props.disabled) { if (shouldUseDynamicFilteringInputMode.value) { localInputValue.value = (event.target as HTMLInputElement).value; } if (props.dynamicFiltering && !props.open) { emit(DropdownBaseTextboxEvent.OPEN); } emit(DropdownBaseTextboxEvent.INPUT, (event.target as HTMLInputElement).value); nextTick(() => { if (shouldShowInput.value) { textbox.value?.focus(); } }); } else { event.preventDefault(); event.stopPropagation(); } }; const focus = () => { textbox.value.focus(); }; defineExpose({ focus, }); </script> <script lang="ts"> /** * Only for internal use, implements the logic of the textbox element to be implemented in the combobox. */ export default defineComponent({ i18n: { messages }, name: 'dropdown-base-textbox', inheritAttrs: false, }); </script> <style lang="scss" scoped src="./dropdown-base-textbox.scss" />
1
+ <template> <div :class="$attrs.class" data-testid="textbox-wrapper" @click="onToggleDropdown" @keydown.escape="onCloseDropdown" > <bento-typography class="b-dropdown-base-textbox" :class="conditionalClasses" :variant="isUnderlineStyle ? 'title' : 'body'" el="div" data-testid="base-textbox" > <span v-if="shouldShowAutocompleteSuggestion(hasSlot('autocomplete-suggestion'))" class="b-dropdown-base-textbox__autocomplete-suggestion" aria-hidden="true" data-testid="autocomplete-suggestion" > <slot name="autocomplete-suggestion"></slot> </span> <input v-if="shouldShowInput" v-bind="ariaAttributes" :id="textboxId" ref="textbox" class="b-dropdown-base-textbox__input" role="combobox" type="text" :aria-autocomplete=" shouldShowAutocompleteSuggestion(hasSlot('autocomplete-suggestion')) ? 'both' : 'list' " size="1" :value="inputValue" :disabled="disabled ? true : null" :readonly="readonly" :required="showRequiredAttribute" @keydown="$emit(DropdownBaseTextboxEvent.KEYDOWN, $event)" @keydown.enter="onTextboxEnterKey" @focus="onTextboxFocus" @blur="onTextboxBlur" @input="onInputEvent" /> <div v-else :id="textboxId" v-bind="ariaAttributes" ref="textbox" class="b-dropdown-base-textbox__input" :class="comboboxConditionalClasses" :aria-readonly="readonly ? 'true' : null" role="combobox" :tabindex="disabled ? -1 : 0" @focus="onTextboxFocus" @mousedown="onClosedComboboxMouseDown" > <template v-if="shouldShowSingleSelectSlot"> <slot v-if="hasSlot('display-value')" v-bind="selectedValueItem" name="display-value"></slot> <slot v-else v-bind="selectedValueItem"> {{ displayValue }} </slot> </template> <template v-else-if="shouldShowMultipleSlot"> <div class="b-dropdown-base-textbox__input-multiple-selected"> <div v-for="singleItem of selectedValueItems" :key="singleItem.value"> <div class="b-dropdown-base-textbox__input-selected-element"> <slot v-bind="singleItem"> {{ displayValue }} </slot> </div> </div> </div> </template> <template v-else>{{ displayValue }}</template> </div> <span v-if="additionalItemsSelected" class="b-dropdown-base-textbox__additional-items"> {{ additionalItemsSelected }} </span> <span v-if="hasSlot('icon') && !showClearAction" class="b-dropdown-base-textbox__icon" aria-hidden="true"> <slot name="icon"></slot> </span> <span v-if="showClearAction" ref="clearAction" class="b-dropdown-base-textbox__clear-action" :tabindex="disabled ? -1 : 0" role="button" :aria-label="clearActionLabel" :title="clearActionLabel" :data-testid="isFiltering ? 'clear-search-button' : 'clear-selection-button'" @click.stop="onClearAction" @keydown.enter.stop="onClearAction" @keydown.space.prevent.stop="onClearAction" > <cross-circle-fill-small-icon v-if="size === BentoDropdownSize.SMALL" aria-hidden="true" /> <cross-circle-fill-icon v-else aria-hidden="true" /> </span> </bento-typography> </div> </template> <script setup lang="ts"> import { computed, defineComponent, inject, nextTick, type PropType, ref, useAttrs, useSlots, watch } from 'vue'; import { generateUid } from '@/utils/ts/generate-uid'; import { useI18n } from '@/utils/ts/i18n'; import { stopEventPropagation } from '@/utils/ts/events'; import { useHasSlot } from '@/composables'; import { BentoTypography } from '@/components/typography'; import { type Booleanish } from '@/types/prop-types'; import { type BentoListboxOptionItem } from '@/types/listbox'; import type { HTMLAttributes } from 'vue/types/jsx.d.ts'; import { DropdownBaseTextboxEvent, type DropdownBaseTextboxFocus } from './dropdown-base-textbox.internal.types'; import { DropdownBaseTextboxState } from './dropdown-base-textbox.types'; import CrossCircleFillIcon from '@adyen/ui-assets-icons-16/vue/cross-circle-fill'; import CrossCircleFillSmallIcon from '@adyen/ui-assets-icons-16/vue/cross-circle-fill-small'; import { BentoDropdownSize } from '@/components/dropdown/dropdown.internal.types'; import { DROPDOWN_UNDERLINE_STYLE_INJECTION_KEY } from '../../dropdown.keys'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const attrs = useAttrs(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = defineProps({ /** * The number of additional items that have been selected in the dropdown. */ additionalItemsSelected: { type: [Number, String] as PropType<number | string>, default: 0 }, /** * Allows the selected value to be cleared in a single-select dropdown. */ clearable: { type: Boolean, default: false }, /** * If the combobox DOM element should be an `input` element. */ alwaysComboboxIsInput: { type: Boolean, default: false, }, /** * Sets the aria-controls attribute on the textbox. This should correspond to the popover's id that the textbox triggers. */ ariaControls: { type: String, required: true }, /** * Indicates whether the content the textbox triggers is visible or not. Needs to be set accordingly when the textbox is used and changed every time the popover is shown/hidden. */ ariaExpanded: { type: [String, Boolean] as PropType<Booleanish>, required: true, }, /** * If no label element is used then the aria-label is be set should be set. */ ariaLabel: { type: String, default: null }, /** * Identifies the currently active option in the listbox. */ ariaActiveDescendant: { type: String, default: null }, /** * Corresponds to the label's id that labels the textbox element. */ ariaLabelledby: { type: String, default: null }, /** * Disables any textbox functionality */ disabled: { type: Boolean, default: false }, /** * Selected option's label */ displayValue: { type: String, default: null }, /** * Sets the possibility to type into the textbox. */ dynamicFiltering: { type: Boolean, default: false }, /** * Indicates whether the dropdown currently has a selected value. */ hasSelectedValue: { type: Boolean, default: false }, /** * Marks the textbox as invalid, rendering an error state */ isInvalid: { type: Boolean, default: false }, /* * If enabled, the user will be able to select multiple items. */ multiple: { type: Boolean, default: false }, /** * Indicates if the dropdown is open so that * the respective event can be emitted */ open: { type: Boolean, default: false }, /** * Sets the dropdown to readonly state */ readonly: { type: Boolean, default: false }, /** * The input value */ value: { type: String, default: '' }, /** * The selected value's list box item. */ selectedValueItem: { type: [Object, Array] as PropType<BentoListboxOptionItem | Array<BentoListboxOptionItem>>, default: undefined, }, /** * Toggles whether to use the slot content select items shown in the textbox in multiple mode. */ showSlotContentInMultiple: { type: Boolean, default: false }, /** * Dropdown size */ size: { type: String as PropType<BentoDropdownSize | `${BentoDropdownSize}`>, default: null, }, }); const emit = defineEmits<{ /** * Emitted when the "clear" button is clicked */ (e: DropdownBaseTextboxEvent.CLEAR); /** * Emitted when the selected value should be cleared. */ (e: DropdownBaseTextboxEvent.CLEAR_SELECTION); /** * Emitted on click, space, enter and esc user events. It will close the dropdown. */ (e: DropdownBaseTextboxEvent.CLOSE); /** * Emitted when `dynamicFiltering` is enabled and a search input is entered */ (e: DropdownBaseTextboxEvent.INPUT, searchValue: string); /** * Emitted when any key is pressed and the focus is on the input field */ (e: DropdownBaseTextboxEvent.KEYDOWN, event: KeyboardEvent); /** * Emitted on click, space and enter user events. It will open the dropdown. */ (e: DropdownBaseTextboxEvent.OPEN); }>(); const textbox = ref(null); const clearAction = ref<HTMLElement | null>(null); const textboxId = generateUid('textbox'); const isTextboxFocused = ref(false); const localInputValue = ref(props.value); // Inject underline style key to change the dropdown layout const isUnderlineStyle = inject(DROPDOWN_UNDERLINE_STYLE_INJECTION_KEY, false); const conditionalClasses = computed(() => ({ [`b-dropdown-base-textbox--${DropdownBaseTextboxState.DISABLED}`]: props.disabled, [`b-dropdown-base-textbox--${DropdownBaseTextboxState.READONLY}`]: props.readonly, [`b-dropdown-base-textbox--${DropdownBaseTextboxState.HAS_ITEMS}`]: !!props.additionalItemsSelected, 'b-dropdown-base-textbox--underline': isUnderlineStyle, })); const showRequiredAttribute = computed(() => shouldShowInput.value && attrs['aria-required'] ? ('true' as Booleanish) : null ); const comboboxConditionalClasses = computed(() => ({ [`b-dropdown-base-textbox__input--dynamic-filtering`]: props.dynamicFiltering, })); const isFiltering = computed(() => props.dynamicFiltering && !!props.value && props.open); // Clear selection is opt-in and only applies to open single-selects with a value. const showClearSelection = computed( () => props.clearable && props.open && !props.multiple && props.hasSelectedValue ); // Search and selection clearing share a control, with search taking precedence. const showClearAction = computed( () => !props.disabled && !props.readonly && (isFiltering.value || showClearSelection.value) ); // Match the accessible name to the action currently shown. const clearActionLabel = computed(() => (isFiltering.value ? t('clearSearch') : t('clearSelection'))); const shouldShowAutocompleteSuggestion = (hasAutocompleteSuggestionSlot: boolean) => shouldShowInput.value && !props.multiple && !!props.value && hasAutocompleteSuggestionSlot; const ariaAttributes = computed( () => ({ 'aria-haspopup': 'listbox', 'aria-controls': props.ariaControls, 'aria-describedby': attrs['aria-describedby']?.toString() as string, 'aria-expanded': props.ariaExpanded ? 'true' : 'false', 'aria-activedescendant': props.ariaActiveDescendant, 'aria-labelledby': props.ariaLabelledby, 'aria-label': props.ariaLabelledby ? null : props.ariaLabel, 'aria-invalid': props.isInvalid, 'aria-disabled': props.disabled ? true : null, 'aria-required': attrs['aria-required'] as Booleanish, }) as HTMLAttributes ); // Cast selectedValueItem if it is an array of selected items const selectedValueItems = computed(() => props.multiple ? (props.selectedValueItem as Array<BentoListboxOptionItem>) : [] ); const shouldUseDynamicFilteringInputMode = computed( () => props.dynamicFiltering && !props.multiple && !props.alwaysComboboxIsInput ); const shouldShowSearchValue = computed( () => props.open || props.alwaysComboboxIsInput || (shouldUseDynamicFilteringInputMode.value && isTextboxFocused.value) ); const shouldShowInput = computed(() => props.dynamicFiltering && shouldShowSearchValue.value); const getInitialInputValue = () => { if (!props.value && props.hasSelectedValue && shouldUseDynamicFilteringInputMode.value) { return props.displayValue ?? ''; } return props.value; }; const inputValue = computed(() => { if (!shouldShowSearchValue.value || (props.alwaysComboboxIsInput && !props.open)) { return props.displayValue; } if (shouldUseDynamicFilteringInputMode.value) { return localInputValue.value; } return props.value; }); const shouldShowSingleSelectSlot = computed(() => props.selectedValueItem && !props.multiple); const shouldShowMultipleSlot = computed( () => props.multiple && props.selectedValueItem && selectedValueItems.value.length > 0 && props.showSlotContentInMultiple ); watch( () => shouldShowInput.value, (shouldShowInput, wasShowingInput) => { if (!shouldShowInput) { localInputValue.value = props.value; } else if (!wasShowingInput && shouldUseDynamicFilteringInputMode.value) { localInputValue.value = getInitialInputValue(); } if (shouldShowInput && !props.alwaysComboboxIsInput) { nextTick(() => { textbox.value?.focus(); textbox.value?.select(); }); } } ); watch( () => props.value, newValue => { if (!shouldUseDynamicFilteringInputMode.value || newValue !== localInputValue.value) { localInputValue.value = newValue; } } ); watch( () => props.displayValue, () => { if (shouldUseDynamicFilteringInputMode.value && props.hasSelectedValue) { localInputValue.value = getInitialInputValue(); } } ); watch( () => props.hasSelectedValue, hasSelectedValue => { if (shouldUseDynamicFilteringInputMode.value && !hasSelectedValue) { localInputValue.value = props.value; } } ); watch( () => props.open, open => { if (!open) { isTextboxFocused.value = false; } } ); const onOpenDropdown = async () => { if (!props.disabled && !props.readonly) { emit(DropdownBaseTextboxEvent.OPEN); // wait for popover to open before setting focus await nextTick(); textbox.value.focus(); } }; const onCloseDropdown = (event?: Event) => { if (props.open) { if (event) { stopEventPropagation(event); } textbox.value.focus(); emit(DropdownBaseTextboxEvent.CLOSE); } }; const onToggleDropdown = (event: Event) => { const isDynamicFilteringInputClickEvent = props.open && (event.target as HTMLDivElement).tagName === 'INPUT'; if (isDynamicFilteringInputClickEvent) { /** * When there's a click even on the input during filtering * and the popover is open, do not toggle the popover. * Allow for click events inside the input field */ return null; } return props.open ? onCloseDropdown() : onOpenDropdown(); }; const onTextboxEnterKey = (event: KeyboardEvent) => { if (props.dynamicFiltering) { return null; } return onToggleDropdown(event); }; const onClearSearch = () => { emit(DropdownBaseTextboxEvent.CLEAR); emit(DropdownBaseTextboxEvent.INPUT, ''); textbox.value.focus(); }; const onClearAction = () => { if (isFiltering.value) { onClearSearch(); return; } emit(DropdownBaseTextboxEvent.CLEAR_SELECTION); }; const onTextboxFocus = () => { isTextboxFocused.value = true; }; const onTextboxBlur = () => { isTextboxFocused.value = false; }; const onClosedComboboxMouseDown = (event: MouseEvent) => { if (!shouldUseDynamicFilteringInputMode.value || props.open || props.readonly || props.disabled) { return; } event.preventDefault(); event.stopPropagation(); }; const onInputEvent = (event: Event) => { if (!props.readonly && !props.disabled) { if (shouldUseDynamicFilteringInputMode.value) { localInputValue.value = (event.target as HTMLInputElement).value; } if (props.dynamicFiltering && !props.open) { emit(DropdownBaseTextboxEvent.OPEN); } emit(DropdownBaseTextboxEvent.INPUT, (event.target as HTMLInputElement).value); nextTick(() => { if (shouldShowInput.value) { textbox.value?.focus(); } }); } else { event.preventDefault(); event.stopPropagation(); } }; /** * Focuses an element in the base textbox. * @param target Element to focus. Defaults to the textbox. * @returns Whether the target element was available to focus. Returns `false` * when the requested target is not rendered, such as an unavailable clear action. */ const focus = (target: DropdownBaseTextboxFocus = 'textbox') => { const element = target === 'clear-action' ? clearAction.value : textbox.value; element?.focus(); return !!element; }; defineExpose({ focus, }); </script> <script lang="ts"> /** * Only for internal use, implements the logic of the textbox element to be implemented in the combobox. */ export default defineComponent({ i18n: { messages }, name: 'dropdown-base-textbox', inheritAttrs: false, }); </script> <style lang="scss" scoped src="./dropdown-base-textbox.scss" />
@@ -1 +1 @@
1
- <template> <div class="b-dropdown-default-textbox" :class="conditionalClasses" data-testid="default-textbox"> <dropdown-base-textbox ref="baseTextbox" v-bind="{ ...$attrs, ...$props }" class="b-dropdown-default-textbox__textbox" :value="value" v-on="listeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope ?? {}" /> </template> <template #icon> <div class="b-dropdown-default-textbox__chevron"> <chevron-up-icon v-show="isDropdownOpen" svg-title="opened" /> <chevron-down-icon v-show="!isDropdownOpen" svg-title="closed" /> </div> </template> </dropdown-base-textbox> </div> </template> <script lang="ts" setup> import { computed, defineComponent, type PropType, ref, useSlots } from 'vue'; import DropdownBaseTextbox from '@/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue'; import { type Booleanish } from '@/types/prop-types'; import { type BentoListboxOptionItem } from '@/types/listbox'; import { stopEventPropagation } from '@/utils/ts/events'; import { DropdownBaseTextboxEvent } from '../dropdown-base-textbox/dropdown-base-textbox.internal.types'; import { DropdownBaseTextboxState } from '../dropdown-base-textbox/dropdown-base-textbox.types'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import ChevronUpIcon from '@adyen/ui-assets-icons-16/vue/chevron-up'; const emit = defineEmits<{ /** * Emitted on clearing the input. */ (e: 'clear'); /** * Emitted on click, space and enter user events. It will open the dropdown. */ (e: 'close'); /** * Emitted on input event. */ (e: 'input', newInput: string); /** * Emitted on keydown event. */ (e: 'keydown', keydownEvent: KeyboardEvent); /** * Emitted on click, space, enter and esc user events. It will close the dropdown. */ (e: 'open'); }>(); const slots = useSlots(); const props = defineProps({ /** * The number of additional items that have been selected in the dropdown. */ additionalItemsSelected: { type: [Number, String] as PropType<number | string>, default: 0 }, /** * Sets the aria-controls attribute on the textbox. This should correspond to the popover's id that the textbox triggers. */ ariaControls: { type: String, required: true, }, /** * Identifies the currently active option in the listbox. */ ariaActiveDescendant: { type: String, default: null, }, /** * Indicates whether the content the textbox triggers is visible or not. Needs to be set accordingly when the textbox is used and changed every time the popover is shown/hidden. */ ariaExpanded: { type: [String, Boolean] as PropType<Booleanish>, required: true, }, /** * If no label element is used then the aria-label is be set should be set. */ ariaLabel: { type: String, default: null, }, /** * Corresponds to the label's id that labels the textbox element. */ ariaLabelledby: { type: String, default: null, }, /** * Renders the dropdown with a condensed style. */ condensed: { type: Boolean, default: false }, /** * Disables any textbox functionality */ disabled: { type: Boolean, default: false }, /** * Selected option's label */ displayValue: { type: String, default: null }, /** * Sets the possibility to type into the textbox */ dynamicFiltering: { type: Boolean, default: false, }, /** * Marks the textbox as invalid, rendering an error state */ isInvalid: { type: Boolean, default: false }, /** * Indicates if the dropdown is open so that * the respective event can be emitted */ open: { type: Boolean, default: false }, /** * Sets the dropdown to readonly state */ readonly: { type: Boolean, default: false }, /** * The selected value's list box item. */ selectedValueItem: { type: [Object, Array] as PropType<BentoListboxOptionItem | Array<BentoListboxOptionItem>>, default: undefined, }, /** * Toggles whether to use the slot content select items shown in the textbox in multiple mode. */ showSlotContentInMultiple: { type: Boolean, default: false }, /** * The input value */ value: { type: String, default: '' }, /* * If enabled, the user will be able to select multiple items. */ multiple: { type: Boolean, default: false }, }); const baseTextbox = ref(null); const conditionalClasses = computed(() => ({ [`b-dropdown-default-textbox--${DropdownBaseTextboxState.ERROR}`]: !props.disabled && !props.readonly && props.isInvalid, [`b-dropdown-default-textbox--${DropdownBaseTextboxState.DISABLED}`]: props.disabled, [`b-dropdown-default-textbox--${DropdownBaseTextboxState.READONLY}`]: props.readonly, [`b-dropdown-default-textbox--${DropdownBaseTextboxState.OPENED}`]: props.open, 'b-dropdown-default-textbox--condensed': props.condensed, })); const isDropdownOpen = computed(() => props.open && !props.disabled && !props.readonly); const onInput = (newInput: string) => { emit('input', newInput); }; const onKeyDown = (e: KeyboardEvent) => { emit('keydown', e); }; const openDropdown = (e: Event) => { stopEventPropagation(e); emit('open'); }; const closeDropdown = (e: Event) => { stopEventPropagation(e); emit('close'); }; const clearValue = (e: Event) => { stopEventPropagation(e); emit('clear'); }; const listeners = { [DropdownBaseTextboxEvent.INPUT]: onInput, [DropdownBaseTextboxEvent.OPEN]: openDropdown, [DropdownBaseTextboxEvent.CLOSE]: closeDropdown, [DropdownBaseTextboxEvent.CLEAR]: clearValue, [DropdownBaseTextboxEvent.KEYDOWN]: onKeyDown, }; const focus = () => { baseTextbox.value.focus(); }; defineExpose({ focus, }); </script> <script lang="ts"> /** * For internal use only, textbox component used in the dropdown component. */ export default defineComponent({ name: 'dropdown-default-textbox', inheritAttrs: false, }); </script> <style lang="scss" scoped src="./dropdown-default-textbox.scss" />
1
+ <template> <div class="b-dropdown-default-textbox" :class="conditionalClasses" data-testid="default-textbox"> <dropdown-base-textbox ref="baseTextbox" v-bind="{ ...$attrs, ...$props }" class="b-dropdown-default-textbox__textbox" :value="value" v-on="listeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope ?? {}" /> </template> <template #icon> <div class="b-dropdown-default-textbox__chevron"> <chevron-up-icon v-show="isDropdownOpen" svg-title="opened" /> <chevron-down-icon v-show="!isDropdownOpen" svg-title="closed" /> </div> </template> </dropdown-base-textbox> </div> </template> <script lang="ts" setup> import { computed, defineComponent, type PropType, ref, useSlots } from 'vue'; import DropdownBaseTextbox from '@/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue'; import { type Booleanish } from '@/types/prop-types'; import { type BentoListboxOptionItem } from '@/types/listbox'; import { stopEventPropagation } from '@/utils/ts/events'; import { DropdownBaseTextboxEvent, type DropdownBaseTextboxFocus, } from '../dropdown-base-textbox/dropdown-base-textbox.internal.types'; import { DropdownBaseTextboxState } from '../dropdown-base-textbox/dropdown-base-textbox.types'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import ChevronUpIcon from '@adyen/ui-assets-icons-16/vue/chevron-up'; const emit = defineEmits<{ /** * Emitted on clearing the input. */ (e: 'clear'); /** * Emitted when the selected value should be cleared. */ (e: 'clear-selection'); /** * Emitted on click, space and enter user events. It will open the dropdown. */ (e: 'close'); /** * Emitted on input event. */ (e: 'input', newInput: string); /** * Emitted on keydown event. */ (e: 'keydown', keydownEvent: KeyboardEvent); /** * Emitted on click, space, enter and esc user events. It will close the dropdown. */ (e: 'open'); }>(); const slots = useSlots(); const props = defineProps({ /** * The number of additional items that have been selected in the dropdown. */ additionalItemsSelected: { type: [Number, String] as PropType<number | string>, default: 0 }, /** * Sets the aria-controls attribute on the textbox. This should correspond to the popover's id that the textbox triggers. */ ariaControls: { type: String, required: true, }, /** * Identifies the currently active option in the listbox. */ ariaActiveDescendant: { type: String, default: null, }, /** * Indicates whether the content the textbox triggers is visible or not. Needs to be set accordingly when the textbox is used and changed every time the popover is shown/hidden. */ ariaExpanded: { type: [String, Boolean] as PropType<Booleanish>, required: true, }, /** * If no label element is used then the aria-label is be set should be set. */ ariaLabel: { type: String, default: null, }, /** * Corresponds to the label's id that labels the textbox element. */ ariaLabelledby: { type: String, default: null, }, /** * Renders the dropdown with a condensed style. */ condensed: { type: Boolean, default: false }, /** * Allows the selected value to be cleared in a single-select dropdown. */ clearable: { type: Boolean, default: false }, /** * Disables any textbox functionality */ disabled: { type: Boolean, default: false }, /** * Selected option's label */ displayValue: { type: String, default: null }, /** * Sets the possibility to type into the textbox */ dynamicFiltering: { type: Boolean, default: false, }, /** * Marks the textbox as invalid, rendering an error state */ isInvalid: { type: Boolean, default: false }, /** * Indicates if the dropdown is open so that * the respective event can be emitted */ open: { type: Boolean, default: false }, /** * Sets the dropdown to readonly state */ readonly: { type: Boolean, default: false }, /** * The selected value's list box item. */ selectedValueItem: { type: [Object, Array] as PropType<BentoListboxOptionItem | Array<BentoListboxOptionItem>>, default: undefined, }, /** * Toggles whether to use the slot content select items shown in the textbox in multiple mode. */ showSlotContentInMultiple: { type: Boolean, default: false }, /** * The input value */ value: { type: String, default: '' }, /* * If enabled, the user will be able to select multiple items. */ multiple: { type: Boolean, default: false }, }); const baseTextbox = ref(null); const conditionalClasses = computed(() => ({ [`b-dropdown-default-textbox--${DropdownBaseTextboxState.ERROR}`]: !props.disabled && !props.readonly && props.isInvalid, [`b-dropdown-default-textbox--${DropdownBaseTextboxState.DISABLED}`]: props.disabled, [`b-dropdown-default-textbox--${DropdownBaseTextboxState.READONLY}`]: props.readonly, [`b-dropdown-default-textbox--${DropdownBaseTextboxState.OPENED}`]: props.open, 'b-dropdown-default-textbox--condensed': props.condensed, })); const isDropdownOpen = computed(() => props.open && !props.disabled && !props.readonly); const onInput = (newInput: string) => { emit('input', newInput); }; const onKeyDown = (e: KeyboardEvent) => { emit('keydown', e); }; const openDropdown = (e: Event) => { stopEventPropagation(e); emit('open'); }; const closeDropdown = (e: Event) => { stopEventPropagation(e); emit('close'); }; const clearValue = (e: Event) => { stopEventPropagation(e); emit('clear'); }; const clearSelection = () => { emit('clear-selection'); }; const listeners = { [DropdownBaseTextboxEvent.INPUT]: onInput, [DropdownBaseTextboxEvent.OPEN]: openDropdown, [DropdownBaseTextboxEvent.CLOSE]: closeDropdown, [DropdownBaseTextboxEvent.CLEAR]: clearValue, [DropdownBaseTextboxEvent.CLEAR_SELECTION]: clearSelection, [DropdownBaseTextboxEvent.KEYDOWN]: onKeyDown, }; /** * Forwards focus to the requested base textbox element. */ const focus = (target?: DropdownBaseTextboxFocus) => { return baseTextbox.value.focus(target); }; defineExpose({ focus, }); </script> <script lang="ts"> /** * For internal use only, textbox component used in the dropdown component. */ export default defineComponent({ name: 'dropdown-default-textbox', inheritAttrs: false, }); </script> <style lang="scss" scoped src="./dropdown-default-textbox.scss" />
@@ -1 +1 @@
1
- <template> <div class="b-dropdown-small-textbox" :class="conditionalClasses"> <dropdown-base-textbox ref="baseTextbox" v-bind="{ ...$attrs, ...$props }" class="b-dropdown-small-textbox__textbox" :size="BentoDropdownSize.SMALL" v-on="listeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope ?? {}" /> </template> <template #icon> <div class="b-dropdown-small-textbox__chevron"> <chevron-up-small-icon v-show="isDropdownOpen" svg-title="opened" /> <chevron-down-small-icon v-show="!isDropdownOpen" svg-title="closed" /> </div> </template> </dropdown-base-textbox> </div> </template> <script setup lang="ts"> import { computed, defineComponent, inject, type PropType, ref, useSlots } from 'vue'; import { DropdownBaseTextboxEvent } from '@/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.internal.types'; import { DropdownBaseTextboxState } from '@/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.types'; import { BentoDropdownSize } from '@/components/dropdown/dropdown.internal.types'; import { type Booleanish } from '@/types/prop-types'; import DropdownBaseTextbox from '@/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue'; import ChevronDownSmallIcon from '@adyen/ui-assets-icons-16/vue/chevron-down-small'; import ChevronUpSmallIcon from '@adyen/ui-assets-icons-16/vue/chevron-up-small'; import { stopEventPropagation } from '@/utils/ts/events'; import { DROPDOWN_UNDERLINE_STYLE_INJECTION_KEY } from '../../dropdown.keys'; const emit = defineEmits<{ /** * Emitted on clearing the input. */ (e: 'clear'); /** * Emitted on click, space and enter user events. It will open the dropdown. */ (e: 'close'); /** * Emitted on input event. */ (e: 'input', newInput: string); /** * Emitted on keydown event. */ (e: 'keydown', keydownEvent: KeyboardEvent); /** * Emitted on click, space, enter and esc user events. It will close the dropdown. */ (e: 'open'); }>(); const slots = useSlots(); const props = defineProps({ /** * The number of additional items that have been selected in the dropdown. */ additionalItemsSelected: { type: [Number, String] as PropType<number | string>, default: 0 }, /** * Sets the aria-controls attribute on the textbox. This should correspond to the popover's id that the textbox triggers. */ ariaControls: { type: String, required: true }, /** * Identifies the currently active option in the listbox. */ ariaActiveDescendant: { type: String, default: null }, /** * Indicates whether the content the textbox triggers is visible or not. Needs to be set accordingly when the textbox is used and changed every time the popover is shown/hidden. */ ariaExpanded: { type: [String, Boolean] as PropType<Booleanish>, required: true, }, /** * If no label element is used then the aria-label is be set should be set. */ ariaLabel: { type: String, default: null }, /** * Corresponds to the label's id that labels the textbox element. */ ariaLabelledby: { type: String, default: null }, /** * Disables any textbox functionality */ disabled: { type: Boolean, default: false }, /** * Selected option's label */ displayValue: { type: String, default: null }, /** * Sets the possibility to type into the textbox */ dynamicFiltering: { type: Boolean, default: false }, /** * Marks the textbox as invalid, rendering an error state */ isInvalid: { type: Boolean, default: false }, /** * Indicates if the dropdown is open so that * the respective event can be emitted */ open: { type: Boolean, default: false }, /** * Sets the dropdown to readonly state */ readonly: { type: Boolean, default: false }, /** * The input value */ value: { type: String, default: '' }, }); const baseTextbox = ref(null); // Inject underline style key to change the dropdown layout const isUnderlineStyle = inject(DROPDOWN_UNDERLINE_STYLE_INJECTION_KEY, false); const conditionalClasses = computed(() => ({ [`b-dropdown-small-textbox--${DropdownBaseTextboxState.DISABLED}`]: props.disabled, [`b-dropdown-small-textbox--${DropdownBaseTextboxState.READONLY}`]: props.readonly, [`b-dropdown-small-textbox--${DropdownBaseTextboxState.OPENED}`]: props.open, 'b-dropdown-small-textbox--underline': isUnderlineStyle, })); const isDropdownOpen = computed(() => props.open && !props.disabled && !props.readonly); const onInput = (newInput: string) => { emit('input', newInput); }; const onKeyDown = (e: KeyboardEvent) => { emit('keydown', e); }; const openDropdown = (e: Event) => { stopEventPropagation(e); emit('open'); }; const closeDropdown = (e: Event) => { stopEventPropagation(e); emit('close'); }; const clearValue = (e: Event) => { stopEventPropagation(e); emit('clear'); }; const listeners = { [DropdownBaseTextboxEvent.INPUT]: onInput, [DropdownBaseTextboxEvent.OPEN]: openDropdown, [DropdownBaseTextboxEvent.CLOSE]: closeDropdown, [DropdownBaseTextboxEvent.CLEAR]: clearValue, [DropdownBaseTextboxEvent.KEYDOWN]: onKeyDown, }; const focus = () => { baseTextbox.value.focus(); }; defineExpose({ focus, }); </script> <script lang="ts"> /** * For internal use only, textbox component in its small variant used in the dropdown component. */ export default defineComponent({ name: 'dropdown-small-textbox', inheritAttrs: false, }); </script> <style lang="scss" scoped src="./dropdown-small-textbox.scss" />
1
+ <template> <div class="b-dropdown-small-textbox" :class="conditionalClasses"> <dropdown-base-textbox ref="baseTextbox" v-bind="{ ...$attrs, ...$props }" class="b-dropdown-small-textbox__textbox" :size="BentoDropdownSize.SMALL" v-on="listeners" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope ?? {}" /> </template> <template #icon> <div class="b-dropdown-small-textbox__chevron"> <chevron-up-small-icon v-show="isDropdownOpen" svg-title="opened" /> <chevron-down-small-icon v-show="!isDropdownOpen" svg-title="closed" /> </div> </template> </dropdown-base-textbox> </div> </template> <script setup lang="ts"> import { computed, defineComponent, inject, type PropType, ref, useSlots } from 'vue'; import { DropdownBaseTextboxEvent, type DropdownBaseTextboxFocus, } from '@/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.internal.types'; import { DropdownBaseTextboxState } from '@/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.types'; import { BentoDropdownSize } from '@/components/dropdown/dropdown.internal.types'; import { type Booleanish } from '@/types/prop-types'; import DropdownBaseTextbox from '@/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue'; import ChevronDownSmallIcon from '@adyen/ui-assets-icons-16/vue/chevron-down-small'; import ChevronUpSmallIcon from '@adyen/ui-assets-icons-16/vue/chevron-up-small'; import { stopEventPropagation } from '@/utils/ts/events'; import { DROPDOWN_UNDERLINE_STYLE_INJECTION_KEY } from '../../dropdown.keys'; const emit = defineEmits<{ /** * Emitted on clearing the input. */ (e: 'clear'); /** * Emitted when the selected value should be cleared. */ (e: 'clear-selection'); /** * Emitted on click, space and enter user events. It will open the dropdown. */ (e: 'close'); /** * Emitted on input event. */ (e: 'input', newInput: string); /** * Emitted on keydown event. */ (e: 'keydown', keydownEvent: KeyboardEvent); /** * Emitted on click, space, enter and esc user events. It will close the dropdown. */ (e: 'open'); }>(); const slots = useSlots(); const props = defineProps({ /** * The number of additional items that have been selected in the dropdown. */ additionalItemsSelected: { type: [Number, String] as PropType<number | string>, default: 0 }, /** * Sets the aria-controls attribute on the textbox. This should correspond to the popover's id that the textbox triggers. */ ariaControls: { type: String, required: true }, /** * Identifies the currently active option in the listbox. */ ariaActiveDescendant: { type: String, default: null }, /** * Indicates whether the content the textbox triggers is visible or not. Needs to be set accordingly when the textbox is used and changed every time the popover is shown/hidden. */ ariaExpanded: { type: [String, Boolean] as PropType<Booleanish>, required: true, }, /** * If no label element is used then the aria-label is be set should be set. */ ariaLabel: { type: String, default: null }, /** * Corresponds to the label's id that labels the textbox element. */ ariaLabelledby: { type: String, default: null }, /** * Disables any textbox functionality */ disabled: { type: Boolean, default: false }, /** * Allows the selected value to be cleared in a single-select dropdown. */ clearable: { type: Boolean, default: false }, /** * Selected option's label */ displayValue: { type: String, default: null }, /** * Sets the possibility to type into the textbox */ dynamicFiltering: { type: Boolean, default: false }, /** * Marks the textbox as invalid, rendering an error state */ isInvalid: { type: Boolean, default: false }, /** * Indicates if the dropdown is open so that * the respective event can be emitted */ open: { type: Boolean, default: false }, /** * Sets the dropdown to readonly state */ readonly: { type: Boolean, default: false }, /** * The input value */ value: { type: String, default: '' }, }); const baseTextbox = ref(null); // Inject underline style key to change the dropdown layout const isUnderlineStyle = inject(DROPDOWN_UNDERLINE_STYLE_INJECTION_KEY, false); const conditionalClasses = computed(() => ({ [`b-dropdown-small-textbox--${DropdownBaseTextboxState.DISABLED}`]: props.disabled, [`b-dropdown-small-textbox--${DropdownBaseTextboxState.READONLY}`]: props.readonly, [`b-dropdown-small-textbox--${DropdownBaseTextboxState.OPENED}`]: props.open, 'b-dropdown-small-textbox--underline': isUnderlineStyle, })); const isDropdownOpen = computed(() => props.open && !props.disabled && !props.readonly); const onInput = (newInput: string) => { emit('input', newInput); }; const onKeyDown = (e: KeyboardEvent) => { emit('keydown', e); }; const openDropdown = (e: Event) => { stopEventPropagation(e); emit('open'); }; const closeDropdown = (e: Event) => { stopEventPropagation(e); emit('close'); }; const clearValue = (e: Event) => { stopEventPropagation(e); emit('clear'); }; const clearSelection = () => { emit('clear-selection'); }; const listeners = { [DropdownBaseTextboxEvent.INPUT]: onInput, [DropdownBaseTextboxEvent.OPEN]: openDropdown, [DropdownBaseTextboxEvent.CLOSE]: closeDropdown, [DropdownBaseTextboxEvent.CLEAR]: clearValue, [DropdownBaseTextboxEvent.CLEAR_SELECTION]: clearSelection, [DropdownBaseTextboxEvent.KEYDOWN]: onKeyDown, }; /** * Forwards focus to the requested base textbox element. */ const focus = (target?: DropdownBaseTextboxFocus) => { return baseTextbox.value.focus(target); }; defineExpose({ focus, }); </script> <script lang="ts"> /** * For internal use only, textbox component in its small variant used in the dropdown component. */ export default defineComponent({ name: 'dropdown-small-textbox', inheritAttrs: false, }); </script> <style lang="scss" scoped src="./dropdown-small-textbox.scss" />
@@ -1 +1 @@
1
- import { action } from '@storybook/addon-actions'; import { ref } from 'vue'; import { type BentoListboxOptionItem, type BentoListboxSelectedValue } from '@/types/listbox'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { type BentoAvatarListboxOptions } from './dropdown-avatar.types'; import { type Meta, type StoryObj } from '@storybook/vue'; import { isVue2 } from 'vue-demi'; import BentoDropdownAvatar from './dropdown-avatar.vue'; const DropdownAvatarDefault = "<template>\n <bento-dropdown-avatar v-model=\"selectedValue\" description=\"Choose a user\" :items=\"items\" label=\"User\" />\n</template>\n\n<script setup lang=\"ts\">\n import { ref } from 'vue';\n import {\n type BentoAvatarListboxOptions,\n BentoDropdownAvatar,\n type BentoListboxSelectedValue,\n } from '@adyen/bento-vue2';\n\n const items: BentoAvatarListboxOptions = [\n { label: 'User 1', value: 'user-id-1' },\n { label: 'User 2', value: 'user-id-2', src: 'image-source.jpeg' },\n ];\n const selectedValue = ref<BentoListboxSelectedValue>('user-id-1');\n</script>\n"; export default { title: 'Dropdown/variants/Avatar', component: BentoDropdownAvatar, } as Meta; type Story = StoryObj<typeof BentoDropdownAvatar>; const AVATAR_OPTIONS: BentoAvatarListboxOptions = [ { label: 'User 1', value: 'user-id-1' }, { label: 'User 2', value: 'user-id-2' }, ]; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdownAvatar }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const clickedAction = (selectedOption: BentoListboxSelectedValue) => { action('update:model-value')(selectedOption); }; const isOptionDisabled = (option: BentoListboxOptionItem) => option.value === 3; return { // Values selectedValue, args: isVue2 ? props : _args, // Methods clickedAction, isOptionDisabled, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown-avatar v-model="selectedValue" v-bind="args" :items="args.items" :isOptionDisabled="isOptionDisabled" label="User" description="Choose a user" @update:model-value="clickedAction" /> </div> `, }), args: { items: AVATAR_OPTIONS, multiple: false, modelValue: AVATAR_OPTIONS[0].value, }, parameters: storybookDocsParameter(DropdownAvatarDefault), };
1
+ import { action } from '@storybook/addon-actions'; import { ref } from 'vue'; import { type BentoListboxOptionItem, type BentoListboxSelectedValue } from '@/types/listbox'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { type BentoAvatarListboxOptions } from './dropdown-avatar.types'; import { type Meta, type StoryObj } from '@storybook/vue'; import { isVue2 } from 'vue-demi'; import BentoDropdownAvatar from './dropdown-avatar.vue'; const DropdownAvatarDefault = "<template>\n <bento-dropdown-avatar v-model=\"selectedValue\" description=\"Choose a user\" :items=\"items\" label=\"User\" />\n</template>\n\n<script setup lang=\"ts\">\n import { ref } from 'vue';\n import {\n type BentoAvatarListboxOptions,\n BentoDropdownAvatar,\n type BentoListboxSelectedValue,\n } from '@adyen/bento-vue2';\n\n const items: BentoAvatarListboxOptions = [\n { label: 'User 1', value: 'user-id-1' },\n { label: 'User 2', value: 'user-id-2', src: 'image-source.jpeg' },\n ];\n const selectedValue = ref<BentoListboxSelectedValue>('user-id-1');\n</script>\n"; export default { title: 'Dropdown/variants/Avatar', component: BentoDropdownAvatar, } as Meta; type Story = StoryObj<typeof BentoDropdownAvatar>; const AVATAR_OPTIONS: BentoAvatarListboxOptions = [ { label: 'User 1', value: 'user-id-1' }, { label: 'User 2', value: 'user-id-2' }, ]; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdownAvatar }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const clickedAction = (selectedOption: BentoListboxSelectedValue) => { action('update:model-value')(selectedOption); }; const isOptionDisabled = (option: BentoListboxOptionItem) => option.value === 3; return { // Values selectedValue, args: isVue2 ? props : _args, // Methods clickedAction, isOptionDisabled, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown-avatar v-model="selectedValue" v-bind="args" :items="args.items" :isOptionDisabled="isOptionDisabled" label="User" description="Choose a user" @update:model-value="clickedAction" /> </div> `, }), args: { items: AVATAR_OPTIONS, clearable: false, multiple: false, dynamicFiltering: false, modelValue: AVATAR_OPTIONS[0].value, }, parameters: storybookDocsParameter(DropdownAvatarDefault), };
@@ -1 +1 @@
1
- import { action } from '@storybook/addon-actions'; import { ref } from 'vue'; import { type BentoListboxOptionItem, type BentoListboxSelectedValue } from '@/types/listbox'; import { BentoCountryCode, BentoCountryVariant } from '@/components/country'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { isVue2 } from 'vue-demi'; import { type Meta, type StoryObj } from '@storybook/vue'; import BentoDropdownCountry from './dropdown-country.vue'; const DropdownCountryDefault = "<template>\n <bento-dropdown-country\n v-model=\"selectedValue\"\n description=\"Choose a country/region\"\n :items=\"items\"\n label=\"Country/Region\"\n variant=\"name\"\n />\n</template>\n\n<script setup lang=\"ts\">\n import { ref } from 'vue';\n import {\n type BentoCountryListboxOptions,\n BentoDropdownCountry,\n type BentoListboxSelectedValue,\n } from '@adyen/bento-vue2';\n\n const items: BentoCountryListboxOptions = [{ value: 'NL' }, { value: 'BE' }];\n const selectedValue = ref<BentoListboxSelectedValue>('NL');\n</script>\n"; export default { title: 'Dropdown/variants/Country', component: BentoDropdownCountry, argTypes: { // Props condensed: { defaultValue: 'false', description: 'If enabled, the user will be able to select multiple items.', table: { category: 'props', defaultValue: { summary: 'false' }, }, control: { type: 'boolean', }, }, multiple: { defaultValue: 'false', description: 'If enabled, the user will be able to select multiple items.', table: { category: 'props', defaultValue: { summary: 'false' }, }, control: { type: 'boolean', }, }, readonly: { defaultValue: 'false', description: 'If enabled, dropdown will display as readonly', table: { category: 'props', defaultValue: { summary: 'false' }, }, control: { type: 'boolean', }, }, variant: { options: Object.values(BentoCountryVariant), mapping: BentoCountryVariant, control: { type: 'select', }, }, }, } as Meta; type Story = StoryObj<typeof BentoDropdownCountry>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdownCountry }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const clickedAction = (selectedOption: BentoListboxSelectedValue) => { action('update:model-value')(selectedOption); }; const isOptionDisabled = (option: BentoListboxOptionItem) => option.value === 3; return { // Values selectedValue, args: isVue2 ? props : _args, // Methods clickedAction, isOptionDisabled, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown-country v-model="selectedValue" v-bind="args" :items="args.items" :isOptionDisabled="isOptionDisabled" label="Country/Region" description="Choose a country/region" @update:model-value="clickedAction" /> </div> `, }), args: { items: [ { value: BentoCountryCode.NL, }, { value: BentoCountryCode.BE, }, ], variant: 'name', multiple: false, modelValue: BentoCountryCode.NL, }, parameters: storybookDocsParameter(DropdownCountryDefault), };
1
+ import { action } from '@storybook/addon-actions'; import { ref } from 'vue'; import { type BentoListboxOptionItem, type BentoListboxSelectedValue } from '@/types/listbox'; import { BentoCountryCode, BentoCountryVariant } from '@/components/country'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { isVue2 } from 'vue-demi'; import { type Meta, type StoryObj } from '@storybook/vue'; import BentoDropdownCountry from './dropdown-country.vue'; const DropdownCountryDefault = "<template>\n <bento-dropdown-country\n v-model=\"selectedValue\"\n description=\"Choose a country/region\"\n :items=\"items\"\n label=\"Country/Region\"\n variant=\"name\"\n />\n</template>\n\n<script setup lang=\"ts\">\n import { ref } from 'vue';\n import {\n type BentoCountryListboxOptions,\n BentoDropdownCountry,\n type BentoListboxSelectedValue,\n } from '@adyen/bento-vue2';\n\n const items: BentoCountryListboxOptions = [{ value: 'NL' }, { value: 'BE' }];\n const selectedValue = ref<BentoListboxSelectedValue>('NL');\n</script>\n"; export default { title: 'Dropdown/variants/Country', component: BentoDropdownCountry, argTypes: { // Props condensed: { defaultValue: 'false', description: 'If enabled, the user will be able to select multiple items.', table: { category: 'props', defaultValue: { summary: 'false' }, }, control: { type: 'boolean', }, }, multiple: { defaultValue: 'false', description: 'If enabled, the user will be able to select multiple items.', table: { category: 'props', defaultValue: { summary: 'false' }, }, control: { type: 'boolean', }, }, readonly: { defaultValue: 'false', description: 'If enabled, dropdown will display as readonly', table: { category: 'props', defaultValue: { summary: 'false' }, }, control: { type: 'boolean', }, }, variant: { options: Object.values(BentoCountryVariant), mapping: BentoCountryVariant, control: { type: 'select', }, }, }, } as Meta; type Story = StoryObj<typeof BentoDropdownCountry>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdownCountry }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const clickedAction = (selectedOption: BentoListboxSelectedValue) => { action('update:model-value')(selectedOption); }; const isOptionDisabled = (option: BentoListboxOptionItem) => option.value === 3; return { // Values selectedValue, args: isVue2 ? props : _args, // Methods clickedAction, isOptionDisabled, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown-country v-model="selectedValue" v-bind="args" :items="args.items" :isOptionDisabled="isOptionDisabled" label="Country/Region" description="Choose a country/region" @update:model-value="clickedAction" /> </div> `, }), args: { items: [ { value: BentoCountryCode.NL, }, { value: BentoCountryCode.BE, }, ], variant: 'name', clearable: false, multiple: false, dynamicFiltering: false, modelValue: BentoCountryCode.NL, }, parameters: storybookDocsParameter(DropdownCountryDefault), };
@@ -1 +1 @@
1
- import { action } from '@storybook/addon-actions'; import { ref } from 'vue'; import { type BentoListboxSelectedValue } from '@/types/listbox'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { BentoCurrencyISOCode } from '@/components/currency'; import { isVue2 } from 'vue-demi'; import { type Meta, type StoryObj } from '@storybook/vue'; import BentoDropdownCurrency from './dropdown-currency.vue'; const DropdownCurrencyDefault = "<template>\n <bento-dropdown-currency v-model=\"selectedValue\" description=\"Choose a currency\" :items=\"items\" label=\"Currency\" />\n</template>\n\n<script setup lang=\"ts\">\n import { ref } from 'vue';\n import {\n type BentoCurrencyListboxOptions,\n BentoDropdownCurrency,\n type BentoListboxSelectedValue,\n } from '@adyen/bento-vue2';\n\n const items: BentoCurrencyListboxOptions = [\n { label: 'Australian Dollars', value: 'AUD' },\n { label: 'Euro', value: 'EUR' },\n { label: 'US Dollars', value: 'USD' },\n { label: 'Japanese Yen', value: 'JPY' },\n ];\n const selectedValue = ref<BentoListboxSelectedValue>('EUR');\n</script>\n"; export default { title: 'Dropdown/variants/Currency', component: BentoDropdownCurrency, } as Meta; type Story = StoryObj<typeof BentoDropdownCurrency>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdownCurrency }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const clickedAction = (selectedOption: BentoListboxSelectedValue) => { action('update:model-value')(selectedOption); }; return { // Values selectedValue, args: isVue2 ? props : _args, // Methods clickedAction, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown-currency v-model="selectedValue" v-bind="args" :items="args.items" label="Currency" description="Choose a currency" @update:model-value="clickedAction" /> </div> `, }), args: { items: [ { label: 'Australian Dollars', value: BentoCurrencyISOCode.AUD, }, { label: 'Euro', value: BentoCurrencyISOCode.EUR, }, { label: 'US Dollars', value: BentoCurrencyISOCode.USD, }, { label: 'Japanese Yen', value: BentoCurrencyISOCode.JPY, }, ], multiple: false, dynamicFiltering: false, modelValue: BentoCurrencyISOCode.EUR, }, parameters: storybookDocsParameter(DropdownCurrencyDefault), };
1
+ import { action } from '@storybook/addon-actions'; import { ref } from 'vue'; import { type BentoListboxSelectedValue } from '@/types/listbox'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { BentoCurrencyISOCode } from '@/components/currency'; import { isVue2 } from 'vue-demi'; import { type Meta, type StoryObj } from '@storybook/vue'; import BentoDropdownCurrency from './dropdown-currency.vue'; const DropdownCurrencyDefault = "<template>\n <bento-dropdown-currency v-model=\"selectedValue\" description=\"Choose a currency\" :items=\"items\" label=\"Currency\" />\n</template>\n\n<script setup lang=\"ts\">\n import { ref } from 'vue';\n import {\n type BentoCurrencyListboxOptions,\n BentoDropdownCurrency,\n type BentoListboxSelectedValue,\n } from '@adyen/bento-vue2';\n\n const items: BentoCurrencyListboxOptions = [\n { label: 'Australian Dollars', value: 'AUD' },\n { label: 'Euro', value: 'EUR' },\n { label: 'US Dollars', value: 'USD' },\n { label: 'Japanese Yen', value: 'JPY' },\n ];\n const selectedValue = ref<BentoListboxSelectedValue>('EUR');\n</script>\n"; export default { title: 'Dropdown/variants/Currency', component: BentoDropdownCurrency, } as Meta; type Story = StoryObj<typeof BentoDropdownCurrency>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdownCurrency }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const clickedAction = (selectedOption: BentoListboxSelectedValue) => { action('update:model-value')(selectedOption); }; return { // Values selectedValue, args: isVue2 ? props : _args, // Methods clickedAction, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown-currency v-model="selectedValue" v-bind="args" :items="args.items" label="Currency" description="Choose a currency" @update:model-value="clickedAction" /> </div> `, }), args: { items: [ { label: 'Australian Dollars', value: BentoCurrencyISOCode.AUD, }, { label: 'Euro', value: BentoCurrencyISOCode.EUR, }, { label: 'US Dollars', value: BentoCurrencyISOCode.USD, }, { label: 'Japanese Yen', value: BentoCurrencyISOCode.JPY, }, ], clearable: false, multiple: false, dynamicFiltering: false, modelValue: BentoCurrencyISOCode.EUR, }, parameters: storybookDocsParameter(DropdownCurrencyDefault), };
@@ -1 +1 @@
1
- import { action } from '@storybook/addon-actions'; import { ref } from 'vue'; import { type BentoListboxSelectedValue } from '@/types/listbox'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { isVue2 } from 'vue-demi'; import BentoDropdownPaymentMethod from './dropdown-payment-method.vue'; import { type BentoPaymentMethodListboxOptions } from './dropdown-payment-method.types'; import { type Meta, type StoryObj } from '@storybook/vue'; const DropdownPaymentMethodDefaultExample = "<template>\n <bento-dropdown-payment-method\n v-model=\"selectedValue\"\n :items=\"items\"\n label=\"Payment Method\"\n description=\"Choose a payment method\"\n />\n</template>\n\n<script setup lang=\"ts\">\n import {\n BentoDropdownPaymentMethod,\n type BentoListboxSelectedValue,\n type BentoPaymentMethodListboxOptions,\n } from '@adyen/bento-vue2';\n import { ref } from 'vue';\n\n const selectedValue = ref<BentoListboxSelectedValue>();\n\n const items: BentoPaymentMethodListboxOptions = [\n { label: 'iDEAL', value: 'ideal' },\n { label: 'Visa', value: 'visa' },\n { label: 'Maestro', value: 'maestro' },\n { label: 'Apple Pay', value: 'applepay' },\n ];\n</script>\n"; const DropdownPaymentMethodWithDescriptionExample = "<template>\n <bento-dropdown-payment-method\n v-model=\"selectedValue\"\n :items=\"items\"\n label=\"Payment Method\"\n description=\"Choose a payment method\"\n />\n</template>\n\n<script setup lang=\"ts\">\n import {\n BentoDropdownPaymentMethod,\n type BentoListboxSelectedValue,\n type BentoPaymentMethodListboxOptions,\n } from '@adyen/bento-vue2';\n import { ref } from 'vue';\n\n const selectedValue = ref<BentoListboxSelectedValue>();\n const items: BentoPaymentMethodListboxOptions = [\n {\n label: 'iDEAL',\n value: 'ideal',\n description: 'Dutch online payment system',\n },\n {\n label: 'Visa',\n value: 'visa',\n description: 'Global network for transactions',\n },\n {\n label: 'Maestro',\n value: 'maestro',\n description: 'Global payment network',\n },\n {\n label: 'Apple Pay',\n value: 'applepay',\n description: 'Digital wallet and payment service',\n },\n ];\n</script>\n"; export default { title: 'Dropdown/variants/Payment Method', component: BentoDropdownPaymentMethod, } as Meta; type Story = StoryObj<typeof BentoDropdownPaymentMethod>; const ITEMS: BentoPaymentMethodListboxOptions = [ { label: 'iDEAL', value: 'ideal', description: 'Dutch online payment system', }, { label: 'Visa', value: 'visa', description: 'Global network for transactions', }, { label: 'Maestro', value: 'maestro', description: 'Global payment network', }, { label: 'Apple Pay', value: 'applepay', description: 'Digital wallet and payment service', }, ]; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdownPaymentMethod }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const clickedAction = (selectedOption: BentoListboxSelectedValue) => { selectedValue.value = selectedOption; action('update:model-value')(selectedOption); }; return { // Values selectedValue, args: isVue2 ? props : _args, // Methods clickedAction, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown-payment-method :model-value="selectedValue" v-bind="args" :items="args.items" label="Payment Method" description="Choose a payment method" @update:model-value="clickedAction" /> </div> `, }), args: { items: ITEMS.map(({ description, ...item }) => ({ ...item })), // Remove descriptions multiple: false, }, parameters: storybookDocsParameter(DropdownPaymentMethodDefaultExample), }; export const WithDescription: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdownPaymentMethod }, props: Object.keys(argTypes), setup(props) { const selectedValue = ref<BentoListboxSelectedValue>(''); const clickedAction = (selectedOption: BentoListboxSelectedValue) => { selectedValue.value = selectedOption; action('update:model-value')(selectedOption); }; return { // Values selectedValue, args: isVue2 ? props : _args, // Methods clickedAction, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown-payment-method :model-value="selectedValue" v-bind="args" :items="args.items" label="Payment Method" description="Choose a payment method" @update:model-value="clickedAction" /> </div> `, }), args: { items: ITEMS, multiple: false, }, parameters: storybookDocsParameter(DropdownPaymentMethodWithDescriptionExample), };
1
+ import { action } from '@storybook/addon-actions'; import { ref } from 'vue'; import { type BentoListboxSelectedValue } from '@/types/listbox'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { isVue2 } from 'vue-demi'; import BentoDropdownPaymentMethod from './dropdown-payment-method.vue'; import { type BentoPaymentMethodListboxOptions } from './dropdown-payment-method.types'; import { type Meta, type StoryObj } from '@storybook/vue'; const DropdownPaymentMethodDefaultExample = "<template>\n <bento-dropdown-payment-method\n v-model=\"selectedValue\"\n :items=\"items\"\n label=\"Payment Method\"\n description=\"Choose a payment method\"\n />\n</template>\n\n<script setup lang=\"ts\">\n import {\n BentoDropdownPaymentMethod,\n type BentoListboxSelectedValue,\n type BentoPaymentMethodListboxOptions,\n } from '@adyen/bento-vue2';\n import { ref } from 'vue';\n\n const selectedValue = ref<BentoListboxSelectedValue>();\n\n const items: BentoPaymentMethodListboxOptions = [\n { label: 'iDEAL', value: 'ideal' },\n { label: 'Visa', value: 'visa' },\n { label: 'Maestro', value: 'maestro' },\n { label: 'Apple Pay', value: 'applepay' },\n ];\n</script>\n"; const DropdownPaymentMethodWithDescriptionExample = "<template>\n <bento-dropdown-payment-method\n v-model=\"selectedValue\"\n :items=\"items\"\n label=\"Payment Method\"\n description=\"Choose a payment method\"\n />\n</template>\n\n<script setup lang=\"ts\">\n import {\n BentoDropdownPaymentMethod,\n type BentoListboxSelectedValue,\n type BentoPaymentMethodListboxOptions,\n } from '@adyen/bento-vue2';\n import { ref } from 'vue';\n\n const selectedValue = ref<BentoListboxSelectedValue>();\n const items: BentoPaymentMethodListboxOptions = [\n {\n label: 'iDEAL',\n value: 'ideal',\n description: 'Dutch online payment system',\n },\n {\n label: 'Visa',\n value: 'visa',\n description: 'Global network for transactions',\n },\n {\n label: 'Maestro',\n value: 'maestro',\n description: 'Global payment network',\n },\n {\n label: 'Apple Pay',\n value: 'applepay',\n description: 'Digital wallet and payment service',\n },\n ];\n</script>\n"; export default { title: 'Dropdown/variants/Payment Method', component: BentoDropdownPaymentMethod, } as Meta; type Story = StoryObj<typeof BentoDropdownPaymentMethod>; const ITEMS: BentoPaymentMethodListboxOptions = [ { label: 'iDEAL', value: 'ideal', description: 'Dutch online payment system', }, { label: 'Visa', value: 'visa', description: 'Global network for transactions', }, { label: 'Maestro', value: 'maestro', description: 'Global payment network', }, { label: 'Apple Pay', value: 'applepay', description: 'Digital wallet and payment service', }, ]; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdownPaymentMethod }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const clickedAction = (selectedOption: BentoListboxSelectedValue) => { selectedValue.value = selectedOption; action('update:model-value')(selectedOption); }; return { // Values selectedValue, args: isVue2 ? props : _args, // Methods clickedAction, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown-payment-method :model-value="selectedValue" v-bind="args" :items="args.items" label="Payment Method" description="Choose a payment method" @update:model-value="clickedAction" /> </div> `, }), args: { items: ITEMS.map(({ description, ...item }) => ({ ...item })), // Remove descriptions clearable: false, multiple: false, dynamicFiltering: false, }, parameters: storybookDocsParameter(DropdownPaymentMethodDefaultExample), }; export const WithDescription: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdownPaymentMethod }, props: Object.keys(argTypes), setup(props) { const selectedValue = ref<BentoListboxSelectedValue>(''); const clickedAction = (selectedOption: BentoListboxSelectedValue) => { selectedValue.value = selectedOption; action('update:model-value')(selectedOption); }; return { // Values selectedValue, args: isVue2 ? props : _args, // Methods clickedAction, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown-payment-method :model-value="selectedValue" v-bind="args" :items="args.items" label="Payment Method" description="Choose a payment method" @update:model-value="clickedAction" /> </div> `, }), args: { items: ITEMS, clearable: false, multiple: false, dynamicFiltering: false, }, parameters: storybookDocsParameter(DropdownPaymentMethodWithDescriptionExample), };
@@ -1 +1 @@
1
- import { action } from '@storybook/addon-actions'; import { ref } from 'vue'; import { type BentoListboxSelectedValue } from '@/types/listbox'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { isVue2 } from 'vue-demi'; import { type Meta, type StoryObj } from '@storybook/vue'; import BentoDropdownTag from './dropdown-tag.vue'; const DropdownTagDefault = "<template>\n <bento-dropdown-tag v-model=\"selectedValue\" description=\"Choose a tag\" :items=\"items\" label=\"Tag\" />\n</template>\n\n<script setup lang=\"ts\">\n import { ref } from 'vue';\n import { BentoDropdownTag, type BentoListboxSelectedValue, type BentoTagListboxOptions } from '@adyen/bento-vue2';\n\n const items: BentoTagListboxOptions = [\n { label: 'Offline', value: 'unique-id-1', variant: 'red' },\n { label: 'Online', value: 'unique-id-2', variant: 'green' },\n { label: 'Standby', value: 'unique-id-3', variant: 'orange' },\n { label: 'Busy with things', value: 'unique-id-4', variant: 'blue' },\n { label: 'Extras', value: 'unique-id-5', variant: 'grey' },\n ];\n const selectedValue = ref<BentoListboxSelectedValue>('unique-id-1');\n</script>\n"; export default { title: 'Dropdown/variants/Tag', component: BentoDropdownTag, } as Meta; type Story = StoryObj<typeof BentoDropdownTag>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdownTag }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const clickedAction = (selectedOption: BentoListboxSelectedValue) => { action('update:model-value')(selectedOption); }; return { // Values selectedValue, args: isVue2 ? props : _args, // Methods clickedAction, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown-tag v-model="selectedValue" v-bind="args" :items="args.items" placeholder="Select a Tag" label="Tag" description="Choose a tag" @update:model-value="clickedAction" /> </div> `, }), }; Default.args = { items: [ { label: 'Offline', value: 'unique-id-1', variant: 'red' }, { label: 'Online', value: 'unique-id-2', variant: 'green' }, { label: 'Standby', value: 'unique-id-3', variant: 'orange' }, { label: 'Busy with things', value: 'unique-id-4', variant: 'blue' }, { label: 'Extras', value: 'unique-id-5', variant: 'grey' }, ], multiple: false, modelValue: 'unique-id-1', }; Default.parameters = storybookDocsParameter(DropdownTagDefault);
1
+ import { action } from '@storybook/addon-actions'; import { ref } from 'vue'; import { type BentoListboxSelectedValue } from '@/types/listbox'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { isVue2 } from 'vue-demi'; import { type Meta, type StoryObj } from '@storybook/vue'; import BentoDropdownTag from './dropdown-tag.vue'; const DropdownTagDefault = "<template>\n <bento-dropdown-tag v-model=\"selectedValue\" description=\"Choose a tag\" :items=\"items\" label=\"Tag\" />\n</template>\n\n<script setup lang=\"ts\">\n import { ref } from 'vue';\n import { BentoDropdownTag, type BentoListboxSelectedValue, type BentoTagListboxOptions } from '@adyen/bento-vue2';\n\n const items: BentoTagListboxOptions = [\n { label: 'Offline', value: 'unique-id-1', variant: 'red' },\n { label: 'Online', value: 'unique-id-2', variant: 'green' },\n { label: 'Standby', value: 'unique-id-3', variant: 'orange' },\n { label: 'Busy with things', value: 'unique-id-4', variant: 'blue' },\n { label: 'Extras', value: 'unique-id-5', variant: 'grey' },\n ];\n const selectedValue = ref<BentoListboxSelectedValue>('unique-id-1');\n</script>\n"; export default { title: 'Dropdown/variants/Tag', component: BentoDropdownTag, } as Meta; type Story = StoryObj<typeof BentoDropdownTag>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDropdownTag }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValue = ref(args.modelValue); const clickedAction = (selectedOption: BentoListboxSelectedValue) => { action('update:model-value')(selectedOption); }; return { // Values selectedValue, args: isVue2 ? props : _args, // Methods clickedAction, }; }, template: ` <div style="max-width: 300px"> <bento-dropdown-tag v-model="selectedValue" v-bind="args" :items="args.items" placeholder="Select a Tag" label="Tag" description="Choose a tag" @update:model-value="clickedAction" /> </div> `, }), }; Default.args = { items: [ { label: 'Offline', value: 'unique-id-1', variant: 'red' }, { label: 'Online', value: 'unique-id-2', variant: 'green' }, { label: 'Standby', value: 'unique-id-3', variant: 'orange' }, { label: 'Busy with things', value: 'unique-id-4', variant: 'blue' }, { label: 'Extras', value: 'unique-id-5', variant: 'grey' }, ], clearable: false, multiple: false, dynamicFiltering: false, modelValue: 'unique-id-1', }; Default.parameters = storybookDocsParameter(DropdownTagDefault);
@@ -79,6 +79,17 @@ via the default slot.
79
79
 
80
80
  ## Modifiers
81
81
 
82
+ ### Clearable
83
+
84
+ Set `clearable` to let users remove the selected value from an _optional_ single-select Dropdown. It defaults to `false`
85
+ and is not supported for multi-select Dropdowns. Do not enable it on _required_ fields.
86
+
87
+ When the Dropdown is open with a selected value, Clear selection replaces the chevron. Clearing emits `null`, restores
88
+ the placeholder, keeps the options open, and returns focus to the Dropdown.
89
+
90
+ With dynamic filtering, a search query replaces Clear selection with Clear search. Clearing the query preserves the
91
+ selected value and restores Clear selection.
92
+
82
93
  ### Display value
83
94
 
84
95
  Each item in the dropdown can optionally be customized in the way it display when selected. To enable this feature, pass