@adyen/bento-mcp 0.1.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +50 -5
- package/SKILL.md +188 -0
- package/bin/add-skill.js +46 -0
- package/dist/assets/components/accordion/accordion.types.ts +1 -0
- package/dist/assets/components/action-bar/action-bar.types.ts +1 -0
- package/dist/assets/components/ai-tag/ai-tag.types.ts +1 -0
- package/dist/assets/components/alert/alert.types.ts +1 -0
- package/dist/assets/components/anchor-scroller/anchor-scroller.types.ts +1 -0
- package/dist/assets/components/anchor-scroller/components/anchor-scroller-list/anchor-scroller-list.types.ts +1 -0
- package/dist/assets/components/avatar/components/avatar-image/avatar-image.types.ts +1 -0
- package/dist/assets/components/button/button.types.ts +1 -0
- package/dist/assets/components/button/components/ai-button/ai-button.types.ts +1 -0
- package/dist/assets/components/button/components/button-actions/button-actions.types.ts +1 -0
- package/dist/assets/components/button/components/loading-button/loading-button.types.ts +1 -0
- package/dist/assets/components/button/components/toggle-button/toggle.types.ts +1 -0
- package/dist/assets/components/card/card.types.ts +1 -0
- package/dist/assets/components/checkbox/checkbox.types.ts +1 -0
- package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.types.ts +1 -0
- package/dist/assets/components/chip/chip.types.ts +1 -0
- package/dist/assets/components/code-snippet/code-snippet.types.ts +1 -0
- package/dist/assets/components/country/country.types.ts +1 -0
- package/dist/assets/components/currency/currency.types.ts +1 -0
- package/dist/assets/components/dashed-underline/dashed-underline.types.ts +1 -0
- package/dist/assets/components/data-grid/components/data-grid-cell/data-grid-cell.types.ts +1 -0
- package/dist/assets/components/data-grid/components/data-grid-cell/data-grid-cell.vue +1 -1
- package/dist/assets/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.types.ts +1 -0
- package/dist/assets/components/data-grid/components/data-grid-columns/data-grid-columns.types.ts +1 -0
- package/dist/assets/components/data-grid/components/data-grid-columns/data-grid-columns.vue +1 -1
- package/dist/assets/components/data-grid/components/data-grid-config-settings/data-grid-config-settings.types.ts +1 -0
- package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.types.ts +1 -0
- package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.vue +1 -1
- package/dist/assets/components/data-grid/components/data-grid-static-icon/data-grid-static-icon.types.ts +1 -0
- package/dist/assets/components/data-grid/components/data-grid-summary-cell/data-grid-summary-cell.types.ts +1 -0
- package/dist/assets/components/data-grid/components/data-grid-tag-with-text-cell/data-grid-tag-with-text-cell.types.ts +1 -0
- package/dist/assets/components/data-grid/components/data-grid-trend-cell/data-grid-trend-cell.types.ts +1 -0
- package/dist/assets/components/data-grid/composables/use-calculate-column-width/use-calculate-column-width.types.ts +1 -0
- package/dist/assets/components/data-grid/composables/use-column-hovering/use-column-hovering.types.ts +1 -0
- package/dist/assets/components/data-grid/composables/use-config-settings/use-config.settings.types.ts +1 -0
- package/dist/assets/components/data-grid/composables/use-keyboard-navigator/use-keyboard-navigator.types.ts +1 -0
- package/dist/assets/components/data-grid/data-grid.types.ts +1 -0
- package/dist/assets/components/data-grid/data-grid.vue +1 -1
- package/dist/assets/components/date/date.types.ts +1 -0
- package/dist/assets/components/date-picker/composables/use-date-picker-single-calendar-text.types.ts +1 -0
- package/dist/assets/components/date-picker/date-picker.types.ts +1 -0
- package/dist/assets/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.types.ts +1 -0
- package/dist/assets/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.vue +1 -1
- package/dist/assets/components/date-range-picker/composables/use-date-range-picker-calendar-text.types.ts +1 -0
- package/dist/assets/components/date-range-picker/composables/use-granularity-memory.types.ts +1 -0
- package/dist/assets/components/date-range-picker/date-range-picker.types.ts +1 -0
- package/dist/assets/components/divider/divider.types.ts +1 -0
- package/dist/assets/components/draggable-handle/draggable-handle.types.ts +1 -0
- package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.types.ts +1 -0
- package/dist/assets/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.types.ts +1 -0
- package/dist/assets/components/dropdown/components/variants/dropdown-country/dropdown-country.types.ts +1 -0
- package/dist/assets/components/dropdown/components/variants/dropdown-currency/dropdown-currency.types.ts +1 -0
- package/dist/assets/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.types.ts +1 -0
- package/dist/assets/components/dropdown/components/variants/dropdown-tag/dropdown-tag.types.ts +1 -0
- package/dist/assets/components/dropdown/composables/use-display-value.types.ts +1 -0
- package/dist/assets/components/dropdown/composables/use-keyboard-navigation.types.ts +1 -0
- package/dist/assets/components/dropdown/dropdown.types.ts +1 -0
- package/dist/assets/components/empty-state/empty-state.types.ts +1 -0
- package/dist/assets/components/file-uploader/components/file-uploader-file-card/file-uploader-file-card.types.ts +1 -0
- package/dist/assets/components/file-uploader/components/file-uploader-restrictions/file-uploader-restrictions.types.ts +1 -0
- package/dist/assets/components/file-uploader/composables/use-drag-drop-state/use-drag-drop-state.types.ts +1 -0
- package/dist/assets/components/file-uploader/file-uploader.types.ts +1 -0
- package/dist/assets/components/filter-bar/components/all-filters-modal/all-filters-modal.types.ts +1 -0
- package/dist/assets/components/filter-bar/components/all-filters-modal/all-filters-modal.vue +1 -1
- package/dist/assets/components/filter-bar/components/base-filter/base-filter.types.ts +1 -0
- package/dist/assets/components/filter-bar/components/checkbox-group-filter/checkbox-group-filter.types.ts +1 -0
- package/dist/assets/components/filter-bar/components/filter-bar-button-group/filter-bar-button-group.types.ts +1 -0
- package/dist/assets/components/filter-bar/components/input-filter/input-filter.types.ts +1 -0
- package/dist/assets/components/filter-bar/components/radio-group-filter/radio-group-filter.types.ts +1 -0
- package/dist/assets/components/filter-bar/components/toggle-filter/toggle-filter.types.ts +1 -0
- package/dist/assets/components/filter-bar/filter-bar.types.ts +1 -0
- package/dist/assets/components/focus-trap/focus-trap.types.ts +1 -0
- package/dist/assets/components/form-layout/components/form-layout-field-tip/form-layout-tip.types.ts +1 -0
- package/dist/assets/components/form-layout/components/form-layout-group/form-layout-group.types.ts +1 -0
- package/dist/assets/components/form-layout/form-layout.types.ts +1 -0
- package/dist/assets/components/header/header.types.ts +1 -0
- package/dist/assets/components/header-with-views/components/header-with-views-all-views/header-with-views-all-views.types.ts +1 -0
- package/dist/assets/components/header-with-views/components/header-with-views-item-edit-form/header-with-views-edit-form.types.ts +1 -0
- package/dist/assets/components/header-with-views/header-with-views.types.ts +1 -0
- package/dist/assets/components/image/image.types.ts +1 -0
- package/dist/assets/components/info-icon/info-icon.types.ts +1 -0
- package/dist/assets/components/input-field/input-field.types.ts +1 -0
- package/dist/assets/components/input-field-password/input-field-password.types.ts +1 -0
- package/dist/assets/components/input-field-phone-number/input-field-phone-number.types.ts +1 -0
- package/dist/assets/components/input-time/input-time.docs.mdx +52 -0
- package/dist/assets/components/input-time/input-time.stories.ts +1 -0
- package/dist/assets/components/input-time/input-time.types.ts +1 -0
- package/dist/assets/components/input-time/input-time.vue +1 -0
- package/dist/assets/components/inspector/inspector.types.ts +1 -0
- package/dist/assets/components/internal/base-button/base-button.types.ts +1 -0
- package/dist/assets/components/internal/button-actions-with-menu/button-actions-with-menu.types.ts +1 -0
- package/dist/assets/components/internal/calendar/calendar.types.ts +1 -0
- package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.types.ts +1 -0
- package/dist/assets/components/internal/calendar/components/calendar-month/components/calendar-month-day/calendar-month-day.types.ts +1 -0
- package/dist/assets/components/internal/calendar/components/calendar-month/composables/granular-min-max-date.types.ts +1 -0
- package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.types.ts +1 -0
- package/dist/assets/components/internal/calendar/components/calendar-year/components/calendar-year-month/calendar-year-month.types.ts +1 -0
- package/dist/assets/components/internal/calendar/composables/use-granularity-adjustments.types.ts +1 -0
- package/dist/assets/components/internal/checkbox-input/checkbox-input.types.ts +1 -0
- package/dist/assets/components/internal/copy/copy.types.ts +1 -0
- package/dist/assets/components/internal/dialog-page/dialog-page.types.ts +1 -0
- package/dist/assets/components/internal/grid-layout/grid-layout.types.ts +1 -0
- package/dist/assets/components/internal/header-meta/header-meta.types.ts +1 -0
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-category/listbox-multi-select-category.types.ts +1 -0
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/listbox-multi-select-options.types.ts +1 -0
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/listbox-multi-select.types.ts +1 -0
- package/dist/assets/components/internal/listbox/listbox.types.ts +1 -0
- package/dist/assets/components/internal/radio-input/radio-input.types.ts +1 -0
- package/dist/assets/components/internal/teleport/teleport.types.ts +1 -0
- package/dist/assets/components/internal/tooltip/tooltip.types.ts +1 -0
- package/dist/assets/components/layout/layout.types.ts +1 -0
- package/dist/assets/components/link/link.types.ts +1 -0
- package/dist/assets/components/list/components/list-item/list-item.stories.ts +1 -0
- package/dist/assets/components/list/components/list-item/list-item.types.ts +1 -0
- package/dist/assets/components/list/components/list-item/list-item.vue +1 -0
- package/dist/assets/components/list/list.docs.mdx +97 -0
- package/dist/assets/components/list/list.stories.ts +1 -0
- package/dist/assets/components/list/list.types.ts +1 -0
- package/dist/assets/components/list/list.vue +1 -0
- package/dist/assets/components/loading-indicator/loading-indicator.types.ts +1 -0
- package/dist/assets/components/menu/components/menu-item/menu-item.types.ts +1 -0
- package/dist/assets/components/menu/menu.types.ts +1 -0
- package/dist/assets/components/modal/components/modal-page/modal-page.types.ts +1 -0
- package/dist/assets/components/modal/modal.types.ts +1 -0
- package/dist/assets/components/pagination/pagination.types.ts +1 -0
- package/dist/assets/components/payment-method/payment-method.types.ts +1 -0
- package/dist/assets/components/popover/popover.types.ts +1 -0
- package/dist/assets/components/popper-container/popper-container.types.ts +1 -0
- package/dist/assets/components/promo-banner/promo-banner.types.ts +1 -0
- package/dist/assets/components/radio-group/components/radio-button/radio-button.types.ts +1 -0
- package/dist/assets/components/radio-group/radio-group.types.ts +1 -0
- package/dist/assets/components/rich-text-editor/components/rich-text-editor-content-area/rich-text-editor-content-area.types.ts +1 -0
- package/dist/assets/components/rich-text-editor/components/rich-text-editor-renderer/components/rich-text-editor-code-block/rich-text-editor-code-block.types.ts +1 -0
- package/dist/assets/components/rich-text-editor/components/rich-text-editor-renderer/components/rich-text-editor-inline-code/rich-text-editor-inline-code.types.ts +1 -0
- package/dist/assets/components/rich-text-editor/components/rich-text-editor-renderer/components/rich-text-editor-link/rich-text-editor-link.types.ts +1 -0
- package/dist/assets/components/rich-text-editor/components/rich-text-editor-renderer/components/rich-text-editor-list-item/rich-text-editor-list-item.types.ts +1 -0
- package/dist/assets/components/rich-text-editor/components/rich-text-editor-renderer/rich-text-editor-renderer.types.ts +1 -0
- package/dist/assets/components/rich-text-editor/rich-text-editor.types.ts +1 -0
- package/dist/assets/components/search-bar/search-bar.types.ts +1 -0
- package/dist/assets/components/secondary-nav/components/secondary-nav-category/secondary-nav-category.vue +1 -1
- package/dist/assets/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue +1 -1
- package/dist/assets/components/secondary-nav/secondary-nav.docs.mdx +32 -2
- package/dist/assets/components/secondary-nav/secondary-nav.stories.ts +1 -1
- package/dist/assets/components/secondary-nav/secondary-nav.types.ts +1 -0
- package/dist/assets/components/secondary-nav/secondary-nav.vue +1 -1
- package/dist/assets/components/segmented-control/segmented-control.types.ts +1 -0
- package/dist/assets/components/selection-card/components/selection-card-group/selection-card-group.types.ts +1 -0
- package/dist/assets/components/selection-card/selection-card.types.ts +1 -0
- package/dist/assets/components/sidepanel/sidepanel.types.ts +1 -0
- package/dist/assets/components/status/status.types.ts +1 -0
- package/dist/assets/components/stepper/stepper.types.ts +1 -0
- package/dist/assets/components/structured-list/structured-list.types.ts +1 -0
- package/dist/assets/components/summary-grid/components/summary-grid-item/summary-grid-item.types.ts +1 -0
- package/dist/assets/components/summary-grid/components/summary-grid-item-amount/summary-grid-item-amount.types.ts +1 -0
- package/dist/assets/components/summary-grid/components/summary-grid-item-custom/summary-grid-item-custom.types.ts +1 -0
- package/dist/assets/components/summary-grid/components/summary-grid-item-numeric/summary-grid-item-numeric.types.ts +1 -0
- package/dist/assets/components/summary-grid/components/summary-grid-item-percentage/summary-grid-item-percentage.types.ts +1 -0
- package/dist/assets/components/summary-grid/components/summary-grid-item-text/summary-grid-item-text.types.ts +1 -0
- package/dist/assets/components/summary-grid/summary-grid.types.ts +1 -0
- package/dist/assets/components/tabs/components/tab.types.ts +1 -0
- package/dist/assets/components/tabs/tabs.types.ts +1 -0
- package/dist/assets/components/tag/tag.types.ts +1 -0
- package/dist/assets/components/textarea/textarea.types.ts +1 -0
- package/dist/assets/components/timeline/components/timeline-item/timeline-item.vue +1 -1
- package/dist/assets/components/timeline/timeline.stories.ts +1 -1
- package/dist/assets/components/timeline/timeline.types.ts +1 -0
- package/dist/assets/components/toast/components/toast-shadow-stack/toast-shadow-stack.types.ts +1 -0
- package/dist/assets/components/toggle/toggle.types.ts +1 -0
- package/dist/assets/components/tutorial/tutorial.types.ts +1 -0
- package/dist/assets/components/typography/typography.types.ts +1 -0
- package/dist/assets/components.json +102 -0
- package/dist/assets/composables/use-bento-theme/install.ts +1 -0
- package/dist/assets/composables/use-bento-theme/install.types.ts +1 -0
- package/dist/assets/composables/use-bento-theme/use-bento-theme.types.ts +1 -0
- package/dist/assets/composables/use-bento-toast-controller/install.ts +1 -0
- package/dist/assets/composables/use-bento-toast-controller/use-bento-toast-controller.types.ts +1 -0
- package/dist/assets/composables/use-form-validate/use-form.types.ts +1 -0
- package/dist/assets/composables/use-form-validate-zod-4/use-form.types.ts +1 -0
- package/dist/assets/composables/use-virtual-list/use-virtual-list.types.ts +1 -0
- package/dist/assets/deprecations.md +330 -0
- package/dist/assets/directives/keyboard-navigation/keyboard-navigation.types.ts +1 -0
- package/dist/assets/directives/tooltip/tooltip.types.ts +1 -0
- package/dist/assets/icons.json +415 -0
- package/dist/assets/index.ts +1 -1
- package/dist/assets/install.ts +1 -0
- package/dist/assets/tailwind/README.md +89 -0
- package/dist/assets/templates/dashboard-template/dashboard-template.types.ts +1 -0
- package/dist/assets/templates/data-grid-template/data-grid-template.stories.ts +1 -1
- package/dist/assets/templates/data-grid-template/data-grid-template.types.ts +1 -0
- package/dist/assets/utility-classes.md +92 -0
- package/dist/assets/utils/ts/currency/currency.types.ts +1 -0
- package/dist/assets/variables.css +309 -0
- package/dist/main.js +395 -29
- package/package.json +15 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div ref="allColumnsRef" class="b-data-grid-columns" :class="conditionalColumnsClasses" data-testid="data-grid-columns" > <div v-for="(column, index) in columns" :key="column.field" :ref="el => registerColumnsRefs(el, column.field)" :aria-colindex="index + 1" :aria-label="column.label" :aria-sort="getSortingColumnDirection(column)" :class="conditionalColumnClasses(column, index)" :data-testid="`${column.field}-column-header`" :draggable="hasDraggableColumns && !isResizing && !isActionsColumn(column)" class="b-data-grid-columns__column" role="columnheader" :tabindex="column.sortable || hasColumnPanel || column.infoIconTooltip ? '0' : '-1'" :style="{ width: column.autoWidth && isMeasuringColumnWidths ? 'auto' : `${column.width}px` }" v-on="dataGridColumnEvents(column)" > <div v-if="isActionsColumn(column) && selectable" class="b-data-grid-columns__select-column" :class="conditionalSelectColumnClasses" > <bento-checkbox v-if="!selectableAcrossPages" :aria-label="t('selectAll')" :model-value="allItemsSelectedState.allItemsSelected" data-testid="checkbox-select-all" :indeterminate="allItemsSelectedState.isIndeterminate" value="data-grid-select-all" @update:model-value="onAllRowSelect" /> <template v-if="selectableAcrossPages"> <bento-menu :button="{ variant: 'tertiary', condensed: true, 'aria-label': t('selectAllMenu') }" :data="selectAllMenu" class="b-data-grid-columns__select-column-menu" disable-arrow-down-opening-menu menu-fixed-positioning menu-position="bottom-start" > <template #iconLeft> <div class="b-data-grid-columns__select-column-menu-button"> <decorative-checkbox :indeterminate="allItemsSelectedState.isIndeterminate" :checked="allItemsSelectedState.allItemsSelected" :all-selected="isAllSelectionState" class="b-data-grid-columns__select-column-menu-checkbox" data-testid="checkbox-select-all" /> <triangle-down :svg-title="t('selectionMenu')" aria-hidden="true" /> </div> </template> </bento-menu> </template> </div> <template v-else> <div :aria-label="t('columnActions', { name: column.label })" aria-live="polite" role="region" class="b-data-grid-columns__column-header-and-actions" :class="conditionalColumnHeaderAndActionsClasses(column)" > <bento-typography el="div" strongest class="b-data-grid-columns__column-header" :class="conditionalColumnHeaderClasses" > <slot :column="column" :name="`head-${column.field}`"> <span class="b-data-grid-columns__label">{{ column.label }}</span> </slot> </bento-typography> <div v-if="hasColumnActions(column)" class="b-data-grid-columns__column-actions" :class="conditionalColumnActionsClasses(column)" > <div v-if="column.infoIconTooltip" class="b-data-grid-columns__column-info-icon"> <bento-info-icon :tooltip-text="column.infoIconTooltip" /> </div> <div v-if="column.sortable" class="b-data-grid-columns__column-sort"> <bento-toggle-button condensed :toggled="isSorting(column.field)" variant="tertiary" @click="sortByColumnField(column.field)" > <template #iconLeft> <arrow-up-and-down-sort-icon v-if="!getSortingDirection(column.field)" :svg-title="t('sortColumn', { name: column.label })" /> <sort-ascending-icon v-else-if=" getSortingDirection(column.field) === BentoDatagridSortDirection.ASCENDING " :svg-title="t('sortColumnInDescendingOrder', { name: column.label })" /> <sort-descending-icon v-else-if=" getSortingDirection(column.field) === BentoDatagridSortDirection.DESCENDING " :svg-title="t('sortColumnInAscendingOrder', { name: column.label })" /> </template> </bento-toggle-button> </div> <bento-menu v-if="hasColumnPanel" :ref="el => (contextMenuRef[column.field] = el)" class="b-data-grid-columns__menu" :class="conditionalColumnMenuClasses(column)" disable-arrow-down-opening-menu :data="contextualMenuData(column)" :menu-position="computeMenuPosition(hasFirstColumnPanelRightAligned, index)" :menu-fixed-positioning="true" :button="{ variant: 'tertiary', condensed: true }" @open="$emit(DatagridColumnsEvent.COLUMN_MENU_OPEN)" @close="$emit(DatagridColumnsEvent.COLUMN_MENU_CLOSE)" /> </div> </div> <div :data-testid="`data-grid-column-${column.field}`" class="b-data-grid-columns__column-separator-container" :class="conditionalColumnSeparatorClasses(column)" role="presentation" @mousedown="startResize(hasResizableColumns, column.field, $event)" @click.stop > <div v-if="!isFrozenColumns || (isFrozenColumns && index !== columns.length - 1)" class="b-data-grid-columns__column-separator" ></div> </div> </template> </div> <template v-if="rowActionsMaximumWidth && !isFrozenColumns"> <div role="presentation" class="b-data-grid-columns__row-actions-filler"></div> <div :ref="el => registerColumnsRefs(el, DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME)" :aria-label="t('rowActionsColumn')" :aria-colindex="columns.length + 1" class="b-data-grid-columns__column" role="columnheader" tabindex="0" :style="{ minWidth: `${rowActionsMaximumWidth}px` }" ></div> </template> </div> </template> <script lang="ts" setup> import { computed, type ComputedRef, inject, type PropType, type Ref, ref, toRefs } from 'vue'; import { BentoCheckbox } from '@/components/checkbox'; import { type BentoColumn, type BentoColumnWidthOverridesLookup, type BentoDatagridAllItemsSelectedState, BentoDatagridAriaSortValue, type BentoDatagridSortByColumn, BentoDatagridSortDirection, type BentoDataGridVisibleRowsState, } from '../../data-grid.types'; import { BentoTypography } from '@/components/typography'; import { DATAGRID_ACTIONS_COLUMN_FIELD_NAME, DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME, useResizer, useSorter, } from '../../composables'; import { focusOnInteractiveChild } from '../../utils'; import { BentoInfoIcon } from '@/components/info-icon'; import { type BentoDatagridColumnSelectionState, type BentoDatagridDraggingColumnState, DatagridColumnsEvent, type DatagridColumnsHeaderReferences, } from './data-grid-columns.types'; import { BentoMenu } from '@/components/menu'; import { BentoToggleButton } from '@/components/button'; import { DecorativeCheckbox } from '@/internal/decorative-checkbox'; import { useI18n } from '@/utils/ts/i18n'; import type { BentoMenuItem } from '@/components/menu/components'; import type { DatagridColumnHoveringState } from '@/components/data-grid/composables/use-column-hovering/use-column-hovering.types'; import { BentoPopoverPositions } from '@/components/popover/popover.types'; import ArrowUpAndDownSortIcon from '@adyen/ui-assets-icons-16/vue/arrow-up-down-sort'; import ArrowLeftIcon from '@adyen/ui-assets-icons-16/vue/arrow-left'; import ArrowRightIcon from '@adyen/ui-assets-icons-16/vue/arrow-right'; import FreezeIcon from '@adyen/ui-assets-icons-16/vue/freeze'; import HideIcon from '@adyen/ui-assets-icons-16/vue/hide'; import SortAscendingIcon from '@adyen/ui-assets-icons-16/vue/sort-ascending'; import SortDescendingIcon from '@adyen/ui-assets-icons-16/vue/sort-descending'; import ShowIcon from '@adyen/ui-assets-icons-16/vue/show'; import TriangleDown from '@adyen/ui-assets-icons-16/vue/triangle-down'; import messages from './messages.json'; import { DATA_GRID_SMALL_CONTAINER, DATA_GRID_TOUCH_DEVICE } from '@/components/data-grid/data-grid.keys'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * Column definitions using 'key' to relate to data object. */ columns: { type: Array as PropType<Array<BentoColumn>>, required: true, }, /** * Columns being dragged, contains dragging column and a target column */ draggingColumns: { type: Object as PropType<BentoDatagridDraggingColumnState>, default: undefined, }, /** * Determines the state of the current results: { filtered: boolean, allItemsVisible: boolean } to render the correct select all menu options */ visibleRowsState: { type: Object as PropType<BentoDataGridVisibleRowsState>, default: undefined, }, /** * Toggle the columns draggable events. */ hasDraggableColumns: { type: Boolean, default: true, }, /** * If resize is enabled */ hasResizableColumns: { type: Boolean, default: true, }, /** * Signifies if the column panel is enabled. */ hasColumnPanel: { type: Boolean, default: false, }, /** * If set to true, aligns the first column panel to the right of its button. */ hasFirstColumnPanelRightAligned: { type: Boolean, default: false, }, /** * If the data grid has overflow on the left side */ hasLeftOverflow: { type: Boolean, default: false, }, /** * The column hovered over state and whether the hovered over column should show a border right or left. */ hoveredOverColumnState: { type: Object as PropType<DatagridColumnHoveringState>, required: true, }, /** * Toggled to true when the DataGrid has auto width columns and is measuring the */ isMeasuringColumnWidths: { type: Boolean, default: false, }, /** * Used to disable hiding the last visible column. */ isOnlyOneVisibleColumn: { type: Boolean, default: false, }, /** * If these columns are frozen (pinned) columns */ isFrozenColumns: { type: Boolean, default: false, }, /** * Show frozen columns separator */ showFrozenColumnsSeparator: { type: Boolean, default: false, }, /** * The column all items selected state */ allItemsSelectedState: { type: Object as PropType<BentoDatagridAllItemsSelectedState>, default: undefined, }, /** * Determines if the user has enabled row selection */ selectable: { type: Boolean, default: false, }, /** * Adds an extra option when selecting all, to select items in other pages too */ selectableAcrossPages: { type: Boolean, default: false, }, /** * Determines which state of row selection is currently active. Used to display the "Select all" menu options. */ selectionState: { type: String as PropType<BentoDatagridColumnSelectionState>, default: undefined, }, /** * Sortby handles proper rendering of sort icon in header. Sorting currently is only supported server side. * It does make sure the currently sorted column has a more visible sort icon, and the sort icon points in the right direction. */ sortBy: { type: Array as PropType<Array<BentoDatagridSortByColumn>>, // NOTE: is an array to allow multi-sorting in the future default: () => [], }, /** * Truncates all data grid column headers */ truncateColumnHeaders: { type: Boolean, default: false, }, /** * The maximum width of the row actions in all rows */ rowActionsMaximumWidth: { type: Number, default: 0, }, }); const emit = defineEmits<{ /** * Emitted when a user manually resizes a column width. */ ( e: DatagridColumnsEvent.COLUMN_RESIZE, column: { field: string; width: number; } ); /** * Triggered when a column is resized. */ (e: DatagridColumnsEvent.COLUMN_RESIZING, value: BentoColumnWidthOverridesLookup); /** * Triggered when a column is sorted. */ (e: 'sort', updatedSortBy: Array<BentoDatagridSortByColumn>); /** * Triggered when selecting all rows. */ (e: DatagridColumnsEvent.SELECT_ALL_ROWS, areAllItemsSelected: boolean); /** * Triggered when all rows across all pages are selected. */ (e: DatagridColumnsEvent.SELECT_ALL_ACROSS_PAGES); /** * Triggered when all filtered rows are selected. */ (e: DatagridColumnsEvent.SELECT_ALL_CURRENT_RESULTS); /** * Triggered when deselecting all rows. */ (e: DatagridColumnsEvent.DESELECT_ALL); /** * Triggered when all filtered rows are deselected. */ (e: DatagridColumnsEvent.DESELECT_ALL_CURRENT_RESULTS); /** * Triggered when a columns started to be dragged. */ (e: DatagridColumnsEvent.COLUMN_DRAG_START, column: BentoColumn, event: DragEvent & Event); /** * Triggered when a column is being dragged over another DOM element. */ (e: DatagridColumnsEvent.COLUMN_DRAG_ENTER, column: BentoColumn, event: DragEvent & Event); /** * Triggered when a column is being dragged and is dropped over another DOM element. */ (e: DatagridColumnsEvent.COLUMN_DROP, column: BentoColumn); /** * Triggered when a column drag operation has ended. */ (e: DatagridColumnsEvent.COLUMN_DRAG_END); /** * Triggered when a column is being moved by a certain offset. */ (e: DatagridColumnsEvent.COLUMN_MOVE, movingColumn: BentoColumn, offset: number); /** * Triggered when a column is updated. */ (e: DatagridColumnsEvent.COLUMN_UPDATE, column: BentoColumn); /** * Triggered when the column contextual menu is opened. */ (e: DatagridColumnsEvent.COLUMN_MENU_OPEN); /** * Triggered when the column contextual menu is closed. */ (e: DatagridColumnsEvent.COLUMN_MENU_CLOSE); /** * Emitted when a column header is hovered over. */ (e: 'column-hover', columnKey: BentoColumn); }>(); const CLOSING_CONTEXTUAL_MENU_DELAY_MS = 100; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const { columns, sortBy } = toRefs(props); const allColumnsRef = ref<HTMLDivElement>(); const columnHeaderRefs = ref<DatagridColumnsHeaderReferences>({}); const contextMenuRef = ref({}); const hoveredColumnField = ref(); const closingTimeOutId = ref(); // Selecting const onAllRowSelect = (areAllItemsSelected: boolean) => emit(DatagridColumnsEvent.SELECT_ALL_ROWS, areAllItemsSelected); const isActionsColumn = (column: BentoColumn) => column.field === DATAGRID_ACTIONS_COLUMN_FIELD_NAME; const isAllSelectionState = computed(() => props.selectionState === 'all'); // Select all menu const selectAllMenu = computed(() => [ { text: t('selectPage') as string, handler: () => onAllRowSelect(true), disabled: props.selectionState === 'page' || props.selectionState === 'all', }, props.visibleRowsState?.filtered ? { text: t('selectAllCurrentResults') as string, handler: () => emit(DatagridColumnsEvent.SELECT_ALL_CURRENT_RESULTS), addDivider: props.selectionState !== 'none', disabled: props.selectionState === 'all', } : { text: t('selectAll') as string, handler: () => emit(DatagridColumnsEvent.SELECT_ALL_ACROSS_PAGES), addDivider: props.selectionState !== 'none', disabled: props.selectionState === 'all', }, props.selectionState !== 'none' && { text: t('deselectPage') as string, handler: () => onAllRowSelect(false), addDivider: !props.visibleRowsState?.filtered, // otherwise divider shown in deselect all in current results disabled: props.selectionState === 'unselected-page', }, props.visibleRowsState?.filtered && props.selectionState !== 'none' && { text: t('deselectAllCurrentResults') as string, handler: () => emit(DatagridColumnsEvent.DESELECT_ALL_CURRENT_RESULTS), addDivider: true, disabled: props.selectionState === 'unselected-page' && props.visibleRowsState.allRowsVisible, }, props.selectionState !== 'none' && { text: t('deselectAll') as string, handler: () => emit(DatagridColumnsEvent.DESELECT_ALL), }, ].filter(Boolean) ); // Resizing const { startResize, isResizing, columnHoverEventToEmitAfterResizing } = useResizer(columns, emit); // Sorting const { isSorting, getSortingDirection, sortByColumnField } = useSorter(sortBy, columns, emit); const getSortingColumnDirection = (column: BentoColumn): BentoDatagridAriaSortValue => { if (!column.sortable) { return null; } const sortingDirection = props.sortBy.find(({ field }) => field === column.field)?.direction; if (!sortingDirection) { return BentoDatagridAriaSortValue.NONE; } const ariaSortingDirectionMap: { [key in BentoDatagridSortDirection]: BentoDatagridAriaSortValue } = { [BentoDatagridSortDirection.ASCENDING]: BentoDatagridAriaSortValue.ASCENDING, [BentoDatagridSortDirection.DESCENDING]: BentoDatagridAriaSortValue.DESCENDING, }; return ariaSortingDirectionMap[sortingDirection]; }; // Dragging emit events and methods const draggingEventsForColumn = computed(() => (column: BentoColumn) => { if (!props.hasDraggableColumns) { return {}; } return { dragstart: (e: DragEvent & Event) => { emit(DatagridColumnsEvent.COLUMN_DRAG_START, column, e); }, dragover: (e: DragEvent) => { e.preventDefault(); }, dragenter: (e: DragEvent) => { e.preventDefault(); emit(DatagridColumnsEvent.COLUMN_DRAG_ENTER, column, e); }, drop: () => { emit(DatagridColumnsEvent.COLUMN_DROP, column); }, dragend: () => { emit(DatagridColumnsEvent.COLUMN_DRAG_END); }, }; }); // Contextual menu (moving, visiblity and freezing) const contextualMenuData = computed<(col: BentoColumn) => Array<BentoMenuItem>>( () => (column: BentoColumn) => [ { text: t('moveColumnLeft') as string, handler: () => emit(DatagridColumnsEvent.COLUMN_MOVE, column, -1), icon: ArrowLeftIcon, disabled: props.columns[0].field === column.field, }, { text: t('moveColumnRight') as string, handler: () => emit(DatagridColumnsEvent.COLUMN_MOVE, column, 1), icon: ArrowRightIcon, disabled: props.columns[props.columns.length - 1].field === column.field, }, !isSmallContainer.value && { text: (column?.frozen ? t('unfreezeColumn') : t('freezeColumn')) as string, handler: () => emit(DatagridColumnsEvent.COLUMN_UPDATE, { ...column, frozen: !column?.frozen }), icon: FreezeIcon, }, { text: (column?.visible !== false ? t('hideColumn') : t('showColumn')) as string, handler: () => { emit(DatagridColumnsEvent.COLUMN_UPDATE, { ...column, visible: !(column?.visible !== false), }); }, icon: column?.visible !== false ? HideIcon : ShowIcon, disabled: column?.mandatory || (column?.visible !== false && props.isOnlyOneVisibleColumn), }, ].filter(Boolean) ); const showContextualMenu = (column: BentoColumn) => { hoveredColumnField.value = column.field; if (closingTimeOutId.value) { clearTimeout(closingTimeOutId.value); } }; const hideContextualMenu = (column: BentoColumn) => { // We want to delay closing for a couple of seconds so // it doesn't flicker if a user hovers over the icon and then inside the popover. const timeOutId = setTimeout(() => { if (column.field === hoveredColumnField.value) { hoveredColumnField.value = ''; } }, CLOSING_CONTEXTUAL_MENU_DELAY_MS); closingTimeOutId.value = timeOutId; }; /** * Generates events for a Datagrid Column. * @param column - the BentoColumn we are attaching events onto. */ const dataGridColumnEvents = (column: BentoColumn) => { let events: { [eventName: string]: (e: Event) => void } = { mouseenter: () => { showContextualMenu(column); if (!isResizing.value) { emit('column-hover', column); } else { columnHoverEventToEmitAfterResizing.value = column; } }, mouseleave: () => { hideContextualMenu(column); if (!isResizing.value) { emit('column-hover', null); } else { columnHoverEventToEmitAfterResizing.value = null; } }, focus: () => { showContextualMenu(column); if (isActionsColumn(column)) { focusOnInteractiveChild(columnHeaderRefs.value[column.field]); } }, focusout: (e: FocusEvent) => { if (!allColumnsRef.value?.contains(e?.relatedTarget as Node)) { hideContextualMenu(column); } }, }; if (!isActionsColumn(column)) { events = { ...events, ...draggingEventsForColumn.value(column), }; } return events; }; const hasColumnActions = (column: BentoColumn) => column.infoIconTooltip || column.sortable || props.hasColumnPanel; const registerColumnsRefs = (el: unknown, field: string) => { if (columnHeaderRefs.value?.[field] === undefined) { columnHeaderRefs.value = { ...columnHeaderRefs.value, [field]: null }; } if (el !== null) { columnHeaderRefs.value[field] = el as HTMLDivElement; } }; const isSmallContainer: ComputedRef<boolean> = inject( DATA_GRID_SMALL_CONTAINER, computed(() => false) ); const isTouchDevice: Ref<boolean> = inject(DATA_GRID_TOUCH_DEVICE, ref(false)); // Conditional column styles // Conditional styles const conditionalColumnsClasses = computed(() => { const defaultConditionalClasses = { 'b-data-grid-columns--is-measuring': props.isMeasuringColumnWidths, }; return columns.value[0]?.frozen && !isSmallContainer.value ? { ...defaultConditionalClasses, 'b-data-grid-columns--frozen': true, 'b-data-grid-columns--frozen-separator': props.showFrozenColumnsSeparator, 'b-data-grid-columns--overflow-shadow-left': props.hasLeftOverflow, } : { ...defaultConditionalClasses, 'b-data-grid-columns--unfrozen': true, }; }); const conditionalSelectColumnClasses = computed(() => ({ 'b-data-grid-columns__select-column--all': props.selectableAcrossPages, })); const computeMenuPosition = (hasFirstColumnPanelRightAligned, index) => hasFirstColumnPanelRightAligned && index === 0 ? BentoPopoverPositions.BOTTOM_START : BentoPopoverPositions.BOTTOM_END; const conditionalColumnClasses = (column: BentoColumn, columnIndex: number) => ({ 'b-data-grid-columns__column--column-hovered': column.field === props.hoveredOverColumnState.hoveredOverColumnField, 'b-data-grid-columns__column--column-hovered-no-border-left': column.field === props.hoveredOverColumnState.hoveredOverColumnField && props.hoveredOverColumnState.noBorderLeft, 'b-data-grid-columns__column--column-hovered-no-border-right': column.field === props.hoveredOverColumnState.hoveredOverColumnField && props.hoveredOverColumnState.noBorderRight, 'b-data-grid-columns__column--last-frozen-frozen-column': props.isFrozenColumns && columnIndex === columns.value.length - 1, 'b-data-grid-columns__column--sortable': column.sortable, 'b-data-grid-columns__column--padding-left-unset': column?.padding?.left === false, 'b-data-grid-columns__column--padding-right-unset': column?.padding?.right === false, 'b-data-grid-columns__column--numeric': column?.numeric, 'b-data-grid-columns__column--frozen': column?.frozen, 'b-data-grid-columns__column--is-re-ordering': column?.field === props.draggingColumns?.dragging?.field, 'b-data-grid-columns__column--is-re-ordering-target-left': column?.field === props.draggingColumns?.target?.field && props.draggingColumns?.dragToLeft, 'b-data-grid-columns__column--is-re-ordering-target-right': column?.field === props.draggingColumns?.target?.field && !props.draggingColumns?.dragToLeft, 'b-data-grid-columns__column--with-sorting-or-column-panel': column.sortable || column.infoIconTooltip || props.hasColumnPanel || props.selectableAcrossPages, 'b-data-grid-columns--small-container': isSmallContainer.value, }); const conditionalColumnHeaderAndActionsClasses = (column: BentoColumn) => ({ 'b-data-grid-columns__column-header-and-actions--numeric': column?.numeric, }); const conditionalColumnActionsClasses = (column: BentoColumn) => ({ 'b-data-grid-columns__column-actions--numeric': column?.numeric, }); const conditionalColumnSeparatorClasses = (column: BentoColumn) => ({ 'b-data-grid-columns__column-separator-container--is-resizeable': props.hasResizableColumns, 'b-data-grid-columns__column-separator-container--is-offset': props.hoveredOverColumnState.hoveredOverColumnField === column.field && props.hoveredOverColumnState.isSeparatorOffset, }); const conditionalColumnHeaderClasses = computed(() => ({ 'b-data-grid-columns__column-header--truncated': props.truncateColumnHeaders, })); const conditionalColumnMenuClasses = (column: BentoColumn) => ({ 'b-data-grid-columns__menu--hovered': hoveredColumnField.value === column.field || isTouchDevice.value, }); // Exposed refs defineExpose({ columnHeaderRefs, }); </script> <script lang="ts"> /** * A component to display the BentoDataGrid columns * * @example * * export default { * components: { DataGridColumns }, * template: ` * <data-grid-columns * v-if="columnSet?.length" * :key="`data-grid-columns-${id}`" * :columns="columnSet" * :has-resizable-columns="hasResizableColumns" * :sort-by="sortBy" * :all-items-selected-state="allItemsSelectedState" * :is-frozen-columns="isFrozenColumns" * :show-frozen-columns-separator="showFrozenColumnsSeparator" * :dragging-columns="draggingColumns" * :has-draggable-columns="allowColumnDragAndDrop" * :has-column-panel="columnPanel" * :has-left-overflow="hasLeftOverflow" * :has-first-column-panel-right-aligned="index === computedColumnPanelAlignment" * :is-only-one-visible-column="isOnlyOneVisibleColumn" * :selectable="selectable" * :selectable-across-pages="selectableAcrossPagesComputed" * :truncate-column-headers="truncateColumnHeaders" * v-on="draggingEventsForColumns" * @column-move="onMoveColumnByOffset" * @column-update="onConfigSettingsColumnUpdate" * @column-resize="onColumnResized" * @column-resizing="onColumnWidthOverride" * @select-all-rows="onAllRowSelect" * @select-all-across-pages="emit(BentoDatagridEvent.SELECT_ALL_ACROSS_PAGES)" * @sort="onSort" * > * <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> * <slot :name="innerSlot" v-bind="scope" /> * </template> * </data-grid-columns> * ` * } */ export default { i18n: { messages }, name: 'data-grid-columns', }; </script> <style lang="scss" scoped src="./data-grid-columns.scss" />
|
|
1
|
+
<template> <div ref="allColumnsRef" class="b-data-grid-columns" :class="conditionalColumnsClasses" data-testid="data-grid-columns" > <div v-for="(column, index) in columns" :key="column.field" :ref="el => registerColumnsRefs(el, column.field)" :aria-colindex="index + 1" :aria-label="column.label" :aria-sort="getSortingColumnDirection(column)" :class="conditionalColumnClasses(column, index)" :data-testid="`${column.field}-column-header`" :draggable="hasDraggableColumns && !isResizing && !isActionsColumn(column)" class="b-data-grid-columns__column" role="columnheader" :tabindex="column.sortable || hasColumnPanel || column.infoIconTooltip ? '0' : '-1'" :style="{ width: column.autoWidth && isMeasuringColumnWidths ? 'auto' : `${column.width}px` }" v-on="dataGridColumnEvents(column)" > <div v-if="isActionsColumn(column) && selectable" class="b-data-grid-columns__select-column" :class="conditionalSelectColumnClasses" > <bento-checkbox v-if="!selectableAcrossPages" :aria-label="t('selectAll')" :model-value="allItemsSelectedState.allItemsSelected" data-testid="checkbox-select-all" :indeterminate="allItemsSelectedState.isIndeterminate" value="data-grid-select-all" @update:model-value="onAllRowSelect" /> <template v-if="selectableAcrossPages"> <bento-menu :button="{ variant: 'tertiary', condensed: true, 'aria-label': t('selectAllMenu') }" :data="selectAllMenu" class="b-data-grid-columns__select-column-menu" disable-arrow-down-opening-menu menu-fixed-positioning menu-position="bottom-start" > <template #iconLeft> <div class="b-data-grid-columns__select-column-menu-button"> <decorative-checkbox :indeterminate="allItemsSelectedState.isIndeterminate" :checked="allItemsSelectedState.allItemsSelected" :all-selected="isAllSelectionState" class="b-data-grid-columns__select-column-menu-checkbox" data-testid="checkbox-select-all" /> <triangle-down :svg-title="t('selectionMenu')" aria-hidden="true" /> </div> </template> </bento-menu> </template> </div> <template v-else> <div :aria-label="t('columnActions', { name: column.label })" aria-live="polite" role="region" class="b-data-grid-columns__column-header-and-actions" :class="conditionalColumnHeaderAndActionsClasses(column)" > <bento-typography el="div" strongest class="b-data-grid-columns__column-header" :class="conditionalColumnHeaderClasses" > <slot :column="column" :name="`head-${column.field}`"> <span class="b-data-grid-columns__label">{{ column.label }}</span> </slot> </bento-typography> <div v-if="hasColumnActions(column)" class="b-data-grid-columns__column-actions" :class="conditionalColumnActionsClasses(column)" > <div v-if="column.infoIconTooltip" class="b-data-grid-columns__column-info-icon"> <bento-info-icon :tooltip-text="column.infoIconTooltip" /> </div> <div v-if="column.sortable" class="b-data-grid-columns__column-sort"> <bento-toggle-button condensed :toggled="isSorting(column.field)" variant="tertiary" @click="sortByColumnField(column.field)" > <template #iconLeft> <arrow-up-and-down-sort-icon v-if="!getSortingDirection(column.field)" :svg-title="t('sortColumn', { name: column.label })" /> <sort-ascending-icon v-else-if=" getSortingDirection(column.field) === BentoDatagridSortDirection.ASCENDING " :svg-title="t('sortColumnInDescendingOrder', { name: column.label })" /> <sort-descending-icon v-else-if=" getSortingDirection(column.field) === BentoDatagridSortDirection.DESCENDING " :svg-title="t('sortColumnInAscendingOrder', { name: column.label })" /> </template> </bento-toggle-button> </div> <bento-menu v-if="hasColumnPanel" :ref="el => (contextMenuRef[column.field] = el)" class="b-data-grid-columns__menu" :class="conditionalColumnMenuClasses(column)" disable-arrow-down-opening-menu :data="contextualMenuData(column)" :menu-position="computeMenuPosition(hasFirstColumnPanelRightAligned, index)" :menu-fixed-positioning="true" :button="{ variant: 'tertiary', condensed: true }" @open="$emit(DatagridColumnsEvent.COLUMN_MENU_OPEN)" @close="$emit(DatagridColumnsEvent.COLUMN_MENU_CLOSE)" /> </div> </div> <div :data-testid="`data-grid-column-${column.field}`" class="b-data-grid-columns__column-separator-container" :class="conditionalColumnSeparatorClasses(column)" role="presentation" @mousedown="startResize(hasResizableColumns, column.field, $event)" @click.stop > <div v-if="!isFrozenColumns || (isFrozenColumns && index !== columns.length - 1)" class="b-data-grid-columns__column-separator" ></div> </div> </template> </div> <template v-if="rowActionsMaximumWidth && !isFrozenColumns"> <div role="presentation" class="b-data-grid-columns__row-actions-filler"></div> <div :ref="el => registerColumnsRefs(el, DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME)" :aria-label="t('rowActionsColumn')" :aria-colindex="columns.length + 1" class="b-data-grid-columns__column" role="columnheader" :tabindex="areAnyColumnsFocusable ? '0' : '-1'" :style="{ minWidth: `${rowActionsMaximumWidth}px` }" ></div> </template> </div> </template> <script lang="ts" setup> import { computed, type ComputedRef, inject, type PropType, type Ref, ref, toRefs } from 'vue'; import { BentoCheckbox } from '@/components/checkbox'; import { type BentoColumn, type BentoColumnWidthOverridesLookup, type BentoDatagridAllItemsSelectedState, BentoDatagridAriaSortValue, type BentoDatagridSortByColumn, BentoDatagridSortDirection, type BentoDataGridVisibleRowsState, } from '../../data-grid.types'; import { BentoTypography } from '@/components/typography'; import { DATAGRID_ACTIONS_COLUMN_FIELD_NAME, DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME, useResizer, useSorter, } from '../../composables'; import { focusOnInteractiveChild } from '../../utils'; import { BentoInfoIcon } from '@/components/info-icon'; import { type BentoDatagridColumnSelectionState, type BentoDatagridDraggingColumnState, DatagridColumnsEvent, type DatagridColumnsHeaderReferences, } from './data-grid-columns.types'; import { BentoMenu } from '@/components/menu'; import { BentoToggleButton } from '@/components/button'; import { DecorativeCheckbox } from '@/internal/decorative-checkbox'; import { useI18n } from '@/utils/ts/i18n'; import type { BentoMenuItem } from '@/components/menu/components'; import type { DatagridColumnHoveringState } from '@/components/data-grid/composables/use-column-hovering/use-column-hovering.types'; import { BentoPopoverPositions } from '@/components/popover/popover.types'; import ArrowUpAndDownSortIcon from '@adyen/ui-assets-icons-16/vue/arrow-up-down-sort'; import ArrowLeftIcon from '@adyen/ui-assets-icons-16/vue/arrow-left'; import ArrowRightIcon from '@adyen/ui-assets-icons-16/vue/arrow-right'; import FreezeIcon from '@adyen/ui-assets-icons-16/vue/freeze'; import HideIcon from '@adyen/ui-assets-icons-16/vue/hide'; import SortAscendingIcon from '@adyen/ui-assets-icons-16/vue/sort-ascending'; import SortDescendingIcon from '@adyen/ui-assets-icons-16/vue/sort-descending'; import ShowIcon from '@adyen/ui-assets-icons-16/vue/show'; import TriangleDown from '@adyen/ui-assets-icons-16/vue/triangle-down'; import messages from './messages.json'; import { DATA_GRID_SMALL_CONTAINER, DATA_GRID_TOUCH_DEVICE } from '@/components/data-grid/data-grid.keys'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * Column definitions using 'key' to relate to data object. */ columns: { type: Array as PropType<Array<BentoColumn>>, required: true, }, /** * Columns being dragged, contains dragging column and a target column */ draggingColumns: { type: Object as PropType<BentoDatagridDraggingColumnState>, default: undefined, }, /** * Determines the state of the current results: { filtered: boolean, allItemsVisible: boolean } to render the correct select all menu options */ visibleRowsState: { type: Object as PropType<BentoDataGridVisibleRowsState>, default: undefined, }, /** * Toggle the columns draggable events. */ hasDraggableColumns: { type: Boolean, default: true, }, /** * If resize is enabled */ hasResizableColumns: { type: Boolean, default: true, }, /** * Signifies if the column panel is enabled. */ hasColumnPanel: { type: Boolean, default: false, }, /** * If set to true, aligns the first column panel to the right of its button. */ hasFirstColumnPanelRightAligned: { type: Boolean, default: false, }, /** * If the data grid has overflow on the left side */ hasLeftOverflow: { type: Boolean, default: false, }, /** * The column hovered over state and whether the hovered over column should show a border right or left. */ hoveredOverColumnState: { type: Object as PropType<DatagridColumnHoveringState>, required: true, }, /** * Toggled to true when the DataGrid has auto width columns and is measuring the */ isMeasuringColumnWidths: { type: Boolean, default: false, }, /** * Used to disable hiding the last visible column. */ isOnlyOneVisibleColumn: { type: Boolean, default: false, }, /** * If these columns are frozen (pinned) columns */ isFrozenColumns: { type: Boolean, default: false, }, /** * Show frozen columns separator */ showFrozenColumnsSeparator: { type: Boolean, default: false, }, /** * The column all items selected state */ allItemsSelectedState: { type: Object as PropType<BentoDatagridAllItemsSelectedState>, default: undefined, }, /** * Determines if the user has enabled row selection */ selectable: { type: Boolean, default: false, }, /** * Adds an extra option when selecting all, to select items in other pages too */ selectableAcrossPages: { type: Boolean, default: false, }, /** * Determines which state of row selection is currently active. Used to display the "Select all" menu options. */ selectionState: { type: String as PropType<BentoDatagridColumnSelectionState>, default: undefined, }, /** * Sortby handles proper rendering of sort icon in header. Sorting currently is only supported server side. * It does make sure the currently sorted column has a more visible sort icon, and the sort icon points in the right direction. */ sortBy: { type: Array as PropType<Array<BentoDatagridSortByColumn>>, // NOTE: is an array to allow multi-sorting in the future default: () => [], }, /** * Truncates all data grid column headers */ truncateColumnHeaders: { type: Boolean, default: false, }, /** * The maximum width of the row actions in all rows */ rowActionsMaximumWidth: { type: Number, default: 0, }, }); const emit = defineEmits<{ /** * Emitted when a user manually resizes a column width. */ ( e: DatagridColumnsEvent.COLUMN_RESIZE, column: { field: string; width: number; } ); /** * Triggered when a column is resized. */ (e: DatagridColumnsEvent.COLUMN_RESIZING, value: BentoColumnWidthOverridesLookup); /** * Triggered when a column is sorted. */ (e: 'sort', updatedSortBy: Array<BentoDatagridSortByColumn>); /** * Triggered when selecting all rows. */ (e: DatagridColumnsEvent.SELECT_ALL_ROWS, areAllItemsSelected: boolean); /** * Triggered when all rows across all pages are selected. */ (e: DatagridColumnsEvent.SELECT_ALL_ACROSS_PAGES); /** * Triggered when all filtered rows are selected. */ (e: DatagridColumnsEvent.SELECT_ALL_CURRENT_RESULTS); /** * Triggered when deselecting all rows. */ (e: DatagridColumnsEvent.DESELECT_ALL); /** * Triggered when all filtered rows are deselected. */ (e: DatagridColumnsEvent.DESELECT_ALL_CURRENT_RESULTS); /** * Triggered when a columns started to be dragged. */ (e: DatagridColumnsEvent.COLUMN_DRAG_START, column: BentoColumn, event: DragEvent & Event); /** * Triggered when a column is being dragged over another DOM element. */ (e: DatagridColumnsEvent.COLUMN_DRAG_ENTER, column: BentoColumn, event: DragEvent & Event); /** * Triggered when a column is being dragged and is dropped over another DOM element. */ (e: DatagridColumnsEvent.COLUMN_DROP, column: BentoColumn); /** * Triggered when a column drag operation has ended. */ (e: DatagridColumnsEvent.COLUMN_DRAG_END); /** * Triggered when a column is being moved by a certain offset. */ (e: DatagridColumnsEvent.COLUMN_MOVE, movingColumn: BentoColumn, offset: number); /** * Triggered when a column is updated. */ (e: DatagridColumnsEvent.COLUMN_UPDATE, column: BentoColumn); /** * Triggered when the column contextual menu is opened. */ (e: DatagridColumnsEvent.COLUMN_MENU_OPEN); /** * Triggered when the column contextual menu is closed. */ (e: DatagridColumnsEvent.COLUMN_MENU_CLOSE); /** * Emitted when a column header is hovered over. */ (e: 'column-hover', columnKey: BentoColumn); }>(); const CLOSING_CONTEXTUAL_MENU_DELAY_MS = 100; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const { columns, sortBy } = toRefs(props); const allColumnsRef = ref<HTMLDivElement>(); const columnHeaderRefs = ref<DatagridColumnsHeaderReferences>({}); const contextMenuRef = ref({}); const hoveredColumnField = ref(); const closingTimeOutId = ref(); // Focusing const areAnyColumnsFocusable = computed(() => props.columns.some(column => column.sortable || props.hasColumnPanel || column.infoIconTooltip) ); // Selecting const onAllRowSelect = (areAllItemsSelected: boolean) => emit(DatagridColumnsEvent.SELECT_ALL_ROWS, areAllItemsSelected); const isActionsColumn = (column: BentoColumn) => column.field === DATAGRID_ACTIONS_COLUMN_FIELD_NAME; const isAllSelectionState = computed(() => props.selectionState === 'all'); // Select all menu const selectAllMenu = computed(() => [ { text: t('selectPage') as string, handler: () => onAllRowSelect(true), disabled: props.selectionState === 'page' || props.selectionState === 'all', }, props.visibleRowsState?.filtered ? { text: t('selectAllCurrentResults') as string, handler: () => emit(DatagridColumnsEvent.SELECT_ALL_CURRENT_RESULTS), addDivider: props.selectionState !== 'none', disabled: props.selectionState === 'all', } : { text: t('selectAll') as string, handler: () => emit(DatagridColumnsEvent.SELECT_ALL_ACROSS_PAGES), addDivider: props.selectionState !== 'none', disabled: props.selectionState === 'all', }, props.selectionState !== 'none' && { text: t('deselectPage') as string, handler: () => onAllRowSelect(false), addDivider: !props.visibleRowsState?.filtered, // otherwise divider shown in deselect all in current results disabled: props.selectionState === 'unselected-page', }, props.visibleRowsState?.filtered && props.selectionState !== 'none' && { text: t('deselectAllCurrentResults') as string, handler: () => emit(DatagridColumnsEvent.DESELECT_ALL_CURRENT_RESULTS), addDivider: true, disabled: props.selectionState === 'unselected-page' && props.visibleRowsState.allRowsVisible, }, props.selectionState !== 'none' && { text: t('deselectAll') as string, handler: () => emit(DatagridColumnsEvent.DESELECT_ALL), }, ].filter(Boolean) ); // Resizing const { startResize, isResizing, columnHoverEventToEmitAfterResizing } = useResizer(columns, emit); // Sorting const { isSorting, getSortingDirection, sortByColumnField } = useSorter(sortBy, columns, emit); const getSortingColumnDirection = (column: BentoColumn): BentoDatagridAriaSortValue => { if (!column.sortable) { return null; } const sortingDirection = props.sortBy.find(({ field }) => field === column.field)?.direction; if (!sortingDirection) { return BentoDatagridAriaSortValue.NONE; } const ariaSortingDirectionMap: { [key in BentoDatagridSortDirection]: BentoDatagridAriaSortValue } = { [BentoDatagridSortDirection.ASCENDING]: BentoDatagridAriaSortValue.ASCENDING, [BentoDatagridSortDirection.DESCENDING]: BentoDatagridAriaSortValue.DESCENDING, }; return ariaSortingDirectionMap[sortingDirection]; }; // Dragging emit events and methods const draggingEventsForColumn = computed(() => (column: BentoColumn) => { if (!props.hasDraggableColumns) { return {}; } return { dragstart: (e: DragEvent & Event) => { emit(DatagridColumnsEvent.COLUMN_DRAG_START, column, e); }, dragover: (e: DragEvent) => { e.preventDefault(); }, dragenter: (e: DragEvent) => { e.preventDefault(); emit(DatagridColumnsEvent.COLUMN_DRAG_ENTER, column, e); }, drop: () => { emit(DatagridColumnsEvent.COLUMN_DROP, column); }, dragend: () => { emit(DatagridColumnsEvent.COLUMN_DRAG_END); }, }; }); // Contextual menu (moving, visiblity and freezing) const contextualMenuData = computed<(col: BentoColumn) => Array<BentoMenuItem>>( () => (column: BentoColumn) => [ { text: t('moveColumnLeft') as string, handler: () => emit(DatagridColumnsEvent.COLUMN_MOVE, column, -1), icon: ArrowLeftIcon, disabled: props.columns[0].field === column.field, }, { text: t('moveColumnRight') as string, handler: () => emit(DatagridColumnsEvent.COLUMN_MOVE, column, 1), icon: ArrowRightIcon, disabled: props.columns[props.columns.length - 1].field === column.field, }, !isSmallContainer.value && { text: (column?.frozen ? t('unfreezeColumn') : t('freezeColumn')) as string, handler: () => emit(DatagridColumnsEvent.COLUMN_UPDATE, { ...column, frozen: !column?.frozen }), icon: FreezeIcon, }, { text: (column?.visible !== false ? t('hideColumn') : t('showColumn')) as string, handler: () => { emit(DatagridColumnsEvent.COLUMN_UPDATE, { ...column, visible: !(column?.visible !== false), }); }, icon: column?.visible !== false ? HideIcon : ShowIcon, disabled: column?.mandatory || (column?.visible !== false && props.isOnlyOneVisibleColumn), }, ].filter(Boolean) ); const showContextualMenu = (column: BentoColumn) => { hoveredColumnField.value = column.field; if (closingTimeOutId.value) { clearTimeout(closingTimeOutId.value); } }; const hideContextualMenu = (column: BentoColumn) => { // We want to delay closing for a couple of seconds so // it doesn't flicker if a user hovers over the icon and then inside the popover. const timeOutId = setTimeout(() => { if (column.field === hoveredColumnField.value) { hoveredColumnField.value = ''; } }, CLOSING_CONTEXTUAL_MENU_DELAY_MS); closingTimeOutId.value = timeOutId; }; /** * Generates events for a Datagrid Column. * @param column - the BentoColumn we are attaching events onto. */ const dataGridColumnEvents = (column: BentoColumn) => { let events: { [eventName: string]: (e: Event) => void } = { mouseenter: () => { showContextualMenu(column); if (!isResizing.value) { emit('column-hover', column); } else { columnHoverEventToEmitAfterResizing.value = column; } }, mouseleave: () => { hideContextualMenu(column); if (!isResizing.value) { emit('column-hover', null); } else { columnHoverEventToEmitAfterResizing.value = null; } }, focus: () => { showContextualMenu(column); if (isActionsColumn(column)) { focusOnInteractiveChild(columnHeaderRefs.value[column.field]); } }, focusout: (e: FocusEvent) => { if (!allColumnsRef.value?.contains(e?.relatedTarget as Node)) { hideContextualMenu(column); } }, }; if (!isActionsColumn(column)) { events = { ...events, ...draggingEventsForColumn.value(column), }; } return events; }; const hasColumnActions = (column: BentoColumn) => column.infoIconTooltip || column.sortable || props.hasColumnPanel; const registerColumnsRefs = (el: unknown, field: string) => { if (columnHeaderRefs.value?.[field] === undefined) { columnHeaderRefs.value = { ...columnHeaderRefs.value, [field]: null }; } if (el !== null) { columnHeaderRefs.value[field] = el as HTMLDivElement; } }; const isSmallContainer: ComputedRef<boolean> = inject( DATA_GRID_SMALL_CONTAINER, computed(() => false) ); const isTouchDevice: Ref<boolean> = inject(DATA_GRID_TOUCH_DEVICE, ref(false)); // Conditional column styles // Conditional styles const conditionalColumnsClasses = computed(() => { const defaultConditionalClasses = { 'b-data-grid-columns--is-measuring': props.isMeasuringColumnWidths, }; return columns.value[0]?.frozen && !isSmallContainer.value ? { ...defaultConditionalClasses, 'b-data-grid-columns--frozen': true, 'b-data-grid-columns--frozen-separator': props.showFrozenColumnsSeparator, 'b-data-grid-columns--overflow-shadow-left': props.hasLeftOverflow, } : { ...defaultConditionalClasses, 'b-data-grid-columns--unfrozen': true, }; }); const conditionalSelectColumnClasses = computed(() => ({ 'b-data-grid-columns__select-column--all': props.selectableAcrossPages, })); const computeMenuPosition = (hasFirstColumnPanelRightAligned, index) => hasFirstColumnPanelRightAligned && index === 0 ? BentoPopoverPositions.BOTTOM_START : BentoPopoverPositions.BOTTOM_END; const conditionalColumnClasses = (column: BentoColumn, columnIndex: number) => ({ 'b-data-grid-columns__column--column-hovered': column.field === props.hoveredOverColumnState.hoveredOverColumnField, 'b-data-grid-columns__column--column-hovered-no-border-left': column.field === props.hoveredOverColumnState.hoveredOverColumnField && props.hoveredOverColumnState.noBorderLeft, 'b-data-grid-columns__column--column-hovered-no-border-right': column.field === props.hoveredOverColumnState.hoveredOverColumnField && props.hoveredOverColumnState.noBorderRight, 'b-data-grid-columns__column--last-frozen-frozen-column': props.isFrozenColumns && columnIndex === columns.value.length - 1, 'b-data-grid-columns__column--sortable': column.sortable, 'b-data-grid-columns__column--padding-left-unset': column?.padding?.left === false, 'b-data-grid-columns__column--padding-right-unset': column?.padding?.right === false, 'b-data-grid-columns__column--numeric': column?.numeric, 'b-data-grid-columns__column--frozen': column?.frozen, 'b-data-grid-columns__column--is-re-ordering': column?.field === props.draggingColumns?.dragging?.field, 'b-data-grid-columns__column--is-re-ordering-target-left': column?.field === props.draggingColumns?.target?.field && props.draggingColumns?.dragToLeft, 'b-data-grid-columns__column--is-re-ordering-target-right': column?.field === props.draggingColumns?.target?.field && !props.draggingColumns?.dragToLeft, 'b-data-grid-columns__column--with-sorting-or-column-panel': column.sortable || column.infoIconTooltip || props.hasColumnPanel || props.selectableAcrossPages, 'b-data-grid-columns--small-container': isSmallContainer.value, }); const conditionalColumnHeaderAndActionsClasses = (column: BentoColumn) => ({ 'b-data-grid-columns__column-header-and-actions--numeric': column?.numeric, }); const conditionalColumnActionsClasses = (column: BentoColumn) => ({ 'b-data-grid-columns__column-actions--numeric': column?.numeric, }); const conditionalColumnSeparatorClasses = (column: BentoColumn) => ({ 'b-data-grid-columns__column-separator-container--is-resizeable': props.hasResizableColumns, 'b-data-grid-columns__column-separator-container--is-offset': props.hoveredOverColumnState.hoveredOverColumnField === column.field && props.hoveredOverColumnState.isSeparatorOffset, }); const conditionalColumnHeaderClasses = computed(() => ({ 'b-data-grid-columns__column-header--truncated': props.truncateColumnHeaders, })); const conditionalColumnMenuClasses = (column: BentoColumn) => ({ 'b-data-grid-columns__menu--hovered': hoveredColumnField.value === column.field || isTouchDevice.value, }); // Exposed refs defineExpose({ columnHeaderRefs, }); </script> <script lang="ts"> /** * A component to display the BentoDataGrid columns * * @example * * export default { * components: { DataGridColumns }, * template: ` * <data-grid-columns * v-if="columnSet?.length" * :key="`data-grid-columns-${id}`" * :columns="columnSet" * :has-resizable-columns="hasResizableColumns" * :sort-by="sortBy" * :all-items-selected-state="allItemsSelectedState" * :is-frozen-columns="isFrozenColumns" * :show-frozen-columns-separator="showFrozenColumnsSeparator" * :dragging-columns="draggingColumns" * :has-draggable-columns="allowColumnDragAndDrop" * :has-column-panel="columnPanel" * :has-left-overflow="hasLeftOverflow" * :has-first-column-panel-right-aligned="index === computedColumnPanelAlignment" * :is-only-one-visible-column="isOnlyOneVisibleColumn" * :selectable="selectable" * :selectable-across-pages="selectableAcrossPagesComputed" * :truncate-column-headers="truncateColumnHeaders" * v-on="draggingEventsForColumns" * @column-move="onMoveColumnByOffset" * @column-update="onConfigSettingsColumnUpdate" * @column-resize="onColumnResized" * @column-resizing="onColumnWidthOverride" * @select-all-rows="onAllRowSelect" * @select-all-across-pages="emit(BentoDatagridEvent.SELECT_ALL_ACROSS_PAGES)" * @sort="onSort" * > * <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> * <slot :name="innerSlot" v-bind="scope" /> * </template> * </data-grid-columns> * ` * } */ export default { i18n: { messages }, name: 'data-grid-columns', }; </script> <style lang="scss" scoped src="./data-grid-columns.scss" />
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/** * All the emit events provided by the DatagridConfigSettings component */ export enum DatagridConfigSettingsEvent { UPDATE_CONDENSED = 'update:condensed', UPDATE_COLUMNS = 'update:columns', UPDATE_ZEBRA_ROWS = 'update:zebra-rows', }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type BentoDatagridDataItemId = string | number; export interface BentoDatagridDataItem { id?: BentoDatagridDataItemId; children?: Array<BentoDatagridDataItem>; [fieldName: string]: unknown; }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div class="b-data-grid-row-container" role="presentation"> <div v-bind="ariaRowAttributes" :id="rowId?.toString()" :key="rowIndex" class="b-data-grid-row-container__row" :class="conditionalRowClasses(rowData, rowIndex)" role="row" :aria-selected="getRowAriaSelectedValue(rowData)" @click="onRowClick" @keyup.enter="onRowClick" @keyup.space="onRowClick" > <template v-for="({ columnSet, id, isFrozenColumns }, columnSetIndex) of columnSets"> <div v-if="columnSet?.length" :key="`data-grid-rows-${id}`" class="b-data-grid-row-container__cells" :class="conditionalRowSetClasses(isFrozenColumns, isSmallContainer)" :data-testid="`data-grid-${id}-rows`" > <data-grid-cell v-for="(column, colIndex) in columnSet" :key="`cell-${column.field}`" :ref="el => registerRowCellRef(el, column.field)" class="b-data-grid-row-container__cell" :column="column" :column-set-index="columnSetIndex" :col-index="colIndex" :column-sets="columnSets" :condensed="condensed" :dragging-columns="draggingColumns" :dragging-events-for-row-cell="draggingEventsForRowCell" :get-data-item-id="getDataItemId" :has-zebra-background="isRowEven" :is-highlighted="isHighlightedRow(rowData)" :is-selected="isSelectedRow(rowData)" :hovered-over-column-state="hoveredOverColumnState" :is-data-item-disabled="isDataItemDisabled" :is-measuring-column-widths="isMeasuringColumnWidths" :is-nested-content-expanded="isNestedContentShown" :nested-content="shouldShowNestedContentToggle" :nested-rows-element-ids="allNestedRowIds" :row-data="rowData" :row-index="rowIndex" :selection-is-indeterminate="hasUnSelectedNestedRows" :selectable="selectable" :selected-items="selectedItems" @cell-hovered="emit('cell-hovered', $event)" @cell-focus="emit('cell-focus', $event)" @on-frozen-columns-key-press="emit('on-frozen-columns-key-press', $event)" @select-row="onRowSelect" @nested-content-toggled="nestedContentToggled" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-cell> </div> <div v-if="!isFrozenColumns && rowActions" :key="`data-grid-row-actions-${id}`" ref="rowActionsRef" :aria-colindex="sumColumnAmountsInColumnSets().toString()" class="b-data-grid-row-container__row-actions-container" :class="conditionalRowActionsContainerClasses" :style="computedRowActionsStyles" :data-cellindex="getRowActionsCellIndex()" tabindex="-1" role="gridcell" @focus="onRowActionsCellFocus" @focusOut="onRowActionsCellFocusOut" @keydown="onRowActionsCellOnKeyDown" > <data-grid-cell-actions class="b-data-grid-row-container__row-actions" disable-arrow-down-opening-menu :cell-actions="rowActions" :has-right-overflow="hasRightOverflow" :row-data-item="rowData" teleport-menu @menu-open="onRowActionMenuOpen" @menu-close="onRowActionMenuClose" /> </div> </template> </div> <Transition v-if=" (hasNestedContent && hasSlot('nested-slot') && shouldShowNestedContent(rowData)) || (hasNestedRows && shouldShowNestedRowsBooleanResult) " :key="`${rowIndex}-nest-content`" name="b-data-grid-row-container__animation--nested-content" > <div v-if="isNestedContentShown && shouldShowRenderNestedRows" :class="conditionalNestedRowClasses" data-testid="nested-content-container" :style="nestedContentStyles" role="presentation" > <div ref="nestedContentRef"> <div v-if="isLoadingNestedRows" class="b-data-grid-row-container__row-nested-rows-loading-container" > <bento-loading-indicator /> </div> <data-grid-row v-for="(childRowData, childRowIndex) in rowData.children" v-bind="props" :id="getRowId(childRowIndex)" :key="`child-${childRowIndex}`" :row-data="childRowData" :row-index="childRowIndex" :row-set-size="rowData.children.length" is-nested-row @cell-hovered="emit('cell-hovered', $event)" @cell-focus="emit('cell-focus', $event)" @row-click="emitRowClick" @on-frozen-columns-key-press="emit('on-frozen-columns-key-press', $event)" @select-row="onNestedRowSelect(childRowData, $event)" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-row> </div> </div> <div v-else-if="isNestedContentShown" v-bind="nestedContentContainerDivAriaAttributes" :class="conditionalNestedRowClasses" data-testid="nested-content-container" :style="nestedContentStyles" role="row" tabindex="-1" > <div ref="nestedContentRef" role="gridcell"> <div v-if="isLoadingNestedRows" class="b-data-grid-row-container__row-nested-rows-loading-container" > <bento-loading-indicator /> </div> <div v-else-if="hasSlot('nested-slot') && shouldShowNestedContent(rowData)"> <slot :item="rowData" name="nested-slot" /> </div> </div> </div> </Transition> </div> </template> <script setup lang="ts"> import { type ComponentPublicInstance, computed, type ComputedRef, inject, onMounted, type PropType, ref, useSlots, watch, } from 'vue'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { focusOnInteractiveChild, makeAllInteractiveChildrenFocusable, makeAllInteractiveChildrenUnFocusable, } from '@/components/data-grid/utils'; import DataGridRow from '@/components/data-grid/components/data-grid-row/data-grid-row.vue'; import DataGridCell from '@/components/data-grid/components/data-grid-cell/data-grid-cell.vue'; import DataGridCellActions from '@/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.vue'; // Types and enums import { type BentoColumn, type BentoDatagridDataItem, type BentoDatagridDataItemId, type BentoDatagridGetDataItemIdFn, type BentoDatagridIsDataItemDisabledFn, type BentoDatagridShouldShowNestedContentFn, type BentoDatagridShouldShowNestedRowsFn, BentoDatagridStyleState, } from '@/components/data-grid/data-grid.types'; import type { DataGridCellColumnSet, DataGridCellDragEventsForColumn, } from '../data-grid-cell/data-grid-cell.types'; import type { BentoDatagridDraggingColumnState } from '../data-grid-columns/data-grid-columns.types'; import type { DatagridColumnHoveringState } from '@/components/data-grid/composables/use-column-hovering'; // Translations import { type Booleanish } from '@/types/prop-types'; import { useHasSlot } from '@/composables/use-has-slot/use-has-slot'; import { useExpandableContentHeight } from '@/composables/use-expandable-content-height/use-expandable-content-height'; import { observeSizeOfElement } from '@/utils/ts/resize'; import { DATAGRID_ACTIONS_COLUMN_FIELD_NAME, DATAGRID_CELL_OFFSET_PADDING, } from '@/components/data-grid/composables/use-calculate-column-width/use-calculate-column-width'; import { generateUid } from '@/core/utils/ts'; import { type BentoDataGridRowActionsProp } from '@/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.types'; import type { HTMLAttributes } from 'vue/types/jsx'; import { EventKey } from '@/types/utils-events'; import { DATA_GRID_SMALL_CONTAINER } from '@/components/data-grid/data-grid.keys'; const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = defineProps({ /** * Row actions property that generates which button to show in row actions area */ rowActions: { type: [Function, Array] as PropType<BentoDataGridRowActionsProp>, default: undefined, }, /** * When true, will emit a 'rowClick' event when a row is clicked. Will add cursor: pointer to rows. */ allowRowClicks: { type: Boolean, default: false }, /** * The columns sets including */ columnSets: { type: Array as PropType<Array<DataGridCellColumnSet>>, required: true, }, /** * Sets the row cells to the condensed padding state. */ condensed: { type: Boolean, default: false, }, /** * Columns being dragged, contains dragging column and a target column */ draggingColumns: { type: Object as PropType<BentoDatagridDraggingColumnState>, default: undefined, }, /** * Dragging event handlers for each row cell */ draggingEventsForRowCell: { type: Function as PropType<DataGridCellDragEventsForColumn>, required: true, }, /** * A customisable method to retrieve a unique identifier for a data item in the array of data supplied. * This is used selecting and performance reasons and must exist in each data item. */ getDataItemId: { type: Function as PropType<BentoDatagridGetDataItemIdFn>, default: (item: BentoDatagridDataItem) => item.id, }, /** * Set to true if the datagrid has been scrolled and the left side has overflowing space. */ hasLeftOverflow: { type: Boolean, default: false, }, /** * Enables the nested content slot functionality. */ hasNestedContent: { type: Boolean, default: false, }, /** * Enables the nested content slot functionality. */ hasNestedRows: { type: Boolean, default: false, }, /** * Set to true if the datagrid has been scrolled and the right side has overflowing space. */ hasRightOverflow: { type: Boolean, default: false, }, /** * Highlighted row IDs. */ highlightedRows: { type: Array as PropType<Array<BentoDatagridDataItemId>>, default: () => [], }, /** * The row index of a currently hovered row. */ hoveredRowIndex: { type: Number, default: -1, }, /** * The column hovered over state and whether the hovered over column should show a border right or left. */ hoveredOverColumnState: { type: Object as PropType<DatagridColumnHoveringState>, required: true, }, /** * A customisable method to determine if a row is not selectable. * The default is that every row is selectable. */ isDataItemDisabled: { type: Function as PropType<BentoDatagridIsDataItemDisabledFn>, default: () => false, }, /** * If the row is of an even number. * Rows are 0th based index e.g. index 0 is an odd-numbered row. */ isRowEven: { type: Boolean, default: false, }, /** * If the row is the last child in it's parent container. */ isRowLastChild: { type: Boolean, default: false, }, /** * If columns are being measured for the autoWidth functionality. */ isMeasuringColumnWidths: { type: Boolean, default: false, }, /** * Determines if this row is nested inside another row. */ isNestedRow: { type: Boolean, default: false, }, /** * The maximum width of the row actions in all rows */ rowActionsMaximumWidth: { type: Number, default: 0, }, /** * The row data object */ rowData: { type: Object as PropType<BentoDatagridDataItem>, required: true, }, /** * The row index. */ rowIndex: { type: Number, required: true, }, /** * The row ID to be using for the ID attribute. */ rowId: { type: Number, default: undefined, }, /** * The number of rows that this row is within. */ rowSetSize: { type: Number, default: 0, }, /** * Determines whether to render frozen seperator on the row. */ showFrozenColumnsSeparator: { type: Boolean, default: false, }, /** * Determines if the row selection is enabled. */ selectable: { type: Boolean, default: false, }, /** * The array of IDs for the selected rows. */ selectedItems: { type: Array as PropType<Array<BentoDatagridDataItemId>>, required: true, }, /** * Enables / Disables the nested-content slot */ shouldShowNestedContent: { type: Function as PropType<BentoDatagridShouldShowNestedContentFn>, default: (_item: BentoDatagridDataItem) => true, }, /** * Returns the nested rows for a given BentoDatagridDataItem. */ shouldShowNestedRows: { type: Function as PropType<BentoDatagridShouldShowNestedRowsFn>, default: (_item: BentoDatagridDataItem) => true, }, }); const emit = defineEmits<{ /** * Emitted when a cell has been hovered over. This is used to keep the row in a hovered state when hovering over a menu. */ (e: 'cell-hovered', value: number); /** * Emitted when an a frozen column cell fires a key up or down event. */ (e: 'on-frozen-columns-key-press', event: Event); /** * Emitted when a row has been selected. */ (e: 'select-row', value: Array<BentoDatagridDataItemId>, rowData: BentoDatagridDataItem); /** * Emitted when a nested row has been selected. */ ( e: 'select-nested-row', value: Array<BentoDatagridDataItemId>, parentRowData: BentoDatagridDataItem, rowData: BentoDatagridDataItem ); /** * Emitted when a row has been clicked. */ (e: 'row-click', rowData: BentoDatagridDataItem, event: MouseEvent | KeyboardEvent); /** * Emitted when a row with nested content is toggled */ (e: 'nested-content-toggled', rowData: BentoDatagridDataItem); /** * Emitted when a row cell is focused. */ ( e: 'cell-focus', value: { rowData: BentoDatagridDataItem; column: BentoColumn; rowIndex: number; colIndex: number } ); }>(); // Refs const gridcellRefs = ref<{ [key: string]: HTMLDivElement }>({}); const isNestedContentShown = ref(false); const nestedContentRef = ref(); const rowActionsRef = ref(); const nestedContentSlotId = generateUid('nested-content-slot'); const isRowActionMenuOpen = ref(false); const focusableItems = ref<Array<{ node: HTMLElement }>>(); onMounted(() => { if (rowActionsRef.value) { focusableItems.value = makeAllInteractiveChildrenUnFocusable(rowActionsRef.value); } }); const shouldShowNestedRowsBooleanResult = computed(() => props.shouldShowNestedRows(props.rowData)); const shouldShowRenderNestedRows = computed( () => !props.isNestedRow && props.hasNestedRows && shouldShowNestedRowsBooleanResult.value && props.rowData?.children?.length > 0 ); const isLoadingNestedRows = computed( () => !props.isNestedRow && props.hasNestedRows && shouldShowNestedRowsBooleanResult.value && (props.rowData?.children === undefined || props?.rowData?.children.length === 0) ); const shouldShowNestedContentToggle = computed( () => (props.hasNestedContent && props.shouldShowNestedContent(props.rowData)) || (props.hasNestedRows && shouldShowNestedRowsBooleanResult.value && !props.isNestedRow) ); const ariaRowAttributes = computed<HTMLAttributes>(() => { if (props.hasNestedContent || props.hasNestedRows) { const isNestedContentShownBooleanishVal = isNestedContentShown.value.toString() as Booleanish; return { 'aria-expanded': shouldShowNestedContentToggle.value ? isNestedContentShownBooleanishVal : undefined, 'aria-level': props.isNestedRow ? '2' : '1', 'aria-posinset': props.rowIndex + 1, 'aria-setsize': props.rowSetSize, }; } return { 'aria-rowindex': props.rowIndex + 1 }; }); const nestedContentContainerDivAriaAttributes = computed<HTMLAttributes>(() => { if (isLoadingNestedRows.value) { return { 'aria-busy': 'true', 'aria-live': 'polite', }; } if ( shouldShowRenderNestedRows.value || !props.hasNestedContent || !props.shouldShowNestedContent(props.rowData) ) { return undefined; } return { 'aria-level': props.isNestedRow ? '3' : '2', 'aria-posinset': '1', 'aria-setsize': '1', id: nestedContentSlotId, role: 'row', tabindex: '-1', }; }); const nestedRowIds = computed(() => { if (shouldShowRenderNestedRows.value) { return props.rowData?.children?.reduce((acc, _val, index) => { acc[index] = generateUid('nested-row'); return acc; }, {}); } return []; }); const allNestedRowIds = computed(() => { if (!isNestedContentShown.value) { return undefined; } if (props.hasNestedContent && props.shouldShowNestedContent(props.rowData)) { return [nestedContentSlotId]; } if (shouldShowRenderNestedRows.value && Object.keys(nestedRowIds.value)?.length > 0) { const generatedRowIds = Object.values(nestedRowIds.value); if (generatedRowIds?.length > 0) { return generatedRowIds; } } return []; }); const hasUnSelectedNestedRows = computed(() => { const childrendDataItemIds = props.rowData?.children?.map(nestedItem => props.getDataItemId(nestedItem)); if (childrendDataItemIds?.length > 0) { const everyChildIsSelected = childrendDataItemIds.every(childId => props?.selectedItems?.includes(childId)); const someItemsAreSelected = childrendDataItemIds.some(childId => props?.selectedItems?.includes(childId)); return !everyChildIsSelected && someItemsAreSelected; } return false; }); // Methods const registerRowCellRef = (el: Element | ComponentPublicInstance, field: string) => { // We always know this will be used on a DataGridCell component so we can cast it const gridCellComponent = el as InstanceType<typeof DataGridCell>; if (el !== null && gridCellComponent?.gridcellRef) { gridcellRefs.value[field] = gridCellComponent.gridcellRef as HTMLDivElement; } }; const getRowId = (childRowIndex: number) => { return nestedRowIds.value[childRowIndex]; }; const getRowAriaSelectedValue = (row: BentoDatagridDataItem): Booleanish => { if (!props.selectable) { return null; } const id = props.getDataItemId(row); return props.selectedItems.includes(id).toString() as Booleanish; }; const emitRowClick = (rowData: BentoDatagridDataItem, event: MouseEvent | KeyboardEvent) => { emit('row-click', rowData, event); }; /** * Handles row clicks when the `allowRowClicks` prop is true. * * This method also prevents the `row-click` event from firing if the click * originated from an interactive element (like a button, input, or link) * within the row. This is to avoid unintended behavior when a user * interacts with controls inside a row. * * @param event The mouse or keyboard event from the click or key press. */ const onRowClick = (event: MouseEvent | KeyboardEvent) => { // Only proceed if allowRowClicks is true if (!props.allowRowClicks) { return; } let isEventTargetInteractiveElement = false; const dataGridRowInteractiveTypes = ['button', 'input', 'a']; for (let i = 0; i < dataGridRowInteractiveTypes.length; i++) { if ((event?.target as HTMLElement)?.closest(dataGridRowInteractiveTypes[i])) { isEventTargetInteractiveElement = true; break; } } // Only trigger if element is NOT interactive if (!isEventTargetInteractiveElement) { emitRowClick(props.rowData, event); } }; const onRowSelect = (value: Array<BentoDatagridDataItemId>) => { emit('select-row', value, props.rowData); }; const onNestedRowSelect = (rowData: BentoDatagridDataItem, value: Array<BentoDatagridDataItemId>) => { emit('select-nested-row', value, props.rowData, rowData); }; /** * Sets the state to indicate that a row action menu is open. This is used to apply a higher * z-index to the row's action container, ensuring the menu displays correctly above * subsequent rows. */ const onRowActionMenuOpen = () => { isRowActionMenuOpen.value = true; }; /** * Resets the state when a row action menu is closed, removing the elevated z-index from the * row's action container. */ const onRowActionMenuClose = () => { isRowActionMenuOpen.value = false; }; /** * When the row actions cell is focused, make the first interactive child focusable and focus it. * This allows for keyboard navigation into the cell's actions. */ const onRowActionsCellFocus = () => { makeAllInteractiveChildrenFocusable([focusableItems.value?.[0]]); focusOnInteractiveChild(rowActionsRef.value); }; /** * When focus leaves the row actions cell, make all interactive children unfocusable. * This ensures that tabbing through the grid skips over the individual actions within the cell. */ const onRowActionsCellFocusOut = () => { makeAllInteractiveChildrenUnFocusable(rowActionsRef.value); }; /** * Handles keyboard navigation within the row actions cell. * Allows using left and right arrow keys to move between interactive elements (e.g., buttons) in the cell. * @param e The keyboard event. */ const onRowActionsCellOnKeyDown = (e: KeyboardEvent) => { if (e.key !== EventKey.TAB) { makeAllInteractiveChildrenUnFocusable(rowActionsRef.value); } if ( e.key === EventKey.ARROW_RIGHT && focusableItems.value?.length > 1 && focusableItems.value[0].node === document.activeElement ) { makeAllInteractiveChildrenUnFocusable(rowActionsRef.value); focusableItems.value[1].node.setAttribute('tabindex', '0'); focusableItems.value[1].node.focus(); e.stopPropagation(); e.preventDefault(); } else if ( e.key === EventKey.ARROW_LEFT && focusableItems.value?.length > 1 && focusableItems.value[1].node === document.activeElement ) { makeAllInteractiveChildrenUnFocusable(rowActionsRef.value); focusableItems.value[0].node.setAttribute('tabindex', '0'); focusableItems.value[0].node.focus(); e.stopPropagation(); e.preventDefault(); } }; // Cell and Columns index calculation const sumColumnAmountsInColumnSets = () => props.columnSets.reduce((total, { columnSet }) => total + columnSet.length, 0); const getRowActionsCellIndex = () => sumColumnAmountsInColumnSets() * props.rowIndex + sumColumnAmountsInColumnSets(); // Nested content expanding logic const { updateContentHeight, contentHeight } = useExpandableContentHeight(nestedContentRef, isNestedContentShown); const nestedContentStyles = computed(() => { if (props.hasNestedContent || props.hasNestedRows) { // calculate the padding by getting the action column width and the first column left padding: const paddingLeft = shouldShowRenderNestedRows.value || isLoadingNestedRows.value ? 0 : gridcellRefs.value[DATAGRID_ACTIONS_COLUMN_FIELD_NAME].offsetWidth + DATAGRID_CELL_OFFSET_PADDING; return { paddingLeft: `${paddingLeft}px`, 'max-height': (props.hasNestedContent || props.hasNestedRows) && contentHeight.value && !contentHeight?.value?.includes('null') ? contentHeight.value : 0, }; } return undefined; }); watch( () => nestedContentRef.value, () => { let observer: ResizeObserver | null = null; if (!observer && nestedContentRef.value) { observer = observeSizeOfElement(nestedContentRef.value, () => { if (nestedContentRef.value) { updateContentHeight(); } }); } } ); const nestedContentToggled = () => { isNestedContentShown.value = !isNestedContentShown.value; updateContentHeight(); emit('nested-content-toggled', props.rowData); }; watch( () => props.rowData?.children, (_newChildren, oldChildren) => { if (props?.rowData?.children?.length > 0 && oldChildren === undefined && !isNestedContentShown.value) { updateContentHeight(); } } ); const isSmallContainer: ComputedRef<boolean> = inject( DATA_GRID_SMALL_CONTAINER, computed(() => false) ); const isHighlightedRow = row => props.highlightedRows.includes(props.getDataItemId(row)); const isSelectedRow = row => props.selectedItems.includes(props.getDataItemId(row)); // Styling const conditionalRowClasses = (row: BentoDatagridDataItem, rowIndex: number) => ({ 'b-data-grid-row-container__row--disabled': props.isDataItemDisabled(row), 'b-data-grid-row-container__row--row-clicks-enabled': props.allowRowClicks, 'b-data-grid-row-container__row--highlighted': isHighlightedRow(row), 'b-data-grid-row-container__row--selected': isSelectedRow(row), 'b-data-grid-row-container__row--hovered': props.hoveredRowIndex === rowIndex, 'b-data-grid-row-container__row--is-last-child': props.isRowLastChild && !isNestedContentShown.value, 'b-data-grid-row-container__row--nested-row': props.isNestedRow, 'b-data-grid-row-container__row--zebra': props.isRowEven, }); const conditionalRowSetClasses = (isFrozenRowSet: boolean, isSmallContainer: boolean) => ({ 'b-data-grid-row-container__row-cell-set--frozen': isFrozenRowSet && !isSmallContainer, [`b-data-grid-row-container__row-cell-set--${BentoDatagridStyleState.OVERFLOW_SHADOW_LEFT}`]: !isSmallContainer && isFrozenRowSet && props.hasLeftOverflow, 'b-data-grid-row-container__row-cell-set--frozen-separator': !isSmallContainer && isFrozenRowSet && props.showFrozenColumnsSeparator, 'b-data-grid-row-container__row-cell-set--unfrozen': !isFrozenRowSet || isSmallContainer, 'b-data-grid-row-container__row-cell-set--is-measuring': props.isMeasuringColumnWidths, }); const conditionalNestedRowClasses = computed(() => ({ 'b-data-grid-row-container__row-nested-slot--is-last-child': props.isRowLastChild, 'b-data-grid-row-container__row-nested-slot': !shouldShowRenderNestedRows.value && props.shouldShowNestedContent(props.rowData), })); const conditionalRowActionsContainerClasses = computed(() => ({ 'b-data-grid-row-container__row-actions-container--row-actions-menu-open': isRowActionMenuOpen.value, })); const computedRowActionsStyles = computed(() => ({ minWidth: `${props.rowActionsMaximumWidth}px`, })); // Exposed refs defineExpose({ gridcellRefs, rowActionsRef, }); </script> <script lang="ts"> /** * Internal component to display the BentoDataGrid data rows containig the data row cells. * * @example * * export default { * components: { DataGridRow }, * template: ` * <data-grid-row * :ref="el => registerRowCellRefs(el, rowIndex)" * :style="gridColumnWidthStyle" * :allow-row-clicks="allowRowClicks" * :column-sets="columnSets" * :condensed="condensed" * :dragging-columns="draggingColumns" * :dragging-events-for-row-cell="draggingEventsForRowCell" * :get-data-item-id="getDataItemId" * :has-left-overflow="hasLeftOverflow" * :highlighted-rows="highlightedRows" * :hovered-row-index="hoveredRowIndex" * :hovered-over-column-state="hoveredOverColumnState" * :is-data-item-disabled="isDataItemDisabled" * :is-measuring-column-widths="isMeasuringColumnWidths" * :row-data="rowData" * :row-index="rowIndex" * :show-frozen-columns-separator="showFrozenColumnsSeparator" * :selectable="selectable" * :selected-items="selectedItems" * @cell-hovered="onCellHover" * @row-click="onRowClick" * @on-frozen-columns-key-press="updateScrollPositionOnFrozenColumnsNavigation" * @select-row="onRowSelect" * > * <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> * <slot :name="innerSlot" v-bind="scope" /> * </template> * </data-grid-row> * ` */ export default { name: 'data-grid-row', }; </script> <style lang="scss" scoped src="./data-grid-row.scss" />
|
|
1
|
+
<template> <div class="b-data-grid-row-container" role="presentation"> <div v-bind="ariaRowAttributes" :id="rowId?.toString()" :key="rowIndex" class="b-data-grid-row-container__row" :class="conditionalRowClasses(rowData, rowIndex)" role="row" :aria-selected="getRowAriaSelectedValue(rowData)" @click="onRowClick" @keyup.enter="onRowClick" @keyup.space="onRowClick" > <template v-for="({ columnSet, id, isFrozenColumns }, columnSetIndex) of columnSets"> <div v-if="columnSet?.length" :key="`data-grid-rows-${id}`" class="b-data-grid-row-container__cells" :class="conditionalRowSetClasses(isFrozenColumns, isSmallContainer)" :data-testid="`data-grid-${id}-rows`" > <data-grid-cell v-for="(column, colIndex) in columnSet" :key="`cell-${column.field}`" :ref="el => registerRowCellRef(el, column.field)" class="b-data-grid-row-container__cell" :column="column" :column-set-index="columnSetIndex" :col-index="colIndex" :column-sets="columnSets" :condensed="condensed" :dragging-columns="draggingColumns" :dragging-events-for-row-cell="draggingEventsForRowCell" :get-data-item-id="getDataItemId" :has-zebra-background="isRowEven" :is-highlighted="isHighlightedRow(rowData)" :is-selected="isSelectedRow(rowData)" :hovered-over-column-state="hoveredOverColumnState" :is-data-item-disabled="isDataItemDisabled" :is-measuring-column-widths="isMeasuringColumnWidths" :is-nested-content-expanded="isNestedContentShown" :nested-content="shouldShowNestedContentToggle" :nested-rows-element-ids="allNestedRowIds" :row-data="rowData" :row-index="rowIndex" :selection-is-indeterminate="hasUnSelectedNestedRows" :selectable="selectable" :selected-items="selectedItems" @cell-hovered="emit('cell-hovered', $event)" @cell-focus="emit('cell-focus', $event)" @on-frozen-columns-key-press="emit('on-frozen-columns-key-press', $event)" @select-row="onRowSelect" @nested-content-toggled="nestedContentToggled" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-cell> </div> <div v-if="!isFrozenColumns && rowActions" :key="`data-grid-row-actions-${id}`" ref="rowActionsRef" :aria-colindex="sumColumnAmountsInColumnSets().toString()" class="b-data-grid-row-container__row-actions-container" :class="conditionalRowActionsContainerClasses" :style="computedRowActionsStyles" :data-cellindex="getRowActionsCellIndex()" tabindex="-1" role="gridcell" @focus="onRowActionsCellFocus" @focusOut="onRowActionsCellFocusOut" @keydown="onRowActionsCellOnKeyDown" > <data-grid-cell-actions class="b-data-grid-row-container__row-actions" disable-arrow-down-opening-menu :cell-actions="rowActions" :has-right-overflow="hasRightOverflow" :row-data-item="rowData" teleport-menu @menu-open="onRowActionMenuOpen" @menu-close="onRowActionMenuClose" /> </div> </template> </div> <Transition v-if=" (hasNestedContent && hasSlot('nested-slot') && shouldShowNestedContent(rowData)) || (hasNestedRows && shouldShowNestedRowsBooleanResult) " :key="`${rowIndex}-nest-content`" name="b-data-grid-row-container__animation--nested-content" > <div v-if="isNestedContentShown && shouldShowRenderNestedRows" :class="conditionalNestedRowClasses" data-testid="nested-content-container" :style="nestedContentStyles" role="presentation" > <div ref="nestedContentRef"> <div v-if="isLoadingNestedRows" class="b-data-grid-row-container__row-nested-rows-loading-container" > <bento-loading-indicator /> </div> <data-grid-row v-for="(childRowData, childRowIndex) in rowData.children" v-bind="props" :id="getRowId(childRowIndex)" :key="`child-${childRowIndex}`" :ref="el => registerNestedRowRef(el, childRowIndex)" :row-data="childRowData" :row-index="childRowIndex" :row-set-size="rowData.children.length" is-nested-row @cell-hovered="emit('cell-hovered', $event)" @cell-focus="emit('cell-focus', $event)" @row-click="emitRowClick" @on-frozen-columns-key-press="emit('on-frozen-columns-key-press', $event)" @select-row="onNestedRowSelect(childRowData, $event)" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-row> </div> </div> <div v-else-if="isNestedContentShown" v-bind="nestedContentContainerDivAriaAttributes" :class="conditionalNestedRowClasses" data-testid="nested-content-container" :style="nestedContentStyles" role="row" tabindex="-1" > <div ref="nestedContentRef" role="gridcell"> <div v-if="isLoadingNestedRows" class="b-data-grid-row-container__row-nested-rows-loading-container" > <bento-loading-indicator /> </div> <div v-else-if="hasSlot('nested-slot') && shouldShowNestedContent(rowData)"> <slot :item="rowData" name="nested-slot" /> </div> </div> </div> </Transition> </div> </template> <script setup lang="ts"> import { type ComponentPublicInstance, computed, type ComputedRef, inject, onMounted, type PropType, ref, useSlots, watch, } from 'vue'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { focusOnInteractiveChild, makeAllInteractiveChildrenFocusable, makeAllInteractiveChildrenUnFocusable, } from '@/components/data-grid/utils'; import DataGridRow from '@/components/data-grid/components/data-grid-row/data-grid-row.vue'; import DataGridCell from '@/components/data-grid/components/data-grid-cell/data-grid-cell.vue'; import DataGridCellActions from '@/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.vue'; // Types and enums import { type BentoColumn, type BentoDatagridDataItem, type BentoDatagridDataItemId, type BentoDatagridGetDataItemIdFn, type BentoDatagridIsDataItemDisabledFn, type BentoDatagridShouldShowNestedContentFn, type BentoDatagridShouldShowNestedRowsFn, BentoDatagridStyleState, } from '@/components/data-grid/data-grid.types'; import type { DataGridCellColumnSet, DataGridCellDragEventsForColumn, } from '../data-grid-cell/data-grid-cell.types'; import type { BentoDatagridDraggingColumnState } from '../data-grid-columns/data-grid-columns.types'; import type { DatagridColumnHoveringState } from '@/components/data-grid/composables/use-column-hovering'; // Translations import { type Booleanish } from '@/types/prop-types'; import { useHasSlot } from '@/composables/use-has-slot/use-has-slot'; import { useExpandableContentHeight } from '@/composables/use-expandable-content-height/use-expandable-content-height'; import { observeSizeOfElement } from '@/utils/ts/resize'; import { DATAGRID_ACTIONS_COLUMN_FIELD_NAME, DATAGRID_CELL_OFFSET_PADDING, } from '@/components/data-grid/composables/use-calculate-column-width/use-calculate-column-width'; import { generateUid } from '@/core/utils/ts'; import { type BentoDataGridRowActionsProp } from '@/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.types'; import type { HTMLAttributes } from 'vue/types/jsx'; import { EventKey } from '@/types/utils-events'; import { DATA_GRID_SMALL_CONTAINER } from '@/components/data-grid/data-grid.keys'; const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = defineProps({ /** * Row actions property that generates which button to show in row actions area */ rowActions: { type: [Function, Array] as PropType<BentoDataGridRowActionsProp>, default: undefined, }, /** * When true, will emit a 'rowClick' event when a row is clicked. Will add cursor: pointer to rows. */ allowRowClicks: { type: Boolean, default: false }, /** * The columns sets including */ columnSets: { type: Array as PropType<Array<DataGridCellColumnSet>>, required: true, }, /** * Sets the row cells to the condensed padding state. */ condensed: { type: Boolean, default: false, }, /** * Columns being dragged, contains dragging column and a target column */ draggingColumns: { type: Object as PropType<BentoDatagridDraggingColumnState>, default: undefined, }, /** * Dragging event handlers for each row cell */ draggingEventsForRowCell: { type: Function as PropType<DataGridCellDragEventsForColumn>, required: true, }, /** * A customisable method to retrieve a unique identifier for a data item in the array of data supplied. * This is used selecting and performance reasons and must exist in each data item. */ getDataItemId: { type: Function as PropType<BentoDatagridGetDataItemIdFn>, default: (item: BentoDatagridDataItem) => item.id, }, /** * Set to true if the datagrid has been scrolled and the left side has overflowing space. */ hasLeftOverflow: { type: Boolean, default: false, }, /** * Enables the nested content slot functionality. */ hasNestedContent: { type: Boolean, default: false, }, /** * Enables the nested content slot functionality. */ hasNestedRows: { type: Boolean, default: false, }, /** * Set to true if the datagrid has been scrolled and the right side has overflowing space. */ hasRightOverflow: { type: Boolean, default: false, }, /** * Highlighted row IDs. */ highlightedRows: { type: Array as PropType<Array<BentoDatagridDataItemId>>, default: () => [], }, /** * The row index of a currently hovered row. */ hoveredRowIndex: { type: Number, default: -1, }, /** * The column hovered over state and whether the hovered over column should show a border right or left. */ hoveredOverColumnState: { type: Object as PropType<DatagridColumnHoveringState>, required: true, }, /** * A customisable method to determine if a row is not selectable. * The default is that every row is selectable. */ isDataItemDisabled: { type: Function as PropType<BentoDatagridIsDataItemDisabledFn>, default: () => false, }, /** * If the row is of an even number. * Rows are 0th based index e.g. index 0 is an odd-numbered row. */ isRowEven: { type: Boolean, default: false, }, /** * If the row is the last child in it's parent container. */ isRowLastChild: { type: Boolean, default: false, }, /** * If columns are being measured for the autoWidth functionality. */ isMeasuringColumnWidths: { type: Boolean, default: false, }, /** * Determines if this row is nested inside another row. */ isNestedRow: { type: Boolean, default: false, }, /** * The maximum width of the row actions in all rows */ rowActionsMaximumWidth: { type: Number, default: 0, }, /** * The row data object */ rowData: { type: Object as PropType<BentoDatagridDataItem>, required: true, }, /** * The row index. */ rowIndex: { type: Number, required: true, }, /** * The row ID to be using for the ID attribute. */ rowId: { type: Number, default: undefined, }, /** * The number of rows that this row is within. */ rowSetSize: { type: Number, default: 0, }, /** * Determines whether to render frozen seperator on the row. */ showFrozenColumnsSeparator: { type: Boolean, default: false, }, /** * Determines if the row selection is enabled. */ selectable: { type: Boolean, default: false, }, /** * The array of IDs for the selected rows. */ selectedItems: { type: Array as PropType<Array<BentoDatagridDataItemId>>, required: true, }, /** * Enables / Disables the nested-content slot */ shouldShowNestedContent: { type: Function as PropType<BentoDatagridShouldShowNestedContentFn>, default: (_item: BentoDatagridDataItem) => true, }, /** * Returns the nested rows for a given BentoDatagridDataItem. */ shouldShowNestedRows: { type: Function as PropType<BentoDatagridShouldShowNestedRowsFn>, default: (_item: BentoDatagridDataItem) => true, }, }); const emit = defineEmits<{ /** * Emitted when a cell has been hovered over. This is used to keep the row in a hovered state when hovering over a menu. */ (e: 'cell-hovered', value: number); /** * Emitted when an a frozen column cell fires a key up or down event. */ (e: 'on-frozen-columns-key-press', event: Event); /** * Emitted when a row has been selected. */ (e: 'select-row', value: Array<BentoDatagridDataItemId>, rowData: BentoDatagridDataItem); /** * Emitted when a nested row has been selected. */ ( e: 'select-nested-row', value: Array<BentoDatagridDataItemId>, parentRowData: BentoDatagridDataItem, rowData: BentoDatagridDataItem ); /** * Emitted when a row has been clicked. */ (e: 'row-click', rowData: BentoDatagridDataItem, event: MouseEvent | KeyboardEvent); /** * Emitted when a row with nested content is toggled */ (e: 'nested-content-toggled', rowData: BentoDatagridDataItem); /** * Emitted when a row cell is focused. */ ( e: 'cell-focus', value: { rowData: BentoDatagridDataItem; column: BentoColumn; rowIndex: number; colIndex: number } ); }>(); /** * Checks if the relatedTarget element appears after the reference element in DOM order. * Used to detect backward navigation (Shift+Tab). */ const isRelatedTargetAfterInDOM = (relatedTarget: HTMLElement | null, referenceEl: HTMLElement): boolean => { if (!relatedTarget || !referenceEl) { return false; } return !!(referenceEl.compareDocumentPosition(relatedTarget) & Node.DOCUMENT_POSITION_FOLLOWING); }; // Refs const gridcellRefs = ref<{ [key: string]: HTMLDivElement }>({}); const isNestedContentShown = ref(false); const nestedContentRef = ref(); const rowActionsRef = ref(); const nestedRowRefs = ref<Array<InstanceType<typeof DataGridRow>>>([]); const nestedContentSlotId = generateUid('nested-content-slot'); const isRowActionMenuOpen = ref(false); const isExitingRowActionInteraction = ref(false); const focusableItems = ref<Array<{ node: HTMLElement }>>(); /** * Returns the row-action container element, handling the case where Vue 2 * may wrap refs inside a v-for as an array. */ const getRowActionsElement = (): HTMLElement | undefined => { const val = rowActionsRef.value; return Array.isArray(val) ? val[0] : val; }; onMounted(() => { const container = getRowActionsElement(); if (container) { focusableItems.value = makeAllInteractiveChildrenUnFocusable(container); } }); const shouldShowNestedRowsBooleanResult = computed(() => props.shouldShowNestedRows(props.rowData)); const shouldShowRenderNestedRows = computed( () => !props.isNestedRow && props.hasNestedRows && shouldShowNestedRowsBooleanResult.value && props.rowData?.children?.length > 0 ); const isLoadingNestedRows = computed( () => !props.isNestedRow && props.hasNestedRows && shouldShowNestedRowsBooleanResult.value && (props.rowData?.children === undefined || props?.rowData?.children.length === 0) ); const shouldShowNestedContentToggle = computed( () => (props.hasNestedContent && props.shouldShowNestedContent(props.rowData)) || (props.hasNestedRows && shouldShowNestedRowsBooleanResult.value && !props.isNestedRow) ); const ariaRowAttributes = computed<HTMLAttributes>(() => { if (props.hasNestedContent || props.hasNestedRows) { const isNestedContentShownBooleanishVal = isNestedContentShown.value.toString() as Booleanish; return { 'aria-expanded': shouldShowNestedContentToggle.value ? isNestedContentShownBooleanishVal : undefined, 'aria-level': props.isNestedRow ? '2' : '1', 'aria-posinset': props.rowIndex + 1, 'aria-setsize': props.rowSetSize, }; } return { 'aria-rowindex': props.rowIndex + 1 }; }); const nestedContentContainerDivAriaAttributes = computed<HTMLAttributes>(() => { if (isLoadingNestedRows.value) { return { 'aria-busy': 'true', 'aria-live': 'polite', }; } if ( shouldShowRenderNestedRows.value || !props.hasNestedContent || !props.shouldShowNestedContent(props.rowData) ) { return undefined; } return { 'aria-level': props.isNestedRow ? '3' : '2', 'aria-posinset': '1', 'aria-setsize': '1', id: nestedContentSlotId, role: 'row', tabindex: '-1', }; }); const nestedRowIds = computed(() => { if (shouldShowRenderNestedRows.value) { return props.rowData?.children?.reduce((acc, _val, index) => { acc[index] = generateUid('nested-row'); return acc; }, {}); } return []; }); const allNestedRowIds = computed(() => { if (!isNestedContentShown.value) { return undefined; } if (props.hasNestedContent && props.shouldShowNestedContent(props.rowData)) { return [nestedContentSlotId]; } if (shouldShowRenderNestedRows.value && Object.keys(nestedRowIds.value)?.length > 0) { const generatedRowIds = Object.values(nestedRowIds.value); if (generatedRowIds?.length > 0) { return generatedRowIds; } } return []; }); const hasUnSelectedNestedRows = computed(() => { const childrendDataItemIds = props.rowData?.children?.map(nestedItem => props.getDataItemId(nestedItem)); if (childrendDataItemIds?.length > 0) { const everyChildIsSelected = childrendDataItemIds.every(childId => props?.selectedItems?.includes(childId)); const someItemsAreSelected = childrendDataItemIds.some(childId => props?.selectedItems?.includes(childId)); return !everyChildIsSelected && someItemsAreSelected; } return false; }); // Methods const registerRowCellRef = (el: Element | ComponentPublicInstance, field: string) => { // We always know this will be used on a DataGridCell component so we can cast it const gridCellComponent = el as InstanceType<typeof DataGridCell>; if (el !== null && gridCellComponent?.gridcellRef) { gridcellRefs.value[field] = gridCellComponent.gridcellRef as HTMLDivElement; } }; const registerNestedRowRef = (el: Element | ComponentPublicInstance, childRowIndex: number) => { if (el !== null) { nestedRowRefs.value[childRowIndex] = el as InstanceType<typeof DataGridRow>; } }; const getRowId = (childRowIndex: number) => { return nestedRowIds.value[childRowIndex]; }; const getRowAriaSelectedValue = (row: BentoDatagridDataItem): Booleanish => { if (!props.selectable) { return null; } const id = props.getDataItemId(row); return props.selectedItems.includes(id).toString() as Booleanish; }; const emitRowClick = (rowData: BentoDatagridDataItem, event: MouseEvent | KeyboardEvent) => { emit('row-click', rowData, event); }; /** * Handles row clicks when the `allowRowClicks` prop is true. * * This method also prevents the `row-click` event from firing if the click * originated from an interactive element (like a button, input, or link) * within the row. This is to avoid unintended behavior when a user * interacts with controls inside a row. * * @param event The mouse or keyboard event from the click or key press. */ const onRowClick = (event: MouseEvent | KeyboardEvent) => { // Only proceed if allowRowClicks is true if (!props.allowRowClicks) { return; } let isEventTargetInteractiveElement = false; const dataGridRowInteractiveTypes = ['button', 'input', 'a']; for (let i = 0; i < dataGridRowInteractiveTypes.length; i++) { if ((event?.target as HTMLElement)?.closest(dataGridRowInteractiveTypes[i])) { isEventTargetInteractiveElement = true; break; } } // Only trigger if element is NOT interactive if (!isEventTargetInteractiveElement) { emitRowClick(props.rowData, event); } }; const onRowSelect = (value: Array<BentoDatagridDataItemId>) => { emit('select-row', value, props.rowData); }; const onNestedRowSelect = (rowData: BentoDatagridDataItem, value: Array<BentoDatagridDataItemId>) => { emit('select-nested-row', value, props.rowData, rowData); }; /** * Sets the state to indicate that a row action menu is open. This is used to apply a higher * z-index to the row's action container, ensuring the menu displays correctly above * subsequent rows. */ const onRowActionMenuOpen = () => { isRowActionMenuOpen.value = true; }; /** * Resets the state when a row action menu is closed, removing the elevated z-index from the * row's action container. */ const onRowActionMenuClose = () => { isRowActionMenuOpen.value = false; }; /** * When the row actions cell is focused, determine navigation direction and focus * the appropriate interactive child. Backward navigation (Shift+Tab) focuses the * last child; forward navigation focuses the first. */ const onRowActionsCellFocus = (e: FocusEvent) => { if (isExitingRowActionInteraction.value) { isExitingRowActionInteraction.value = false; return; } const container = getRowActionsElement(); if (!container || !focusableItems.value?.length) { return; } const isBackwardNavigation = isRelatedTargetAfterInDOM(e.relatedTarget as HTMLElement, container); const lastIndex = focusableItems.value.length - 1; const targetItem = isBackwardNavigation ? focusableItems.value[lastIndex] : focusableItems.value[0]; makeAllInteractiveChildrenFocusable([targetItem]); focusOnInteractiveChild(container, { last: isBackwardNavigation }); }; /** * When focus leaves the row actions cell, make all interactive children unfocusable. * This ensures that tabbing through the grid skips over the individual actions within the cell. */ const onRowActionsCellFocusOut = () => { const container = getRowActionsElement(); if (container) { makeAllInteractiveChildrenUnFocusable(container); } }; /** * Handles keyboard navigation within the row actions cell. * Allows using left and right arrow keys to move between interactive elements (e.g., buttons) in the cell. * @param e The keyboard event. */ const onRowActionsCellOnKeyDown = (e: KeyboardEvent) => { const container = getRowActionsElement(); // Escape returns focus to the gridcell container if (e.key === EventKey.ESCAPE) { if (container) { makeAllInteractiveChildrenUnFocusable(container); } isExitingRowActionInteraction.value = true; container?.focus(); e.stopPropagation(); e.preventDefault(); return; } if (e.key !== EventKey.TAB && container) { makeAllInteractiveChildrenUnFocusable(container); } if ( e.key === EventKey.ARROW_RIGHT && focusableItems.value?.length > 1 && focusableItems.value[0].node === document.activeElement ) { if (container) { makeAllInteractiveChildrenUnFocusable(container); } focusableItems.value[1].node.setAttribute('tabindex', '0'); focusableItems.value[1].node.focus(); e.stopPropagation(); e.preventDefault(); } else if ( e.key === EventKey.ARROW_LEFT && focusableItems.value?.length > 1 && focusableItems.value[1].node === document.activeElement ) { if (container) { makeAllInteractiveChildrenUnFocusable(container); } focusableItems.value[0].node.setAttribute('tabindex', '0'); focusableItems.value[0].node.focus(); e.stopPropagation(); e.preventDefault(); } }; // Cell and Columns index calculation const sumColumnAmountsInColumnSets = () => props.columnSets.reduce((total, { columnSet }) => total + columnSet.length, 0); const getRowActionsCellIndex = () => sumColumnAmountsInColumnSets() * props.rowIndex + sumColumnAmountsInColumnSets(); // Nested content expanding logic const { updateContentHeight, contentHeight } = useExpandableContentHeight(nestedContentRef, isNestedContentShown); const nestedContentStyles = computed(() => { if (props.hasNestedContent || props.hasNestedRows) { // calculate the padding by getting the action column width and the first column left padding: const paddingLeft = shouldShowRenderNestedRows.value || isLoadingNestedRows.value ? 0 : gridcellRefs.value[DATAGRID_ACTIONS_COLUMN_FIELD_NAME].offsetWidth + DATAGRID_CELL_OFFSET_PADDING; return { paddingLeft: `${paddingLeft}px`, 'max-height': (props.hasNestedContent || props.hasNestedRows) && contentHeight.value && !contentHeight?.value?.includes('null') ? contentHeight.value : 0, }; } return undefined; }); watch( () => nestedContentRef.value, () => { let observer: ResizeObserver | null = null; if (!observer && nestedContentRef.value) { observer = observeSizeOfElement(nestedContentRef.value, () => { if (nestedContentRef.value) { updateContentHeight(); } }); } } ); const nestedContentToggled = () => { isNestedContentShown.value = !isNestedContentShown.value; updateContentHeight(); emit('nested-content-toggled', props.rowData); }; watch( () => props.rowData?.children, (_newChildren, oldChildren) => { if (props?.rowData?.children?.length > 0 && oldChildren === undefined && !isNestedContentShown.value) { updateContentHeight(); } } ); const isSmallContainer: ComputedRef<boolean> = inject( DATA_GRID_SMALL_CONTAINER, computed(() => false) ); const isHighlightedRow = row => props.highlightedRows.includes(props.getDataItemId(row)); const isSelectedRow = row => props.selectedItems.includes(props.getDataItemId(row)); // Styling const conditionalRowClasses = (row: BentoDatagridDataItem, rowIndex: number) => ({ 'b-data-grid-row-container__row--disabled': props.isDataItemDisabled(row), 'b-data-grid-row-container__row--row-clicks-enabled': props.allowRowClicks, 'b-data-grid-row-container__row--highlighted': isHighlightedRow(row), 'b-data-grid-row-container__row--selected': isSelectedRow(row), 'b-data-grid-row-container__row--hovered': props.hoveredRowIndex === rowIndex, 'b-data-grid-row-container__row--is-last-child': props.isRowLastChild && !isNestedContentShown.value, 'b-data-grid-row-container__row--nested-row': props.isNestedRow, 'b-data-grid-row-container__row--zebra': props.isRowEven, }); const conditionalRowSetClasses = (isFrozenRowSet: boolean, isSmallContainer: boolean) => ({ 'b-data-grid-row-container__row-cell-set--frozen': isFrozenRowSet && !isSmallContainer, [`b-data-grid-row-container__row-cell-set--${BentoDatagridStyleState.OVERFLOW_SHADOW_LEFT}`]: !isSmallContainer && isFrozenRowSet && props.hasLeftOverflow, 'b-data-grid-row-container__row-cell-set--frozen-separator': !isSmallContainer && isFrozenRowSet && props.showFrozenColumnsSeparator, 'b-data-grid-row-container__row-cell-set--unfrozen': !isFrozenRowSet || isSmallContainer, 'b-data-grid-row-container__row-cell-set--is-measuring': props.isMeasuringColumnWidths, }); const conditionalNestedRowClasses = computed(() => ({ 'b-data-grid-row-container__row-nested-slot--is-last-child': props.isRowLastChild, 'b-data-grid-row-container__row-nested-slot': !shouldShowRenderNestedRows.value && props.shouldShowNestedContent(props.rowData), })); const conditionalRowActionsContainerClasses = computed(() => ({ 'b-data-grid-row-container__row-actions-container--row-actions-menu-open': isRowActionMenuOpen.value, })); const computedRowActionsStyles = computed(() => ({ minWidth: `${props.rowActionsMaximumWidth}px`, })); // Exposed refs defineExpose({ gridcellRefs, isNestedContentShown, nestedRowRefs, rowActionsRef, }); </script> <script lang="ts"> /** * Internal component to display the BentoDataGrid data rows containig the data row cells. * * @example * * export default { * components: { DataGridRow }, * template: ` * <data-grid-row * :ref="el => registerRowCellRefs(el, rowIndex)" * :style="gridColumnWidthStyle" * :allow-row-clicks="allowRowClicks" * :column-sets="columnSets" * :condensed="condensed" * :dragging-columns="draggingColumns" * :dragging-events-for-row-cell="draggingEventsForRowCell" * :get-data-item-id="getDataItemId" * :has-left-overflow="hasLeftOverflow" * :highlighted-rows="highlightedRows" * :hovered-row-index="hoveredRowIndex" * :hovered-over-column-state="hoveredOverColumnState" * :is-data-item-disabled="isDataItemDisabled" * :is-measuring-column-widths="isMeasuringColumnWidths" * :row-data="rowData" * :row-index="rowIndex" * :show-frozen-columns-separator="showFrozenColumnsSeparator" * :selectable="selectable" * :selected-items="selectedItems" * @cell-hovered="onCellHover" * @row-click="onRowClick" * @on-frozen-columns-key-press="updateScrollPositionOnFrozenColumnsNavigation" * @select-row="onRowSelect" * > * <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> * <slot :name="innerSlot" v-bind="scope" /> * </template> * </data-grid-row> * ` */ export default { name: 'data-grid-row', }; </script> <style lang="scss" scoped src="./data-grid-row.scss" />
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type BentoDataGridStaticIconVariant = 'checkmark' | 'warning' | 'error' | 'no-value' | 'cross';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import type { BentoDashedUnderlineProps } from '../../../dashed-underline'; export interface BentoDataGridSummaryCellProps { /** * The summary text to display when there are multiple items. * e.g. "2 merchants" */ label: string; /** * An array of strings to display. If there is only one item, it will be displayed as plain text. * If there are multiple items, they will be displayed in a popover. */ items: Array<string>; /** * Props to pass to the underlying `BentoDashedUnderline` component, allowing for customization of the popover. */ dashedUnderline?: BentoDashedUnderlineProps; /** * An optional search term. If provided, any items in the popover that include this term will be highlighted. */ searchTerm?: string; /** * Overrides and displays text in a tooltip when the user hovers over the summary label. */ tooltipText?: string; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { type BentoTagProps } from '@/components/tag/tag.types'; export interface BentoDataGridTagWithTextCellProps { /** * The tag to display. {@see BentoTagProps} */ tag: BentoTagProps; /** * The text to display next to the tag. */ value: string; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type BentoDataGridTrendCellVariant = 'red' | 'green' | 'black'; export type BentoDataGridTrendCellDirection = 'up' | 'down' | 'none'; export interface BentoDataGridTrendCellProps { /** * The direction of the trend icon to display. */ direction?: BentoDataGridTrendCellDirection; /** * The color variant, affecting both the icon and text. */ variant?: BentoDataGridTrendCellVariant; /** * The numeric or string value to display. It will be formatted as a percentage. */ value: number | string; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { type Ref } from 'vue'; import { type BentoColumn } from '../../data-grid.types'; // Internal types export type FlexColumnLookup = Record< BentoColumn['field'], { flex: number; computedWidth: number; frozen: boolean; } >; export interface CalculateColumnWidthOptions { allowColumnDragAndDrop?: Ref<boolean>; hasColumnPanel?: Ref<boolean>; hasNestedContent?: Ref<boolean>; hasNestedRows?: Ref<boolean>; hasSelectableColumn?: Ref<boolean>; hasSelectableAcrossPages?: Ref<boolean>; isDataGridTemplate?: boolean; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import type { ComputedRef } from 'vue'; import type { BentoColumn } from '../../data-grid.types'; export interface DatagridColumnHoveringState { hoveredOverColumnField: string; isSeparatorOffset: boolean; noBorderLeft: boolean; noBorderRight: boolean; } export interface DatagridColumnHoveringUseColumnHoveringReturnType { hoveredOverColumn: BentoColumn; hoveredOverColumnState: ComputedRef<DatagridColumnHoveringState>; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import type { BentoColumn } from '@/components/data-grid/components/data-grid-columns/data-grid-columns.types'; export interface DatagridConfigSettingsColumnSet { columnSet: Array<BentoColumn>; id: string; isFrozenColumns?: boolean; } export type DatagridConfigSettingsColumnSets = Array<DatagridConfigSettingsColumnSet>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { EventKey } from '@/types/utils-events'; export enum BentoDatagridNavigationKey { ARROW_DOWN = EventKey.ARROW_DOWN, ARROW_UP = EventKey.ARROW_UP, ARROW_LEFT = EventKey.ARROW_LEFT, ARROW_RIGHT = EventKey.ARROW_RIGHT, PAGE_UP = EventKey.PAGE_UP, PAGE_DOWN = EventKey.PAGE_DOWN, END = EventKey.END, HOME = EventKey.HOME, }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import type { BentoPaginationProps } from '@/components/pagination/pagination.types'; import type { BentoEmptyStateProps } from '@/components/empty-state/empty-state.types'; import type { BentoColumn } from './components/data-grid-columns/data-grid-columns.types'; import type { BentoDatagridDataItem, BentoDatagridDataItemId } from './components/data-grid-row/data-grid-row.types'; export { type BentoColumn, BentoColumnOverflow } from './components/data-grid-columns/data-grid-columns.types'; export type { BentoDatagridDataItem, BentoDatagridDataItemId } from './components/data-grid-row/data-grid-row.types'; export type { BentoDataGridRowActionsProp, BentoDataGridRowActionsFn, BentoColumnCellActionsProp, BentoColumnCellAction, } from './components/data-grid-cell-actions/data-grid-cell-actions.types'; export enum BentoDatagridStyleState { OUTLINE = 'outline', SCROLL = 'scroll', STICKY_FILTERS = 'sticky-filters', OVERFLOW_SHADOW_LEFT = 'overflow-shadow-left', OVERFLOW_SHADOW_RIGHT = 'overflow-shadow-right', NO_SPACING_TOP = 'no-spacing-top', FIT_CONTENT = 'fit-content', ZEBRA_ROWS = 'zebra-rows', } export enum BentoDatagridSortDirection { ASCENDING = 'asc', DESCENDING = 'desc', } export interface BentoDatagridSortByColumn { field: BentoColumn['field']; direction: BentoDatagridSortDirection; } export type BentoDatagridSelection = Array<BentoDatagridDataItemId>; export type BentoDatagridData = Array<BentoDatagridDataItem>; export type BentoDatagridGetDataItemIdFn = (item: BentoDatagridDataItem) => BentoDatagridDataItemId; export type BentoDatagridIsDataItemDisabledFn = (item: BentoDatagridDataItem) => boolean; export type BentoDatagridPaginationProps = BentoPaginationProps; export interface BentoColumnWidthOverridesLookup { [field: string]: { width: BentoColumn['width']; }; } export interface BentoDatagridAllItemsSelectedState { allItemsSelected: boolean; isIndeterminate: boolean; } export interface BentoDataGridVisibleRowsState { filtered: boolean; allRowsVisible: boolean; } export enum BentoDatagridEvent { CELL_FOCUS = 'cell-focus', COLUMNS = 'columns', COLUMN_RESIZE = 'column-resize', ITEMS_PAGE = 'items-page', NAVIGATE = 'navigate', NESTED_CONTENT_TOGGLED = 'nested-content-toggled', FILTER_SEARCH_INPUT = 'filter-search-input', FILTER_SEARCH_CLEAR = 'filter-search-clear', UPDATE_COLUMNS = 'update:columns', UPDATE_CONDENSED = 'update:condensed', UPDATE_FILTERS = 'update:filters', UPDATE_FILTER_VALUES = 'update:filter-values', UPDATE_FILTER_VALUES_OBJECT = 'update:filter-values-object', UPDATE_FILTER_SEARCH_TERM = 'update:filter-search-term', UPDATE_PAGINATION = 'update:pagination', UPDATE_SELECTION = 'update:selection', UPDATE_SHOW_MORE = 'update:show-more', UPDATE_SORT_BY = 'update:sort-by', UPDATE_ZEBRA_ROWS = 'update:zebra-rows', ROW_CLICK = 'row-click', SELECT = 'select', SELECT_ALL_ACROSS_PAGES = 'select-all-across-pages', SELECT_ALL_CURRENT_RESULTS = 'select-all-current-results', DESELECT_ALL_CURRENT_RESULTS = 'deselect-all-current-results', SORT = 'sort', } export enum BentoDatagridAriaSortValue { ASCENDING = 'ascending', DESCENDING = 'descending', NONE = 'none', } export type BentoDatagridEmptyStateProps = BentoEmptyStateProps; export type BentoDatagridShouldShowNestedContentFn = (item: BentoDatagridDataItem) => boolean; export type BentoDatagridShouldShowNestedRowsFn = (item: BentoDatagridDataItem) => boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div ref="dataGridRef" class="b-data-grid" data-testid="data-grid" :class="conditionalGridClasses" aria-live="polite" :aria-busy="loading" @copy="handleClipboard" > <div v-if="filters" ref="filtersRef" class="b-data-grid__filters" :class="conditionalFilterClasses" :style="{ top: computedStickyFiltersTopPosition }" :aria-controls="dataGridUid" :aria-label="t('filterBar')" role="region" > <bento-filter-bar v-if="filters" ref="filterBarRef" :config="filters" :container-size-layout="filtersContainerSizeLayout" :filter-values="filterValues" :filter-values-object="filterValuesObject" :search="filterSearchConfig" :search-term="filterSearchTerm" @search-input="onSearchTermInput" @search-clear="onSearchTermCleared" @input="filters => $emit(BentoDatagridEvent.UPDATE_FILTERS, filters)" @update:filter-values="filterValues => $emit(BentoDatagridEvent.UPDATE_FILTER_VALUES, filterValues)" @update:filter-values-object=" filterValueObject => $emit(BentoDatagridEvent.UPDATE_FILTER_VALUES_OBJECT, filterValueObject) " /> </div> <div v-if="$slots.header" class="b-data-grid__header"> <slot name="header"></slot> </div> <div :id="dataGridUid" :role="ariaGridRole" :aria-rowcount="data.length" :aria-colcount="columns.length" :aria-label="emptyState ? t('noResults') : null" class="b-data-grid__wrapper" :style="{ top: computedColumnHeadersTopPosition }" > <div class="b-data-grid__column-headers" :style="{ top: computedColumnHeadersTopPosition }"> <div ref="columnsRef" role="rowgroup" class="b-data-grid__columns-row" @scroll="updateScrollPosition" @keydown="navigationInput" > <div v-if="columnPanel" role="row"> <bento-data-grid-config-settings class="b-data-grid__config-settings" role="gridcell" :columns="localColumnsState" :condensed="condensed" :zebra-rows="internalZebraRows" @update:condensed="onConfigSettingsCondensedUpdate" @update:zebra-rows="onConfigSettingsZebraRowsUpdate" @update:columns="onConfigSettingsColumnsUpdate" /> </div> <div :style="gridRowWidthStyle" class="b-data-grid__columns" role="row"> <template v-for="({ columnSet, id, isFrozenColumns }, index) of columnSets"> <data-grid-columns v-if="columnSet?.length" :key="`data-grid-columns-${id}`" :ref="el => registerColumnHeaderRefs(el)" :columns="columnSet" :has-resizable-columns="hasResizableColumns" :sort-by="sortBy" :all-items-selected-state="allItemsSelectedState" :is-frozen-columns="isFrozenColumns" :show-frozen-columns-separator="showFrozenColumnsSeparator" :dragging-columns="draggingColumns" :has-draggable-columns="allowColumnDragAndDrop" :has-column-panel="columnPanel" :has-left-overflow="hasLeftOverflow" :has-first-column-panel-right-aligned="index === computedColumnPanelAlignment" :hovered-over-column-state="hoveredOverColumnState" :is-measuring-column-widths="isMeasuringColumnWidths" :is-only-one-visible-column="isOnlyOneVisibleColumn" :selectable="data?.length > 0 && selectable" :selectable-across-pages="selectableAcrossPagesComputed" :selected-items="selectedItems" :visible-rows-state="visibleRowsState" :selection-state="selectionState" :truncate-column-headers="truncateColumnHeaders" :row-actions-maximum-width="rowActionsMaxWidth" v-on="draggingEventsForColumns" @column-hover="hoveredOverColumn = $event" @column-move="onMoveColumnByOffset" @column-update="onConfigSettingsColumnUpdate" @column-resize="onColumnResized" @column-resizing="onColumnWidthOverride" @select-all-rows="onAllRowSelect" @select-all-across-pages="emit(BentoDatagridEvent.SELECT_ALL_ACROSS_PAGES)" @select-all-current-results="emit(BentoDatagridEvent.SELECT_ALL_CURRENT_RESULTS)" @deselect-all-current-results="emit(BentoDatagridEvent.DESELECT_ALL_CURRENT_RESULTS)" @deselect-all="onDeselectAll" @sort="onSort" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-columns> </template> </div> </div> </div> <div ref="body" data-testid="data-grid-body" class="b-data-grid__body" :class="conditionalGridBodyClasses" @scroll="updateScrollPosition" > <div v-if="loading && !isLazyLoad" class="b-data-grid__loading"> <bento-loading-indicator class="b-data-grid__loading-indicator" large /> <bento-typography el="span" variant="caption">{{ t('gettingYourData') }}</bento-typography> </div> <div v-else role="rowgroup" @keydown="navigationInput"> <template v-if="data?.length === 0 && emptyState"> <div role="row" tabindex="-1"> <div role="gridcell" :aria-colspan="columns.length" tabindex="-1"> <bento-empty-state class="b-data-grid__empty-state" v-bind="limitedEmptyStateProps" /> </div> </div> </template> <data-grid-row v-for="(rowData, rowIndex) in data" v-else :key="rowIndex" :ref="el => registerRowCellRefs(el, rowIndex)" :style="gridRowWidthStyle" :allow-row-clicks="allowRowClicks" :column-sets="columnSets" :condensed="condensed" :dragging-columns="draggingColumns" :dragging-events-for-row-cell="draggingEventsForRowCell" :get-data-item-id="getDataItemId" :has-nested-content="hasNestedContent" :has-nested-rows="hasNestedRows" :has-left-overflow="hasLeftOverflow" :has-right-overflow="hasRightOverflow" :highlighted-rows="highlightedRows" :hovered-row-index="hoveredRowIndex" :hovered-over-column-state="hoveredOverColumnState" :is-data-item-disabled="isDataItemDisabled" :is-row-even="internalZebraRows ? rowIndex % 2 !== 0 : false" :is-row-last-child="rowIndex + 1 === data?.length" :is-measuring-column-widths="isMeasuringColumnWidths" :row-actions-maximum-width="rowActionsMaxWidth" :row-actions="rowActions" :row-data="rowData" :row-index="rowIndex" :row-set-size="data?.length" :show-frozen-columns-separator="showFrozenColumnsSeparator" :should-show-nested-content="shouldShowNestedContent" :should-show-nested-rows="shouldShowNestedRows" :selectable="selectable" :selected-items="selectedItems" @cell-hovered="onCellHover" @cell-focus="emit(BentoDatagridEvent.CELL_FOCUS, $event)" @nested-content-toggled="emit(BentoDatagridEvent.NESTED_CONTENT_TOGGLED, $event)" @row-click="onRowClick" @on-frozen-columns-key-press="updateScrollPositionOnFrozenColumnsNavigation" @select-row="onRowSelect" @select-nested-row="onNestedRowSelect" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-row> </div> </div> </div> <data-grid-lazy-load v-if="isLazyLoad" :loading="loading" :is-infinite-scroll="isInfiniteScroll" @update:show-more="emitLazyLoadEvent" /> <div v-if="stickyHorizontalScrollbar" ref="virtualHorizontalScrollbarRef" class="b-data-grid__horizontal-scrollbar" aria-hidden="true" data-testid="sticky-horizontal-scrollbar" :style="horizontalScrollbarConditionalStyles" @scroll="syncVirtualScrollerWithBody" > <div :style="{ width: `${totalWidthOfAllColumns}px`, height: '15px' }"></div> </div> <div v-if="pagination && !isLazyLoad" ref="paginationContainerRef" data-testid="pagination-container" class="b-data-grid__pagination" :class="conditionalPaginationClasses" > <bento-pagination v-bind="pagination" :aria-label="$t('pagination')" :aria-controls="dataGridUid" @navigate="onPaginationNavigate" @items-page="onItemsPerPageChange" > <slot v-if="hasSlot('pagination-slot')" name="pagination-slot"></slot> </bento-pagination> </div> <div v-if="$slots.footer" class="b-data-grid__footer"><slot name="footer"></slot></div> <bento-action-bar v-if="showActionBar" :actions="bulkActions" :item-counter="itemsSelectedCounter" :extra-bottom-spacing="stickyPagination" @close="onDeselectAll" /> </div> </template> <script lang="ts" setup> import { type ComponentPublicInstance, computed, inject, onMounted, type PropType, provide, ref, toRef, toRefs, useSlots, } from 'vue'; // Components import { BentoActionBar } from '@/components/action-bar'; import { BentoPagination } from '@/components/pagination'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoTypography } from '@/components/typography'; import { BentoDataGridConfigSettings, DataGridColumns, DataGridLazyLoad, DataGridRow } from './components'; // Composables import { DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME, useCalculateColumnWidth, useColumnHovering, useColumnReorderer, useConfigSettings, useKeyboardNavigator, useLazyLoading, useScroller, useSelector, } from './composables'; import { useCalculateStickyElementHeight, useHasSlot } from '@/composables'; // Utils import { generateUid } from '@/core/utils/ts/generate-uid'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { handleClipboard } from '@/components/data-grid/utils/handle-clipboard/handle-clipboard'; // Types import { type BentoColumn, type BentoDatagridData, type BentoDatagridDataItem, type BentoDatagridDataItemId, type BentoDatagridEmptyStateProps, BentoDatagridEvent, type BentoDatagridGetDataItemIdFn, type BentoDatagridIsDataItemDisabledFn, type BentoDatagridPaginationProps, type BentoDatagridShouldShowNestedContentFn, type BentoDatagridShouldShowNestedRowsFn, type BentoDatagridSortByColumn, BentoDatagridStyleState, type BentoDataGridVisibleRowsState, } from './data-grid.types'; import { type BentoTabProvidedTitle } from '../tabs/components/tab.types'; import { type BentoDataGridTemplateDataGrid } from '@/templates/data-grid-template/data-grid-template.types'; import { type BentoLazyLoadType } from '@/types/lazy-load'; import type { BentoButtonActionsList } from '@/components/button'; import BentoFilterBar from '@/components/filter-bar/filter-bar.vue'; import { type BentoFilterBarContainerSizeLayout, type BentoFilterBarModel, type BentoFilterBarSearchBarProps, type BentoFilterBarSearchTerm, type BentoFilterValueObject, type BentoFilterValues, } from '@/components/filter-bar'; import { BentoEmptyState } from '@/components/empty-state'; import type { DatagridColumnsHeaderReferences } from '@/components/data-grid/components/data-grid-columns/data-grid-columns.types'; import type { BentoDataGridRowActionsProp } from '@/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.types'; // Keys import { EXTERNAL_TITLE_REGISTER_ENTRY_INJECTION_KEY } from '@/components/tabs/tabs.keys'; import { DATA_GRID_REGISTER_TEMPLATE_INJECTION_KEY } from '@/templates/data-grid-template/data-grid-template.keys'; import { DATA_GRID_SLASHED_ZERO, DATA_GRID_SMALL_CONTAINER, DATA_GRID_TOUCH_DEVICE } from './data-grid.keys'; // Translations import messages from './messages.json'; import { useElementSize } from '@vueuse/core'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * When true, will emit a 'rowClick' event when a row is clicked. Will add cursor: pointer to rows. */ allowRowClicks: { type: Boolean, default: false }, /** * When true, column headers will become draggable, allowing users to rearrange columns. */ allowColumnDragAndDrop: { type: Boolean, default: true }, /** * List of actions that will be rendered in the action bar when user selects items. * * Each object in the list should have a `title`, an `event` and, * optionally, an `icon`. * * The component will display 4 actions and the hide the rest inside a menu. * * @see BentoButton for a list of all the other props supported by each button action. */ bulkActions: { type: Array as PropType<BentoButtonActionsList>, default: null, }, /** * Sets the row cells to the condensed padding state. */ condensed: { type: Boolean, default: false, }, /** * Column definitions using 'key' to relate to data object. */ columns: { type: Array as PropType<Array<BentoColumn>>, required: true, }, /** * Enables / hides the display options panel and the column contextual menu functionality. */ columnPanel: { type: Boolean, default: false }, /** * An array of data objects to display. */ data: { type: Array as PropType<BentoDatagridData>, required: true, }, /** * Empty state props that will be passed down to inner empty-state component */ emptyState: { type: Object as PropType<BentoDatagridEmptyStateProps>, default: undefined, }, /** * filter-bar value (i.e. filters) that will be passed down to inner filter-bar component * {@see BentoFilterBarModel} */ filters: { // TODO: Change this to BentoFilterBarProps['value'] when the interface is available type: Array as PropType<BentoFilterBarModel>, default: undefined, }, /** * Defines the layout (multi-line or one-line) to be used in the filter bar for different * container sizes: * - Large (>=700px): Always multi-line * - Medium (>=430px & <700px) * - Small (<430px) * * @default * { * medium: 'multi-line', * small: 'one-line' * } */ filtersContainerSizeLayout: { type: Object as PropType<BentoFilterBarContainerSizeLayout>, default: undefined, }, /** * An array of the values for each filter set in `value`. * {@see BentoFilterValues} */ filterValues: { type: Array as PropType<BentoFilterValues>, default: () => [], }, /** * An object of the values for each filter in the filter bar where key is a the field name and the value is the BentoFilterBarValue. * * @example * { * 'company': '200', * 'name': 'some text' * }, */ filterValuesObject: { type: Object as PropType<BentoFilterValueObject>, default: () => undefined, }, // TODO: Move this prop as a part of "config" BentoFilterBarModel = Array<BentoFilterModel | BentoFilterSearchModel> in Bento 2.0. // It was not done now because it's a breaking change for the types. /** * Filter's search bar properties with which the placeholder, hint and other * "bento-search-bar" properties can be set. @see {@link BentoFilterBarSearchBarProps} */ filterSearchConfig: { type: Object as PropType<BentoFilterBarSearchBarProps>, default: undefined, }, /** * Filter bar search term useful to filter all the data by text */ filterSearchTerm: { type: String as PropType<BentoFilterBarSearchTerm>, default: undefined, }, /** * Allows the grid to be rendered inside a confined space. * This makes the data-grid expand to fill the height of its parent container, with the grid body becoming scrollable. */ fitContent: { type: Boolean, default: false }, /** * 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, }, /** * Row actions propery that generates which button to show in row actions area */ rowActions: { type: [Function, Array] as PropType<BentoDataGridRowActionsProp>, default: undefined, }, /** * If resize is enabled */ hasResizableColumns: { type: Boolean, default: true, }, /** * Enables the nested content slot functionality. */ hasNestedContent: { type: Boolean, default: false, }, /** * Enables the nested content slot functionality. */ hasNestedRows: { type: Boolean, default: false, }, /** * Highlighted items */ highlightedRows: { type: Array as PropType<Array<BentoDatagridDataItemId>>, default: () => [], }, /** * 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, }, /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button * @values automatic, button, none */ lazyLoadType: { type: String as PropType<BentoLazyLoadType>, default: 'none', validator: (value: BentoLazyLoadType) => ['automatic', 'button', 'none'].includes(value), }, /** * If the loading indicator should be shown. */ loading: { type: Boolean, default: false, }, /** * Enables / Disables the table's surrounding border. */ outline: { type: Boolean, default: false, }, /** * Renders the data grid with no top spacing, to render inside containers that already have spacing */ noSpacingTop: { type: Boolean, default: false, }, /** * Pagination props that will be passed down to inner pagination component */ pagination: { type: Object as PropType<BentoDatagridPaginationProps>, default: undefined, }, /** * Adds an extra option when selecting all items that allows user to select items in other pages too. * Needs pagination.totalCount to be set. */ selectableAcrossPages: { type: Boolean, default: false, }, /** * Renders a font numeric style with slashed zeros instead of normal ones. */ slashedZero: { type: Boolean, default: false }, /** * Sticks the horizontal scrollbar to the bottom of the screen if the data-grid is larger than the screen height. */ stickyHorizontalScrollbar: { type: Boolean, default: false, }, /** * Make pagination stick to the bottom of the page when the data-grid is bigger than the screen. * Because styles have to be applied to pagination container element, this has to be a data-grid prop (rather than a pagination prop). */ stickyPagination: { type: Boolean, default: false, }, /** * Make data-grid filters stick to the top of the page along with the data-grid headers. * Defaults to true. */ stickyFilters: { type: Boolean, default: true, }, /** * Enables / Disables rows to be selected via a checkbox. On select the "select" event will be emitted. */ selectable: { type: Boolean, default: false, }, /** * Selected items */ selection: { type: Array as PropType<Array<BentoDatagridDataItemId>>, default: () => [], }, /** * 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, }, /** * Sortby handles proper rendering of sort icon in header. Sorting currently is only supported server side. * It does make sure the currently sorted column has a more visible sort icon, and the sort icon points in the right direction. */ sortBy: { type: Array as PropType<Array<BentoDatagridSortByColumn>>, // NOTE: is an array to allow multi-sorting in the future default: () => [], }, /** * Title of the data grid to be rendered as tab title when a data grid is passed in a tab layout. */ tabTitle: { type: String, default: null, }, /** * Truncates all data grid column headers */ truncateColumnHeaders: { type: Boolean, default: false, }, /** * Toggles the alternating background color of rows. */ zebraRows: { type: Boolean, default: false, }, }); const slots = useSlots(); const hasSlot = useHasSlot(slots); /* == Variables and references == */ // Create refs for elements const body = ref<HTMLDivElement>(null); const columnsRef = ref<HTMLDivElement>(); const columnHeaderRefs = ref<DatagridColumnsHeaderReferences>({}); const dataGridUid = ref<string>(generateUid('data-grid')); const gridcellRefs = ref<{ [key: string]: Array<HTMLDivElement> }>(null); const filtersRef = ref<HTMLDivElement>(null); const filterBarRef = ref<InstanceType<typeof BentoFilterBar>>(null); const paginationContainerRef = ref<HTMLDivElement>(null); const verticalScrollPositionRef = ref(null); const virtualHorizontalScrollbarRef = ref<HTMLDivElement>(null); const { allowColumnDragAndDrop, columns, columnPanel, data, lazyLoadType, loading, hasNestedContent, hasNestedRows, selectable, selection, selectableAcrossPages, stickyPagination, outline, pagination, zebraRows, } = toRefs(props); const hoveredRowIndex = ref(-1); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const frozenColumnsLength = computed(() => columnDefs.value.filter(column => column.frozen).length); /* == Responsive behavior == */ // Identifies if the data-grid is in a container smaller than 550px const MOBILE_BREAKPOINT = 550; const dataGridRef = ref(); const { width } = useElementSize(dataGridRef); const isSmallContainer = computed(() => width.value <= MOBILE_BREAKPOINT); provide(DATA_GRID_SMALL_CONTAINER, isSmallContainer); /* == Touch device behavior == */ // Identifies if the data-grid is rendered on a touch device const isTouchDevice = ref(false); onMounted(() => { if (typeof window !== 'undefined') { isTouchDevice.value = 'ontouchstart' in window || navigator?.maxTouchPoints > 0; } }); provide(DATA_GRID_TOUCH_DEVICE, isTouchDevice); /* == Data-grid template == */ const registerDatagridInDataGridTemplate: BentoDataGridTemplateDataGrid = inject( DATA_GRID_REGISTER_TEMPLATE_INJECTION_KEY, () => ({ additionalContentSlotRef: ref(null), isDataGridTemplate: false, stickyAditionalContent: ref(false), stickyTabs: ref(false), tabsRef: ref(null), }) ); const { additionalContentSlotRef, isDataGridTemplate, stickyTabs, tabsRef, stickyAdditionalContent } = registerDatagridInDataGridTemplate(); /* == Composable logic == */ // Column width calculation const { columnDefs, onColumnWidthOverride, gridRowWidthStyle, isMeasuringColumnWidths, isTotalWidthOfColumnsTakingUpAllAvailableGridWidth, columns: localColumnsState, totalWidthOfAllColumns, rowActionsMaxWidth, } = useCalculateColumnWidth(columns, data, body, columnHeaderRefs, gridcellRefs, { hasColumnPanel: columnPanel, hasSelectableColumn: selectable, hasSelectableAcrossPages: selectableAcrossPages, hasNestedContent, hasNestedRows, allowColumnDragAndDrop, isDataGridTemplate: isDataGridTemplate, }); /* == Emitting logic == */ const definedEmitsFunction = defineEmits([ BentoDatagridEvent.COLUMNS, BentoDatagridEvent.COLUMN_RESIZE, BentoDatagridEvent.ITEMS_PAGE, BentoDatagridEvent.FILTER_SEARCH_INPUT, BentoDatagridEvent.FILTER_SEARCH_CLEAR, BentoDatagridEvent.NAVIGATE, BentoDatagridEvent.NESTED_CONTENT_TOGGLED, BentoDatagridEvent.UPDATE_COLUMNS, BentoDatagridEvent.UPDATE_CONDENSED, BentoDatagridEvent.UPDATE_FILTER_VALUES, BentoDatagridEvent.UPDATE_FILTER_VALUES_OBJECT, BentoDatagridEvent.UPDATE_FILTERS, BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM, BentoDatagridEvent.UPDATE_SHOW_MORE, BentoDatagridEvent.UPDATE_PAGINATION, BentoDatagridEvent.UPDATE_SELECTION, BentoDatagridEvent.UPDATE_SORT_BY, BentoDatagridEvent.ROW_CLICK, BentoDatagridEvent.SELECT, BentoDatagridEvent.SELECT_ALL_ACROSS_PAGES, BentoDatagridEvent.SELECT_ALL_CURRENT_RESULTS, BentoDatagridEvent.DESELECT_ALL_CURRENT_RESULTS, BentoDatagridEvent.SORT, BentoDatagridEvent.CELL_FOCUS, ]); /** * Hooks into the emit function and updates the local columns state if we're updating columns. * This is so if users use a computed ref with the columns property the ordering / toggling visibility will still function correctly. */ const emit: typeof definedEmitsFunction = (event, ...args) => { // If we updating columns update the local columns state if (event === BentoDatagridEvent.COLUMNS || event === BentoDatagridEvent.UPDATE_COLUMNS) { [localColumnsState.value] = args; } definedEmitsFunction(event, ...args); }; // Calculate dynamic top position of sticky filters const stickyFiltersHeight = useCalculateStickyElementHeight( tabsRef, stickyTabs, stickyAdditionalContent, additionalContentSlotRef ); const computedStickyFiltersTopPosition = computed(() => props.stickyFilters ? `${stickyFiltersHeight.value}px` : null ); // Calculate dynamic top position of column headers const columnHeadersHeight = useCalculateStickyElementHeight(filtersRef, toRef(props, 'stickyFilters')); const computedColumnHeadersTopPosition = computed( () => `${columnHeadersHeight.value + stickyFiltersHeight.value}px` ); // Config Settings (condensed, visbility and freezing settings) const { sortedColumns, columnSets, isOnlyOneVisibleColumn, internalZebraRows, onConfigSettingsColumnsUpdate, onConfigSettingsColumnUpdate, onConfigSettingsCondensedUpdate, onConfigSettingsZebraRowsUpdate, } = useConfigSettings(columnDefs, zebraRows, emit); // Column hovering const { hoveredOverColumn, hoveredOverColumnState } = useColumnHovering( columnSets, selectable, outline, isTotalWidthOfColumnsTakingUpAllAvailableGridWidth ); // Re-ordering column (dragging) const { draggingColumns, draggingEventsForColumns, draggingEventsForRowCell, onMoveColumnByOffset } = useColumnReorderer(sortedColumns, allowColumnDragAndDrop, emit); // Selecting const selectableAcrossPagesComputed = computed( () => props.data?.length > 0 && !!props.selectableAcrossPages && !!props.pagination && props.pagination.totalCount > 0 ); const { onAllRowSelect, onRowSelect, onNestedRowSelect, onDeselectAll, allItemsSelectedState, selectedItems, selectionState, } = useSelector( selection, emit, data, props.getDataItemId, props.isDataItemDisabled, selectableAcrossPagesComputed, pagination ); const visibleRowsState = computed<BentoDataGridVisibleRowsState>(() => ({ filtered: filterBarRef.value?.isClearAllVisible || filterBarRef.value?.isResetAllVisible || !!props.filterSearchTerm, allRowsVisible: pagination.value?.size >= pagination.value?.totalCount, })); // Keyboard navigation const { navigationInput } = useKeyboardNavigator( gridcellRefs, columnHeaderRefs, sortedColumns, selectable, computed(() => props.rowActions !== undefined), columnPanel ); // Lazy loading const { isLazyLoad, isInfiniteScroll, verticalScrollPosition, emitLazyLoadEvent } = useLazyLoading( lazyLoadType, verticalScrollPositionRef, body, loading, emit ); // Scrolling const { updateScrollPosition, updateScrollPositionOnFrozenColumnsNavigation, syncVirtualScrollerWithBody, hasLeftOverflow, hasRightOverflow, horizontalScrollbarConditionalStyles, } = useScroller( body, columnsRef, verticalScrollPosition, virtualHorizontalScrollbarRef, paginationContainerRef, totalWidthOfAllColumns, { isLazyLoad, pagination, stickyPagination, isDataGridTemplate, } ); /* == Empty State == */ const onEmptyStateClearResetButtonClick = () => { if (filterBarRef.value.isClearAllVisible) { filterBarRef.value.clearAllFilters(); } else if (filterBarRef.value.isResetAllVisible) { filterBarRef.value.resetAllFilters(); } }; const limitedEmptyStateProps = computed<BentoDatagridEmptyStateProps>(() => ({ ...props.emptyState, variant: props.emptyState?.variant === 'embedded' || props.emptyState?.variant === 'basic' ? props.emptyState.variant : 'basic', })); /* == Styles == */ // Conditional grid classes const conditionalGridClasses = computed(() => ({ [`b-data-grid--${BentoDatagridStyleState.OUTLINE}`]: props.outline, [`b-data-grid--${BentoDatagridStyleState.SCROLL}`]: props.fitContent || isLazyLoad.value, [`b-data-grid--${BentoDatagridStyleState.STICKY_FILTERS}`]: props.filters && props.stickyFilters, [`b-data-grid--${BentoDatagridStyleState.OVERFLOW_SHADOW_LEFT}`]: hasLeftOverflow.value && !frozenColumnsLength.value, [`b-data-grid--${BentoDatagridStyleState.OVERFLOW_SHADOW_RIGHT}`]: hasRightOverflow.value && props.rowActions === undefined, [`b-data-grid--${BentoDatagridStyleState.NO_SPACING_TOP}`]: props.noSpacingTop, [`b-data-grid--${BentoDatagridStyleState.FIT_CONTENT}`]: props.fitContent, [`b-data-grid--${BentoDatagridStyleState.ZEBRA_ROWS}`]: internalZebraRows.value, 'b-data-grid--lazy-load': isLazyLoad.value, })); // Conditional grid body classes const conditionalGridBodyClasses = computed(() => ({ 'b-data-grid__body--sticky-horizontal-scrollbar': props.stickyHorizontalScrollbar, })); const computedColumnPanelAlignment = computed(() => columnSets.value.findIndex(el => el.columnSet.length)); const showFrozenColumnsSeparator = computed(() => { const isSelectionOnlyFrozenColumn = (props.selectable || props.hasNestedContent || props.hasNestedRows) && frozenColumnsLength.value === 1; return !isSelectionOnlyFrozenColumn || hasLeftOverflow.value; }); // Conditional filter classes const conditionalFilterClasses = computed(() => ({ 'b-data-grid__filters--sticky': !!props.stickyFilters || isDataGridTemplate, })); // Conditional pagination classes const conditionalPaginationClasses = computed(() => ({ 'b-data-grid__pagination--padding-left-unset': props.columns?.[0]?.padding?.left === false, 'b-data-grid__pagination--padding-right-unset': props.columns?.[props.columns.length - 1]?.padding?.right === false, 'b-data-grid__pagination--sticky': !!props.stickyPagination || isDataGridTemplate, })); /* == Ref registration methods == */ const registerColumnHeaderRefs = (el: Element | ComponentPublicInstance) => { // We always know this will be used on a DataGridColumns component so we can cast it const datagridColumnsComponent = el as InstanceType<typeof DataGridColumns>; if (el !== null && datagridColumnsComponent?.columnHeaderRefs) { Object.entries(datagridColumnsComponent?.columnHeaderRefs).forEach(([fieldName, colHeaderEl]) => { columnHeaderRefs.value[fieldName] = colHeaderEl as HTMLDivElement; }); } }; const registerRowCellRefs = (el: Element | ComponentPublicInstance, rowIndex: number) => { // We always know this will be used on a DataGridCell component so we can cast it const gridRowComponent = el as InstanceType<typeof DataGridRow>; if (el !== null) { if (gridRowComponent?.gridcellRefs) { const areRowActions = props.rowActions !== undefined && gridRowComponent?.rowActionsRef; Object.entries(gridRowComponent?.gridcellRefs).map(([field, cell]) => { if (gridcellRefs.value?.[field] === undefined) { gridcellRefs.value = { ...gridcellRefs.value, [field]: [] }; } gridcellRefs.value[field][rowIndex] = cell as HTMLDivElement; }); // This calculates the maxium width of the row actions column so we can add the same padding to the columns keep everything aligned if (areRowActions) { if (gridcellRefs.value?.[DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME] === undefined) { gridcellRefs.value = { ...gridcellRefs.value, [DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME]: [] }; } gridcellRefs.value[DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME][rowIndex] = gridRowComponent .rowActionsRef?.[0] as HTMLDivElement; rowActionsMaxWidth.value = Math.max( rowActionsMaxWidth.value, gridRowComponent.rowActionsRef?.[0]?.offsetWidth ?? 0 ); } } } }; /* == Row methods == */ // Row selecting const onRowClick = (row: BentoDatagridDataItem, event: MouseEvent) => { if (props.allowRowClicks) { emit(BentoDatagridEvent.ROW_CLICK, row, event); } }; // Programmatically set the hover state on a row const onCellHover = (rowIndex: number) => { hoveredRowIndex.value = rowIndex; }; /* == a11y == */ const ariaGridRole = computed(() => (props.hasNestedContent || props.hasNestedRows ? 'treegrid' : 'grid')); /* == Action bar == */ const itemsSelectedCounter = computed(() => selectedItems.value?.length ?? 0); const showActionBar = computed(() => props.bulkActions && itemsSelectedCounter.value > 0); /* == Emits & Events == */ // Resizing const onColumnResized = (resizeEvent: { field: string; width: number }) => emit(BentoDatagridEvent.COLUMN_RESIZE, resizeEvent); // Sorting const onSort = (updatedSortBy: Array<BentoDatagridSortByColumn>) => { emit(BentoDatagridEvent.UPDATE_SORT_BY, updatedSortBy); emit(BentoDatagridEvent.SORT, updatedSortBy); }; // Pagination const onPaginationNavigate = (page: number, size: number) => { emit(BentoDatagridEvent.NAVIGATE, page); emit(BentoDatagridEvent.UPDATE_PAGINATION, { ...props.pagination, size, page, }); }; const onItemsPerPageChange = (itemsPerPage: number) => { emit(BentoDatagridEvent.ITEMS_PAGE, itemsPerPage); }; // Filter bar const onSearchTermCleared = () => { emit(BentoDatagridEvent.FILTER_SEARCH_CLEAR); }; const onSearchTermInput = newSearchTerm => { emit(BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM, newSearchTerm); emit(BentoDatagridEvent.FILTER_SEARCH_INPUT, newSearchTerm); }; /* == Warnings == */ const hasMandatoryColumn = computed(() => columns.value.some(column => column.mandatory)); if (!hasMandatoryColumn.value) { printDevelopmentWarning( 'WARNING: At least one BentoDataGrid column should be set as mandatory to ensure data is always displayed correctly.' ); } /* == Provide/inject == */ const setTitle: BentoTabProvidedTitle = inject(EXTERNAL_TITLE_REGISTER_ENTRY_INJECTION_KEY, () => null); setTitle(props.tabTitle); provide(DATA_GRID_SLASHED_ZERO, toRef(props, 'slashedZero')); /* == Expose methods == */ defineExpose({ onEmptyStateClearResetButtonClick }); </script> <script lang="ts"> /** * A component to display data in columns and row format with a whole array of configuration for visualization and interaction. * * @example * import { BentoDataGrid } from '@adyen/bento-vue2'; * * export default { * components: { BentoDataGrid }, * template: ` * <bento-data-grid * :data="[ * { id: '123', name: 'John', company: 'TestCompany', 'payment': 200 } * { id: '321', name: 'Jane', company: 'Adyen', 'payment': 300 } * ]" * :columns="[ * { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, * { field: 'company', label: 'Reference' }, * { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, * ]" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-data-grid', }; </script> <style lang="scss" scoped src="./data-grid.scss" />
|
|
1
|
+
<template> <div ref="dataGridRef" class="b-data-grid" data-testid="data-grid" :class="conditionalGridClasses" aria-live="polite" :aria-busy="loading" @copy="handleClipboard" > <div v-if="filters" ref="filtersRef" class="b-data-grid__filters" :class="conditionalFilterClasses" :style="{ top: computedStickyFiltersTopPosition }" :aria-controls="dataGridUid" :aria-label="t('filterBar')" role="region" > <bento-filter-bar v-if="filters" ref="filterBarRef" :config="filters" :container-size-layout="filtersContainerSizeLayout" :filter-values="filterValues" :filter-values-object="filterValuesObject" :search="filterSearchConfig" :search-term="filterSearchTerm" @search-input="onSearchTermInput" @search-clear="onSearchTermCleared" @input="filters => $emit(BentoDatagridEvent.UPDATE_FILTERS, filters)" @update:filter-values="filterValues => $emit(BentoDatagridEvent.UPDATE_FILTER_VALUES, filterValues)" @update:filter-values-object=" filterValueObject => $emit(BentoDatagridEvent.UPDATE_FILTER_VALUES_OBJECT, filterValueObject) " /> </div> <div v-if="$slots.header" class="b-data-grid__header"> <slot name="header"></slot> </div> <div :id="dataGridUid" :role="ariaGridRole" :aria-rowcount="data.length" :aria-colcount="columns.length" :aria-label="emptyState ? t('noResults') : null" class="b-data-grid__wrapper" :style="{ top: computedColumnHeadersTopPosition }" > <div class="b-data-grid__column-headers" :style="{ top: computedColumnHeadersTopPosition }"> <div ref="columnsRef" role="rowgroup" class="b-data-grid__columns-row" @scroll="updateScrollPosition" @keydown="navigationInput" > <div v-if="columnPanel" role="row"> <bento-data-grid-config-settings class="b-data-grid__config-settings" role="gridcell" :columns="localColumnsState" :condensed="condensed" :zebra-rows="internalZebraRows" @update:condensed="onConfigSettingsCondensedUpdate" @update:zebra-rows="onConfigSettingsZebraRowsUpdate" @update:columns="onConfigSettingsColumnsUpdate" /> </div> <div :style="gridRowWidthStyle" class="b-data-grid__columns" role="row"> <template v-for="({ columnSet, id, isFrozenColumns }, index) of columnSets"> <data-grid-columns v-if="columnSet?.length" :key="`data-grid-columns-${id}`" :ref="el => registerColumnHeaderRefs(el)" :columns="columnSet" :has-resizable-columns="hasResizableColumns" :sort-by="sortBy" :all-items-selected-state="allItemsSelectedState" :is-frozen-columns="isFrozenColumns" :show-frozen-columns-separator="showFrozenColumnsSeparator" :dragging-columns="draggingColumns" :has-draggable-columns="allowColumnDragAndDrop" :has-column-panel="columnPanel" :has-left-overflow="hasLeftOverflow" :has-first-column-panel-right-aligned="index === computedColumnPanelAlignment" :hovered-over-column-state="hoveredOverColumnState" :is-measuring-column-widths="isMeasuringColumnWidths" :is-only-one-visible-column="isOnlyOneVisibleColumn" :selectable="data?.length > 0 && selectable" :selectable-across-pages="selectableAcrossPagesComputed" :selected-items="selectedItems" :visible-rows-state="visibleRowsState" :selection-state="selectionState" :truncate-column-headers="truncateColumnHeaders" :row-actions-maximum-width="rowActionsMaxWidth" v-on="draggingEventsForColumns" @column-hover="hoveredOverColumn = $event" @column-move="onMoveColumnByOffset" @column-update="onConfigSettingsColumnUpdate" @column-resize="onColumnResized" @column-resizing="onColumnWidthOverride" @select-all-rows="onAllRowSelect" @select-all-across-pages="emit(BentoDatagridEvent.SELECT_ALL_ACROSS_PAGES)" @select-all-current-results="emit(BentoDatagridEvent.SELECT_ALL_CURRENT_RESULTS)" @deselect-all-current-results="emit(BentoDatagridEvent.DESELECT_ALL_CURRENT_RESULTS)" @deselect-all="onDeselectAll" @sort="onSort" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-columns> </template> </div> </div> </div> <div ref="body" data-testid="data-grid-body" class="b-data-grid__body" :class="conditionalGridBodyClasses" @scroll="updateScrollPosition" > <div v-if="loading && !isLazyLoad" class="b-data-grid__loading"> <bento-loading-indicator class="b-data-grid__loading-indicator" large /> <bento-typography el="span" variant="caption">{{ t('gettingYourData') }}</bento-typography> </div> <div v-else role="rowgroup" @keydown="navigationInput"> <template v-if="data?.length === 0 && emptyState"> <div role="row" tabindex="-1"> <div role="gridcell" :aria-colspan="columns.length" tabindex="-1"> <bento-empty-state class="b-data-grid__empty-state" v-bind="limitedEmptyStateProps" /> </div> </div> </template> <data-grid-row v-for="(rowData, rowIndex) in data" v-else :key="rowIndex" :ref="el => registerRowCellRefs(el, rowIndex)" :style="gridRowWidthStyle" :allow-row-clicks="allowRowClicks" :column-sets="columnSets" :condensed="condensed" :dragging-columns="draggingColumns" :dragging-events-for-row-cell="draggingEventsForRowCell" :get-data-item-id="getDataItemId" :has-nested-content="hasNestedContent" :has-nested-rows="hasNestedRows" :has-left-overflow="hasLeftOverflow" :has-right-overflow="hasRightOverflow" :highlighted-rows="highlightedRows" :hovered-row-index="hoveredRowIndex" :hovered-over-column-state="hoveredOverColumnState" :is-data-item-disabled="isDataItemDisabled" :is-row-even="internalZebraRows ? rowIndex % 2 !== 0 : false" :is-row-last-child="rowIndex + 1 === data?.length" :is-measuring-column-widths="isMeasuringColumnWidths" :row-actions-maximum-width="rowActionsMaxWidth" :row-actions="rowActions" :row-data="rowData" :row-index="rowIndex" :row-set-size="data?.length" :show-frozen-columns-separator="showFrozenColumnsSeparator" :should-show-nested-content="shouldShowNestedContent" :should-show-nested-rows="shouldShowNestedRows" :selectable="selectable" :selected-items="selectedItems" @cell-hovered="onCellHover" @cell-focus="emit(BentoDatagridEvent.CELL_FOCUS, $event)" @nested-content-toggled="onNestedContentToggled($event)" @row-click="onRowClick" @on-frozen-columns-key-press="updateScrollPositionOnFrozenColumnsNavigation" @select-row="onRowSelect" @select-nested-row="onNestedRowSelect" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-row> </div> </div> </div> <data-grid-lazy-load v-if="isLazyLoad" :loading="loading" :is-infinite-scroll="isInfiniteScroll" @update:show-more="emitLazyLoadEvent" /> <div v-if="stickyHorizontalScrollbar" ref="virtualHorizontalScrollbarRef" class="b-data-grid__horizontal-scrollbar" aria-hidden="true" data-testid="sticky-horizontal-scrollbar" :style="horizontalScrollbarConditionalStyles" @scroll="syncVirtualScrollerWithBody" > <div :style="{ width: `${totalWidthOfAllColumns}px`, height: '15px' }"></div> </div> <div v-if="pagination && !isLazyLoad" ref="paginationContainerRef" data-testid="pagination-container" class="b-data-grid__pagination" :class="conditionalPaginationClasses" > <bento-pagination v-bind="pagination" :aria-label="$t('pagination')" :aria-controls="dataGridUid" @navigate="onPaginationNavigate" @items-page="onItemsPerPageChange" > <slot v-if="hasSlot('pagination-slot')" name="pagination-slot"></slot> </bento-pagination> </div> <div v-if="$slots.footer" class="b-data-grid__footer"><slot name="footer"></slot></div> <bento-action-bar v-if="showActionBar" :actions="bulkActions" :item-counter="itemsSelectedCounter" :extra-bottom-spacing="stickyPagination" @close="onDeselectAll" /> </div> </template> <script lang="ts" setup> import { type ComponentPublicInstance, computed, inject, nextTick, onMounted, type PropType, provide, ref, toRef, toRefs, useSlots, } from 'vue'; // Components import { BentoActionBar } from '@/components/action-bar'; import { BentoPagination } from '@/components/pagination'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoTypography } from '@/components/typography'; import { BentoDataGridConfigSettings, DataGridColumns, DataGridLazyLoad, DataGridRow } from './components'; // Composables import { useCalculateColumnWidth, useColumnHovering, useColumnReorderer, useConfigSettings, useKeyboardNavigator, useLazyLoading, useScroller, useSelector, } from './composables'; import { useRowCellRefs } from './composables/use-row-cell-refs/use-row-cell-refs'; import { useCalculateStickyElementHeight, useHasSlot } from '@/composables'; // Utils import { generateUid } from '@/core/utils/ts/generate-uid'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { handleClipboard } from '@/components/data-grid/utils/handle-clipboard/handle-clipboard'; // Types import { type BentoColumn, type BentoDatagridData, type BentoDatagridDataItem, type BentoDatagridDataItemId, type BentoDatagridEmptyStateProps, BentoDatagridEvent, type BentoDatagridGetDataItemIdFn, type BentoDatagridIsDataItemDisabledFn, type BentoDatagridPaginationProps, type BentoDatagridShouldShowNestedContentFn, type BentoDatagridShouldShowNestedRowsFn, type BentoDatagridSortByColumn, BentoDatagridStyleState, type BentoDataGridVisibleRowsState, } from './data-grid.types'; import { type BentoTabProvidedTitle } from '../tabs/components/tab.types'; import { type BentoDataGridTemplateDataGrid } from '@/templates/data-grid-template/data-grid-template.types'; import { type BentoLazyLoadType } from '@/types/lazy-load'; import type { BentoButtonActionsList } from '@/components/button'; import BentoFilterBar from '@/components/filter-bar/filter-bar.vue'; import { type BentoFilterBarContainerSizeLayout, type BentoFilterBarModel, type BentoFilterBarSearchBarProps, type BentoFilterBarSearchTerm, type BentoFilterValueObject, type BentoFilterValues, } from '@/components/filter-bar'; import { BentoEmptyState } from '@/components/empty-state'; import type { DatagridColumnsHeaderReferences } from '@/components/data-grid/components/data-grid-columns/data-grid-columns.types'; import type { BentoDataGridRowActionsProp } from '@/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.types'; // Keys import { EXTERNAL_TITLE_REGISTER_ENTRY_INJECTION_KEY } from '@/components/tabs/tabs.keys'; import { DATA_GRID_REGISTER_TEMPLATE_INJECTION_KEY } from '@/templates/data-grid-template/data-grid-template.keys'; import { DATA_GRID_SLASHED_ZERO, DATA_GRID_SMALL_CONTAINER, DATA_GRID_TOUCH_DEVICE } from './data-grid.keys'; // Translations import messages from './messages.json'; import { useElementSize } from '@vueuse/core'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * When true, will emit a 'rowClick' event when a row is clicked. Will add cursor: pointer to rows. */ allowRowClicks: { type: Boolean, default: false }, /** * When true, column headers will become draggable, allowing users to rearrange columns. */ allowColumnDragAndDrop: { type: Boolean, default: true }, /** * List of actions that will be rendered in the action bar when user selects items. * * Each object in the list should have a `title`, an `event` and, * optionally, an `icon`. * * The component will display 4 actions and the hide the rest inside a menu. * * @see BentoButton for a list of all the other props supported by each button action. */ bulkActions: { type: Array as PropType<BentoButtonActionsList>, default: null, }, /** * Sets the row cells to the condensed padding state. */ condensed: { type: Boolean, default: false, }, /** * Column definitions using 'key' to relate to data object. */ columns: { type: Array as PropType<Array<BentoColumn>>, required: true, }, /** * Enables / hides the display options panel and the column contextual menu functionality. */ columnPanel: { type: Boolean, default: false }, /** * An array of data objects to display. */ data: { type: Array as PropType<BentoDatagridData>, required: true, }, /** * Empty state props that will be passed down to inner empty-state component */ emptyState: { type: Object as PropType<BentoDatagridEmptyStateProps>, default: undefined, }, /** * filter-bar value (i.e. filters) that will be passed down to inner filter-bar component * {@see BentoFilterBarModel} */ filters: { // TODO: Change this to BentoFilterBarProps['value'] when the interface is available type: Array as PropType<BentoFilterBarModel>, default: undefined, }, /** * Defines the layout (multi-line or one-line) to be used in the filter bar for different * container sizes: * - Large (>=700px): Always multi-line * - Medium (>=430px & <700px) * - Small (<430px) * * @default * { * medium: 'multi-line', * small: 'one-line' * } */ filtersContainerSizeLayout: { type: Object as PropType<BentoFilterBarContainerSizeLayout>, default: undefined, }, /** * An array of the values for each filter set in `value`. * {@see BentoFilterValues} */ filterValues: { type: Array as PropType<BentoFilterValues>, default: () => [], }, /** * An object of the values for each filter in the filter bar where key is a the field name and the value is the BentoFilterBarValue. * * @example * { * 'company': '200', * 'name': 'some text' * }, */ filterValuesObject: { type: Object as PropType<BentoFilterValueObject>, default: () => undefined, }, // TODO: Move this prop as a part of "config" BentoFilterBarModel = Array<BentoFilterModel | BentoFilterSearchModel> in Bento 2.0. // It was not done now because it's a breaking change for the types. /** * Filter's search bar properties with which the placeholder, hint and other * "bento-search-bar" properties can be set. @see {@link BentoFilterBarSearchBarProps} */ filterSearchConfig: { type: Object as PropType<BentoFilterBarSearchBarProps>, default: undefined, }, /** * Filter bar search term useful to filter all the data by text */ filterSearchTerm: { type: String as PropType<BentoFilterBarSearchTerm>, default: undefined, }, /** * Allows the grid to be rendered inside a confined space. * This makes the data-grid expand to fill the height of its parent container, with the grid body becoming scrollable. */ fitContent: { type: Boolean, default: false }, /** * 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, }, /** * Row actions propery that generates which button to show in row actions area */ rowActions: { type: [Function, Array] as PropType<BentoDataGridRowActionsProp>, default: undefined, }, /** * If resize is enabled */ hasResizableColumns: { type: Boolean, default: true, }, /** * Enables the nested content slot functionality. */ hasNestedContent: { type: Boolean, default: false, }, /** * Enables the nested content slot functionality. */ hasNestedRows: { type: Boolean, default: false, }, /** * Highlighted items */ highlightedRows: { type: Array as PropType<Array<BentoDatagridDataItemId>>, default: () => [], }, /** * 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, }, /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button * @values automatic, button, none */ lazyLoadType: { type: String as PropType<BentoLazyLoadType>, default: 'none', validator: (value: BentoLazyLoadType) => ['automatic', 'button', 'none'].includes(value), }, /** * If the loading indicator should be shown. */ loading: { type: Boolean, default: false, }, /** * Enables / Disables the table's surrounding border. */ outline: { type: Boolean, default: false, }, /** * Renders the data grid with no top spacing, to render inside containers that already have spacing */ noSpacingTop: { type: Boolean, default: false, }, /** * Pagination props that will be passed down to inner pagination component */ pagination: { type: Object as PropType<BentoDatagridPaginationProps>, default: undefined, }, /** * Adds an extra option when selecting all items that allows user to select items in other pages too. * Needs pagination.totalCount to be set. */ selectableAcrossPages: { type: Boolean, default: false, }, /** * Renders a font numeric style with slashed zeros instead of normal ones. */ slashedZero: { type: Boolean, default: false }, /** * Sticks the horizontal scrollbar to the bottom of the screen if the data-grid is larger than the screen height. */ stickyHorizontalScrollbar: { type: Boolean, default: false, }, /** * Make pagination stick to the bottom of the page when the data-grid is bigger than the screen. * Because styles have to be applied to pagination container element, this has to be a data-grid prop (rather than a pagination prop). */ stickyPagination: { type: Boolean, default: false, }, /** * Make data-grid filters stick to the top of the page along with the data-grid headers. * Defaults to true. */ stickyFilters: { type: Boolean, default: true, }, /** * Enables / Disables rows to be selected via a checkbox. On select the "select" event will be emitted. */ selectable: { type: Boolean, default: false, }, /** * Selected items */ selection: { type: Array as PropType<Array<BentoDatagridDataItemId>>, default: () => [], }, /** * 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, }, /** * Sortby handles proper rendering of sort icon in header. Sorting currently is only supported server side. * It does make sure the currently sorted column has a more visible sort icon, and the sort icon points in the right direction. */ sortBy: { type: Array as PropType<Array<BentoDatagridSortByColumn>>, // NOTE: is an array to allow multi-sorting in the future default: () => [], }, /** * Title of the data grid to be rendered as tab title when a data grid is passed in a tab layout. */ tabTitle: { type: String, default: null, }, /** * Truncates all data grid column headers */ truncateColumnHeaders: { type: Boolean, default: false, }, /** * Toggles the alternating background color of rows. */ zebraRows: { type: Boolean, default: false, }, }); const slots = useSlots(); const hasSlot = useHasSlot(slots); /* == Variables and references == */ // Create refs for elements const body = ref<HTMLDivElement>(null); const columnsRef = ref<HTMLDivElement>(); const columnHeaderRefs = ref<DatagridColumnsHeaderReferences>({}); const dataGridUid = ref<string>(generateUid('data-grid')); const gridcellRefs = ref<{ [key: string]: Array<HTMLDivElement> }>(null); const filtersRef = ref<HTMLDivElement>(null); const filterBarRef = ref<InstanceType<typeof BentoFilterBar>>(null); const paginationContainerRef = ref<HTMLDivElement>(null); const verticalScrollPositionRef = ref(null); const virtualHorizontalScrollbarRef = ref<HTMLDivElement>(null); const { allowColumnDragAndDrop, columns, columnPanel, data, lazyLoadType, loading, hasNestedContent, hasNestedRows, selectable, selection, selectableAcrossPages, stickyPagination, outline, pagination, zebraRows, } = toRefs(props); const hoveredRowIndex = ref(-1); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const frozenColumnsLength = computed(() => columnDefs.value.filter(column => column.frozen).length); /* == Responsive behavior == */ // Identifies if the data-grid is in a container smaller than 550px const MOBILE_BREAKPOINT = 550; const dataGridRef = ref(); const { width } = useElementSize(dataGridRef); const isSmallContainer = computed(() => width.value <= MOBILE_BREAKPOINT); provide(DATA_GRID_SMALL_CONTAINER, isSmallContainer); /* == Touch device behavior == */ // Identifies if the data-grid is rendered on a touch device const isTouchDevice = ref(false); onMounted(() => { if (typeof window !== 'undefined') { isTouchDevice.value = 'ontouchstart' in window || navigator?.maxTouchPoints > 0; } }); provide(DATA_GRID_TOUCH_DEVICE, isTouchDevice); /* == Data-grid template == */ const registerDatagridInDataGridTemplate: BentoDataGridTemplateDataGrid = inject( DATA_GRID_REGISTER_TEMPLATE_INJECTION_KEY, () => ({ additionalContentSlotRef: ref(null), isDataGridTemplate: false, stickyAditionalContent: ref(false), stickyTabs: ref(false), tabsRef: ref(null), }) ); const { additionalContentSlotRef, isDataGridTemplate, stickyTabs, tabsRef, stickyAdditionalContent } = registerDatagridInDataGridTemplate(); /* == Composable logic == */ // Column width calculation const { columnDefs, onColumnWidthOverride, gridRowWidthStyle, isMeasuringColumnWidths, isTotalWidthOfColumnsTakingUpAllAvailableGridWidth, columns: localColumnsState, totalWidthOfAllColumns, rowActionsMaxWidth, } = useCalculateColumnWidth(columns, data, body, columnHeaderRefs, gridcellRefs, { hasColumnPanel: columnPanel, hasSelectableColumn: selectable, hasSelectableAcrossPages: selectableAcrossPages, hasNestedContent, hasNestedRows, allowColumnDragAndDrop, isDataGridTemplate: isDataGridTemplate, }); const hasRowActions = computed(() => props.rowActions !== undefined); const { registerRowCellRefs, rebuildGridcellRefs } = useRowCellRefs( gridcellRefs, rowActionsMaxWidth, hasRowActions ); /* == Emitting logic == */ const definedEmitsFunction = defineEmits([ BentoDatagridEvent.COLUMNS, BentoDatagridEvent.COLUMN_RESIZE, BentoDatagridEvent.ITEMS_PAGE, BentoDatagridEvent.FILTER_SEARCH_INPUT, BentoDatagridEvent.FILTER_SEARCH_CLEAR, BentoDatagridEvent.NAVIGATE, BentoDatagridEvent.NESTED_CONTENT_TOGGLED, BentoDatagridEvent.UPDATE_COLUMNS, BentoDatagridEvent.UPDATE_CONDENSED, BentoDatagridEvent.UPDATE_FILTER_VALUES, BentoDatagridEvent.UPDATE_FILTER_VALUES_OBJECT, BentoDatagridEvent.UPDATE_FILTERS, BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM, BentoDatagridEvent.UPDATE_SHOW_MORE, BentoDatagridEvent.UPDATE_PAGINATION, BentoDatagridEvent.UPDATE_SELECTION, BentoDatagridEvent.UPDATE_SORT_BY, BentoDatagridEvent.ROW_CLICK, BentoDatagridEvent.SELECT, BentoDatagridEvent.SELECT_ALL_ACROSS_PAGES, BentoDatagridEvent.SELECT_ALL_CURRENT_RESULTS, BentoDatagridEvent.DESELECT_ALL_CURRENT_RESULTS, BentoDatagridEvent.SORT, BentoDatagridEvent.CELL_FOCUS, ]); /** * Hooks into the emit function and updates the local columns state if we're updating columns. * This is so if users use a computed ref with the columns property the ordering / toggling visibility will still function correctly. */ const emit: typeof definedEmitsFunction = (event, ...args) => { // If we updating columns update the local columns state if (event === BentoDatagridEvent.COLUMNS || event === BentoDatagridEvent.UPDATE_COLUMNS) { [localColumnsState.value] = args; } definedEmitsFunction(event, ...args); }; // Calculate dynamic top position of sticky filters const stickyFiltersHeight = useCalculateStickyElementHeight( tabsRef, stickyTabs, stickyAdditionalContent, additionalContentSlotRef ); const computedStickyFiltersTopPosition = computed(() => props.stickyFilters ? `${stickyFiltersHeight.value}px` : null ); // Calculate dynamic top position of column headers const columnHeadersHeight = useCalculateStickyElementHeight(filtersRef, toRef(props, 'stickyFilters')); const computedColumnHeadersTopPosition = computed( () => `${columnHeadersHeight.value + stickyFiltersHeight.value}px` ); // Config Settings (condensed, visbility and freezing settings) const { sortedColumns, columnSets, isOnlyOneVisibleColumn, internalZebraRows, onConfigSettingsColumnsUpdate, onConfigSettingsColumnUpdate, onConfigSettingsCondensedUpdate, onConfigSettingsZebraRowsUpdate, } = useConfigSettings(columnDefs, zebraRows, emit); // Column hovering const { hoveredOverColumn, hoveredOverColumnState } = useColumnHovering( columnSets, selectable, outline, isTotalWidthOfColumnsTakingUpAllAvailableGridWidth ); // Re-ordering column (dragging) const { draggingColumns, draggingEventsForColumns, draggingEventsForRowCell, onMoveColumnByOffset } = useColumnReorderer(sortedColumns, allowColumnDragAndDrop, emit); // Selecting const selectableAcrossPagesComputed = computed( () => props.data?.length > 0 && !!props.selectableAcrossPages && !!props.pagination && props.pagination.totalCount > 0 ); const { onAllRowSelect, onRowSelect, onNestedRowSelect, onDeselectAll, allItemsSelectedState, selectedItems, selectionState, } = useSelector( selection, emit, data, props.getDataItemId, props.isDataItemDisabled, selectableAcrossPagesComputed, pagination ); const visibleRowsState = computed<BentoDataGridVisibleRowsState>(() => ({ filtered: filterBarRef.value?.isClearAllVisible || filterBarRef.value?.isResetAllVisible || !!props.filterSearchTerm, allRowsVisible: pagination.value?.size >= pagination.value?.totalCount, })); // Keyboard navigation const { navigationInput } = useKeyboardNavigator( gridcellRefs, columnHeaderRefs, sortedColumns, selectable, hasRowActions, columnPanel ); // Lazy loading const { isLazyLoad, isInfiniteScroll, verticalScrollPosition, emitLazyLoadEvent } = useLazyLoading( lazyLoadType, verticalScrollPositionRef, body, loading, emit ); // Scrolling const { updateScrollPosition, updateScrollPositionOnFrozenColumnsNavigation, syncVirtualScrollerWithBody, hasLeftOverflow, hasRightOverflow, horizontalScrollbarConditionalStyles, } = useScroller( body, columnsRef, verticalScrollPosition, virtualHorizontalScrollbarRef, paginationContainerRef, totalWidthOfAllColumns, { isLazyLoad, pagination, stickyPagination, isDataGridTemplate, } ); /* == Empty State == */ const onEmptyStateClearResetButtonClick = () => { if (filterBarRef.value.isClearAllVisible) { filterBarRef.value.clearAllFilters(); } else if (filterBarRef.value.isResetAllVisible) { filterBarRef.value.resetAllFilters(); } }; const limitedEmptyStateProps = computed<BentoDatagridEmptyStateProps>(() => ({ ...props.emptyState, variant: props.emptyState?.variant === 'embedded' || props.emptyState?.variant === 'basic' ? props.emptyState.variant : 'basic', })); /* == Styles == */ // Conditional grid classes const conditionalGridClasses = computed(() => ({ [`b-data-grid--${BentoDatagridStyleState.OUTLINE}`]: props.outline, [`b-data-grid--${BentoDatagridStyleState.SCROLL}`]: props.fitContent || isLazyLoad.value, [`b-data-grid--${BentoDatagridStyleState.STICKY_FILTERS}`]: props.filters && props.stickyFilters, [`b-data-grid--${BentoDatagridStyleState.OVERFLOW_SHADOW_LEFT}`]: hasLeftOverflow.value && !frozenColumnsLength.value, [`b-data-grid--${BentoDatagridStyleState.OVERFLOW_SHADOW_RIGHT}`]: hasRightOverflow.value && props.rowActions === undefined, [`b-data-grid--${BentoDatagridStyleState.NO_SPACING_TOP}`]: props.noSpacingTop, [`b-data-grid--${BentoDatagridStyleState.FIT_CONTENT}`]: props.fitContent, [`b-data-grid--${BentoDatagridStyleState.ZEBRA_ROWS}`]: internalZebraRows.value, 'b-data-grid--lazy-load': isLazyLoad.value, })); // Conditional grid body classes const conditionalGridBodyClasses = computed(() => ({ 'b-data-grid__body--sticky-horizontal-scrollbar': props.stickyHorizontalScrollbar, })); const computedColumnPanelAlignment = computed(() => columnSets.value.findIndex(el => el.columnSet.length)); const showFrozenColumnsSeparator = computed(() => { const isSelectionOnlyFrozenColumn = (props.selectable || props.hasNestedContent || props.hasNestedRows) && frozenColumnsLength.value === 1; return !isSelectionOnlyFrozenColumn || hasLeftOverflow.value; }); // Conditional filter classes const conditionalFilterClasses = computed(() => ({ 'b-data-grid__filters--sticky': !!props.stickyFilters || isDataGridTemplate, })); // Conditional pagination classes const conditionalPaginationClasses = computed(() => ({ 'b-data-grid__pagination--padding-left-unset': props.columns?.[0]?.padding?.left === false, 'b-data-grid__pagination--padding-right-unset': props.columns?.[props.columns.length - 1]?.padding?.right === false, 'b-data-grid__pagination--sticky': !!props.stickyPagination || isDataGridTemplate, })); /* == Ref registration methods == */ const registerColumnHeaderRefs = (el: Element | ComponentPublicInstance) => { // We always know this will be used on a DataGridColumns component so we can cast it const datagridColumnsComponent = el as InstanceType<typeof DataGridColumns>; if (el !== null && datagridColumnsComponent?.columnHeaderRefs) { Object.entries(datagridColumnsComponent?.columnHeaderRefs).forEach(([fieldName, colHeaderEl]) => { columnHeaderRefs.value[fieldName] = colHeaderEl as HTMLDivElement; }); } }; /* == Row methods == */ // Row selecting const onRowClick = (row: BentoDatagridDataItem, event: MouseEvent) => { if (props.allowRowClicks) { emit(BentoDatagridEvent.ROW_CLICK, row, event); } }; const onNestedContentToggled = (rowData: BentoDatagridDataItem) => { emit(BentoDatagridEvent.NESTED_CONTENT_TOGGLED, rowData); nextTick(() => rebuildGridcellRefs()); }; // Programmatically set the hover state on a row const onCellHover = (rowIndex: number) => { hoveredRowIndex.value = rowIndex; }; /* == a11y == */ const ariaGridRole = computed(() => (props.hasNestedContent || props.hasNestedRows ? 'treegrid' : 'grid')); /* == Action bar == */ const itemsSelectedCounter = computed(() => selectedItems.value?.length ?? 0); const showActionBar = computed(() => props.bulkActions && itemsSelectedCounter.value > 0); /* == Emits & Events == */ // Resizing const onColumnResized = (resizeEvent: { field: string; width: number }) => emit(BentoDatagridEvent.COLUMN_RESIZE, resizeEvent); // Sorting const onSort = (updatedSortBy: Array<BentoDatagridSortByColumn>) => { emit(BentoDatagridEvent.UPDATE_SORT_BY, updatedSortBy); emit(BentoDatagridEvent.SORT, updatedSortBy); }; // Pagination const onPaginationNavigate = (page: number, size: number) => { emit(BentoDatagridEvent.NAVIGATE, page); emit(BentoDatagridEvent.UPDATE_PAGINATION, { ...props.pagination, size, page, }); }; const onItemsPerPageChange = (itemsPerPage: number) => { emit(BentoDatagridEvent.ITEMS_PAGE, itemsPerPage); }; // Filter bar const onSearchTermCleared = () => { emit(BentoDatagridEvent.FILTER_SEARCH_CLEAR); }; const onSearchTermInput = newSearchTerm => { emit(BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM, newSearchTerm); emit(BentoDatagridEvent.FILTER_SEARCH_INPUT, newSearchTerm); }; /* == Warnings == */ const hasMandatoryColumn = computed(() => columns.value.some(column => column.mandatory)); if (!hasMandatoryColumn.value) { printDevelopmentWarning( 'WARNING: At least one BentoDataGrid column should be set as mandatory to ensure data is always displayed correctly.' ); } /* == Provide/inject == */ const setTitle: BentoTabProvidedTitle = inject(EXTERNAL_TITLE_REGISTER_ENTRY_INJECTION_KEY, () => null); setTitle(props.tabTitle); provide(DATA_GRID_SLASHED_ZERO, toRef(props, 'slashedZero')); /* == Expose methods == */ defineExpose({ onEmptyStateClearResetButtonClick }); </script> <script lang="ts"> /** * A component to display data in columns and row format with a whole array of configuration for visualization and interaction. * * @example * import { BentoDataGrid } from '@adyen/bento-vue2'; * * export default { * components: { BentoDataGrid }, * template: ` * <bento-data-grid * :data="[ * { id: '123', name: 'John', company: 'TestCompany', 'payment': 200 } * { id: '321', name: 'Jane', company: 'Adyen', 'payment': 300 } * ]" * :columns="[ * { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, * { field: 'company', label: 'Reference' }, * { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, * ]" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-data-grid', }; </script> <style lang="scss" scoped src="./data-grid.scss" />
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export enum BentoDateFormat { // MONTH SHORT_MONTH = 'LLL', // Jun // DAY DAY_OF_MONTH = 'd', // 1, 2, ..., 31, LONG_WEEKDAY = 'EEEE', // Friday // TIME SHORT_TIME_WITH_PERIOD = `h:mm aa`, // 2:44 PM SHORT_TIME_WITHOUT_PERIOD = 'H:mm', // 14:45 EXACT_TIME_WITH_PERIOD = 'h:mm:ss aa', // 2:05:11 PM EXACT_TIME_WITHOUT_PERIOD = 'H:mm:ss', // 14:05:11 // DATE LONG_DATE = 'LLLL dd, yyyy', // June 30, 2023 SHORT_DATE = 'LLL dd, yyyy', // Jun 30, 2023 FULL_DATE_EXACT_TIME_WITH_PERIOD = `${SHORT_DATE}, h:mm:ss aa`, // June 30, 2023, 2:54:10 PM FULL_DATE_EXACT_TIME_WITHOUT_PERIOD = `${SHORT_DATE}, H:mm:ss`, // June 30, 2023, 14:54:42 FULL_DATE_EXACT_TIME_WITH_TIMEZONE_AND_PERIOD = `${SHORT_DATE}, h:mm:ss aa O`, // June 30, 2023, 2:56:07 PM GMT+2 FULL_DATE_EXACT_TIME_WITH_TIMEZONE_AND_WITHOUT_PERIOD = `${SHORT_DATE}, H:mm:ss O`, // June 30, 2023, 14:56:28 GMT+2 FULL_DATE_TIME_WITH_PERIOD = `${SHORT_DATE}, h:mm aa`, // Jun 30, 2023, 2:48 PM FULL_DATE_TIME_WITH_TIMEZONE_AND_PERIOD = `${SHORT_DATE}, h:mm aa O`, // Jun 30, 2023, 2:44 PM GMT+2 FULL_DATE_TIME_WITHOUT_PERIOD = `${SHORT_DATE}, H:mm`, // Jun 30, 2023, 14:48 FULL_DATE_TIME_WITH_TIMEZONE_AND_WITHOUT_PERIOD = `${SHORT_DATE}, H:mm O`, // Jun 30, 2023, 14:43 GMT+2 } export interface BentoDateProps { /** * The date to format. The data can be supplied as: * - a Date object. * - a number of milliseconds elapsed since the epoch. * - a date string e.g. '2023-07-31T10:55:22+02:00'. */ date?: Date | number | string | null; /** * The string format of the date to be displayed */ format?: BentoDateFormat | `${BentoDateFormat}`; /** * Disable the default typography for the component. Only use this when the component is inside another typography */ disableTypography?: boolean; }
|
package/dist/assets/components/date-picker/composables/use-date-picker-single-calendar-text.types.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { type ComputedRef, type Ref } from 'vue'; export interface UseDateDisplayTextProps<T> { datePickerValue: Ref<T>; isDateIncorrect: Ref<boolean>; isMonthVariant?: Ref<boolean>; placeholder?: Ref<string>; } export type UseDateDisplayText<T> = (props: UseDateDisplayTextProps<T>) => { // Values dateDisplayValue: ComputedRef<string>; textInputValue: Ref<string>; // Methods formatTextInputDisplayValue: (dateToFormat?: Date) => void; };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import type { FormField, FormFieldEmits } from '../../types/form-field'; import { CalendarFirstDayOfWeek as BentoDatePickerFirstDayOfWeek, type CalendarIsDateDisabled as BentoDatePickerIsDateDisabled, type CalendarSingleDateValue as BentoDatePickerValue, CalendarVariant as BentoDatePickerVariant, } from '@/internal'; export { BentoDatePickerFirstDayOfWeek, type BentoDatePickerIsDateDisabled, type BentoDatePickerValue, BentoDatePickerVariant, }; /** * @deprecated since version 2.0. This enum is deprecated. Use the event name as a string literal instead. */ export enum BentoDatePickerEvent { INPUT = 'input', } export type BentoDatePickerEmits = FormFieldEmits<BentoDatePickerValue>; export interface BentoDatePickerProps extends FormField { /** * Default start date for the calendar pane. * Expected format: YYYY-MM-DD * @default null */ defaultDisplayMonth?: string | null; /** * Allows you to set the first day of the week. 0 is sunday, 1 is monday, 6 is saturday * @default undefined */ firstDayOfWeek?: BentoDatePickerFirstDayOfWeek; /** * Indicate if a date should be disabled or not */ isDateDisabled?: BentoDatePickerIsDateDisabled; /** * Sets a minimum limit for selectable dates - dates before `min` will not be selectable * @default null */ min?: Date | null; /** * Sets a maximum limit for selectable dates - dates after `max` will not be selectable * @default null */ max?: Date | null; /** * Selected date * * @default null */ modelValue?: BentoDatePickerValue | null; /** * Selected date * * @deprecated for usage with properties since version 2.0. Use `v-model` or `model-value` instead. * * @default null */ value?: BentoDatePickerValue | null; /** * The type of calendar to display. Defaults to showing days. */ variant?: `${BentoDatePickerVariant}`; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import { type CalendarGranularityType } from '@/internal/calendar/calendar.types'; export interface DateRangePickerCalendarFormData { startDate: string; endDate: string; startTime?: string; endTime?: string; granularity?: CalendarGranularityType; } export enum DateRangePickerCalendarEvent { CUSTOM_RANGE = 'custom-range', INPUT = 'input', ERROR = 'error', FORM_DATE = 'form-date', START_DATE_SELECTED = 'start-date-selected', } export interface DateRangePickerCalendarRangeSelectorItem { label: string; value: string; data: { startDate: Date; endDate?: Date; granularity?: CalendarGranularityType; isRelativeToNow?: boolean; timeDifference?: number; }; } export type DateRangePickerCalendarRangeSelectorItems = Array<DateRangePickerCalendarRangeSelectorItem>;
|