@adyen/bento-mcp 0.4.0 → 0.5.1
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 +22 -0
- package/README.md +41 -9
- package/dist/assets/components/alert/alert.types.ts +1 -1
- package/dist/assets/components/button/button.types.ts +1 -1
- package/dist/assets/components/button/components/button-actions/button-actions.types.ts +1 -1
- package/dist/assets/components/card/card.types.ts +1 -1
- package/dist/assets/components/checkbox/checkbox.vue +1 -1
- package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.types.ts +1 -1
- package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.vue +1 -1
- package/dist/assets/components/code-snippet/code-snippet.types.ts +1 -1
- package/dist/assets/components/country/country.types.ts +1 -1
- package/dist/assets/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.types.ts +1 -1
- package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.vue +1 -1
- package/dist/assets/components/data-grid/data-grid.vue +1 -1
- package/dist/assets/components/date-picker/date-picker.types.ts +1 -1
- 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/date-range-picker.types.ts +1 -1
- package/dist/assets/components/drawer/drawer.docs.mdx +68 -0
- package/dist/assets/components/drawer/drawer.stories.ts +1 -0
- package/dist/assets/components/drawer/drawer.types.ts +1 -0
- package/dist/assets/components/drawer/drawer.vue +1 -0
- package/dist/assets/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.vue +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-country/dropdown-country.vue +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-currency/dropdown-currency.vue +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.vue +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-tag/dropdown-tag.vue +1 -1
- package/dist/assets/components/dropdown/composables/use-keyboard-navigation.types.ts +1 -1
- package/dist/assets/components/dropdown/dropdown.stories.ts +1 -1
- package/dist/assets/components/dropdown/dropdown.types.ts +1 -1
- package/dist/assets/components/dropdown/dropdown.vue +1 -1
- package/dist/assets/components/file-uploader/file-uploader.vue +1 -1
- package/dist/assets/components/filter-bar/components/boolean-filter/boolean-filter.vue +1 -1
- package/dist/assets/components/filter-bar/components/input-filter/input-filter.types.ts +1 -1
- package/dist/assets/components/filter-bar/filter-bar.types.ts +1 -1
- package/dist/assets/components/header/header.types.ts +1 -1
- package/dist/assets/components/input-field/input-field.types.ts +1 -1
- package/dist/assets/components/input-field/input-field.vue +1 -1
- package/dist/assets/components/input-field-password/input-field-password.types.ts +1 -1
- package/dist/assets/components/input-field-password/input-field-password.vue +1 -1
- package/dist/assets/components/input-field-phone-number/input-field-phone-number.types.ts +1 -1
- package/dist/assets/components/input-field-phone-number/input-field-phone-number.vue +1 -1
- package/dist/assets/components/internal/calendar/calendar.types.ts +1 -1
- package/dist/assets/components/internal/calendar/composables/use-range-pane.types.ts +1 -0
- package/dist/assets/components/internal/listbox/components/listbox-single-select/listbox-single-select.vue +1 -1
- package/dist/assets/components/internal/listbox/listbox.vue +1 -1
- package/dist/assets/components/internal/teleport/teleport.vue +1 -1
- package/dist/assets/components/link/link.types.ts +1 -1
- package/dist/assets/components/menu/menu.types.ts +1 -1
- package/dist/assets/components/menu/menu.vue +1 -1
- package/dist/assets/components/modal/modal.types.ts +1 -1
- package/dist/assets/components/modal/modal.vue +1 -1
- package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.stories.ts +1 -0
- package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.types.ts +1 -0
- package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.vue +1 -0
- package/dist/assets/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.stories.ts +1 -0
- package/dist/assets/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.types.ts +1 -0
- package/dist/assets/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.vue +1 -0
- package/dist/assets/components/navigation-menu/navigation-menu.docs.mdx +321 -0
- package/dist/assets/components/navigation-menu/navigation-menu.stories.ts +1 -0
- package/dist/assets/components/navigation-menu/navigation-menu.types.ts +1 -0
- package/dist/assets/components/navigation-menu/navigation-menu.vue +1 -0
- package/dist/assets/components/pagination/pagination.types.ts +1 -1
- package/dist/assets/components/payment-method/payment-method.types.ts +1 -1
- package/dist/assets/components/popover/popover.types.ts +1 -1
- package/dist/assets/components/popper-container/popper-container.types.ts +1 -1
- package/dist/assets/components/promo-banner/promo-banner.types.ts +1 -1
- package/dist/assets/components/radio-group/radio-group.types.ts +1 -1
- package/dist/assets/components/segmented-control/segmented-control.types.ts +1 -1
- package/dist/assets/components/status/status.types.ts +1 -1
- package/dist/assets/components/summary-grid/components/summary-grid-item/summary-grid-item.types.ts +1 -1
- package/dist/assets/components/summary-grid/summary-grid.types.ts +1 -1
- package/dist/assets/components/tag/tag.types.ts +1 -1
- package/dist/assets/components/textarea/textarea.types.ts +1 -1
- package/dist/assets/components/textarea/textarea.vue +1 -1
- package/dist/assets/components/timeline/timeline.types.ts +1 -1
- package/dist/assets/components/toggle/toggle.types.ts +1 -1
- package/dist/assets/components/toggle/toggle.vue +1 -1
- package/dist/assets/components/typography/typography.types.ts +1 -1
- package/dist/assets/components.json +4 -0
- package/dist/assets/composables/use-bento-theme/install.types.ts +1 -1
- package/dist/assets/composables/use-form-validate/use-form.types.ts +1 -1
- package/dist/assets/icons.json +18 -0
- package/dist/assets/index.ts +1 -1
- package/dist/assets/usage.json +107 -0
- package/dist/assets/variables.css +3 -0
- package/dist/main.js +52 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.1 (2026-04-29)
|
|
4
|
+
|
|
5
|
+
### Documentation
|
|
6
|
+
|
|
7
|
+
- updated mcp tools documentation with all new tools ([a027b8b03](https://github.com/Adyen/bento/commit/a027b8b03))
|
|
8
|
+
|
|
9
|
+
### ❤️ Thank You
|
|
10
|
+
|
|
11
|
+
- gerald
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## 0.5.0 (2026-04-22)
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
- added `find_component_usage` to find all usages of a component in the codebase ([5a859f624](https://github.com/Adyen/bento/commit/5a859f624))
|
|
19
|
+
|
|
20
|
+
### ❤️ Thank You
|
|
21
|
+
|
|
22
|
+
- daver
|
|
23
|
+
|
|
24
|
+
|
|
3
25
|
## 0.4.0 (2026-04-15)
|
|
4
26
|
|
|
5
27
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -74,30 +74,62 @@ The server exposes the following tools to the language model:
|
|
|
74
74
|
|
|
75
75
|
### `get_component`
|
|
76
76
|
|
|
77
|
-
Retrieves the documentation, stories,
|
|
77
|
+
Retrieves the documentation, stories, Vue implementation, and type definitions for a list of components.
|
|
78
78
|
|
|
79
|
-
- **Arguments:** `componentNames` (Array<string
|
|
80
|
-
- **Returns:** A Markdown formatted string containing the contents of the `.docs.mdx`, `.stories.ts`,
|
|
81
|
-
for the requested components.
|
|
79
|
+
- **Arguments:** `componentNames` (`Array<string>`): A list of component names (e.g., `["button", "modal"]`).
|
|
80
|
+
- **Returns:** A Markdown formatted string containing the contents of the `.docs.mdx`, `.stories.ts`, `.vue`, and
|
|
81
|
+
`.types.ts` files for the requested components.
|
|
82
82
|
|
|
83
83
|
### `get_all_components`
|
|
84
84
|
|
|
85
85
|
Retrieves the global component type definitions file that lists all available Bento components (including children). Use
|
|
86
|
-
this to understand which components are available in the library.
|
|
86
|
+
this to understand which components are available in the library and their names.
|
|
87
87
|
|
|
88
|
-
- **Returns:** The list of components available in Bento
|
|
88
|
+
- **Returns:** The list of components available in Bento.
|
|
89
89
|
|
|
90
90
|
### `get_deprecations`
|
|
91
91
|
|
|
92
92
|
Retrieves the Bento Deprecations Registry. Use this to find out which components, properties, or features are deprecated
|
|
93
93
|
and what are the recommended alternatives.
|
|
94
94
|
|
|
95
|
-
- **Returns:** A Markdown formatted string containing the registry of all deprecated features
|
|
96
|
-
code.
|
|
95
|
+
- **Returns:** A Markdown formatted string containing the registry of all deprecated features.
|
|
97
96
|
|
|
98
97
|
### `get_docs`
|
|
99
98
|
|
|
100
99
|
Retrieves specific documentation files for Bento.
|
|
101
100
|
|
|
102
|
-
- **Arguments:** `docType` ('tailwind' | 'getting-started'): The type of documentation to retrieve.
|
|
101
|
+
- **Arguments:** `docType` (`'tailwind'` | `'getting-started'`): The type of documentation to retrieve.
|
|
103
102
|
- **Returns:** The content of the requested documentation file.
|
|
103
|
+
|
|
104
|
+
### `get_design_tokens`
|
|
105
|
+
|
|
106
|
+
Retrieves Bento Design Tokens (CSS variables), grouped by semantic categories (color, spacing, border, shadow,
|
|
107
|
+
animation, typography, layout).
|
|
108
|
+
|
|
109
|
+
- **Arguments:** `searchQuery` (`string`, optional): A search term to filter tokens (e.g., `"critical"`, `"spacer"`,
|
|
110
|
+
`"background"`).
|
|
111
|
+
- **Returns:** A Markdown formatted string containing a categorized list of CSS variable names and their values.
|
|
112
|
+
|
|
113
|
+
### `get_utility_classes`
|
|
114
|
+
|
|
115
|
+
Retrieves the reference guide for legacy Bento utility classes (`b-` prefix), including spacing, color, and display
|
|
116
|
+
utilities. Also provides guidance on the transition to Tailwind CSS.
|
|
117
|
+
|
|
118
|
+
- **Returns:** A Markdown formatted string containing the utility classes reference guide.
|
|
119
|
+
|
|
120
|
+
### `get_icons`
|
|
121
|
+
|
|
122
|
+
Retrieves the registry of available Bento icons from `@adyen/ui-assets-icons-16`. Use this to find the correct icon name
|
|
123
|
+
for imports.
|
|
124
|
+
|
|
125
|
+
- **Arguments:** `searchQuery` (`string`, optional): A search term to filter icons (e.g., `"arrow"`, `"check"`,
|
|
126
|
+
`"edit"`).
|
|
127
|
+
- **Returns:** A Markdown formatted string containing a list of matching icon names with import examples.
|
|
128
|
+
|
|
129
|
+
### `find_component_usage`
|
|
130
|
+
|
|
131
|
+
Finds all files in the Bento library where a specific component is used. This helps in understanding real-world
|
|
132
|
+
implementation examples of a component.
|
|
133
|
+
|
|
134
|
+
- **Arguments:** `componentName` (`string`): The name of the component (e.g., `"button"`, `"modal"`).
|
|
135
|
+
- **Returns:** A list of file paths where the component is referenced.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
/** * @deprecated
|
|
1
|
+
/** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoAlertVariant { DEFAULT = 'default', TIP = 'tip', } /** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoAlertTypeOption { WARNING = 'warning', CRITICAL = 'critical', HIGHLIGHT = 'highlight', SUCCESS = 'success', } /** * @deprecated Since v2.0.0. Use `@close-alert` instead. */ export enum BentoAlertEvent { CLOSE_ALERT = 'close-alert', }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
/** * @deprecated
|
|
1
|
+
/** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoButtonVariant { PRIMARY = 'primary', SECONDARY = 'secondary', TERTIARY = 'tertiary', TERTIARY_WITH_BACKGROUND = 'tertiary-with-background', } export enum BentoButtonPropClass { CONDENSED = 'condensed', DESTRUCTIVE = 'critical', DISABLED = 'disabled', ICON_ONLY = 'icon-only', INVERSE = 'inverse', } export interface BentoButtonProps { condensed?: boolean; critical?: boolean; disabled?: boolean; inverse?: boolean; variant?: BentoButtonVariant | `${BentoButtonVariant}`; type?: string; link?: object; }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import type { BentoButtonProps } from '../../button.types'; import type { BentoMenuProps } from '@/components/menu/menu.types'; import type { BentoLoadingButtonState } from '../loading-button/loading-button.types'; import type { VueNodeElement } from '@/types/elements'; import type { OmitStrict } from '@/types/omit-strict'; import type { VNodeRef } from 'vue/types/vnode'; export type BentoButtonActionObject = BentoActionButton; // TODO: Update to Array<BentoButtonActionButton | BentoButtonActionMenuObject> in v2.0.0 export type BentoButtonActionsList = Array<BentoActionButton & BentoActionMenu>; export enum BentoButtonActionsLayoutBasic { BUTTONS_END = 'buttons-end', FILL_CONTAINER = 'fill-container', SPACE_BETWEEN = 'space-between', VERTICAL_STACK = 'vertical-stack', } export enum BentoButtonActionsLayoutExtended { BUTTONS_START = 'buttons-start', } /** * @deprecated
|
|
1
|
+
import type { BentoButtonProps } from '../../button.types'; import type { BentoMenuProps } from '@/components/menu/menu.types'; import type { BentoLoadingButtonState } from '../loading-button/loading-button.types'; import type { VueNodeElement } from '@/types/elements'; import type { OmitStrict } from '@/types/omit-strict'; import type { VNodeRef } from 'vue/types/vnode'; export type BentoButtonActionObject = BentoActionButton; // TODO: Update to Array<BentoButtonActionButton | BentoButtonActionMenuObject> in v2.0.0 export type BentoButtonActionsList = Array<BentoActionButton & BentoActionMenu>; export enum BentoButtonActionsLayoutBasic { BUTTONS_END = 'buttons-end', FILL_CONTAINER = 'fill-container', SPACE_BETWEEN = 'space-between', VERTICAL_STACK = 'vertical-stack', } export enum BentoButtonActionsLayoutExtended { BUTTONS_START = 'buttons-start', } /** * @deprecated Since v2.0.0. Use string literal instead. */ export const BentoButtonActionsLayout = { ...BentoButtonActionsLayoutBasic, ...BentoButtonActionsLayoutExtended }; export type BentoButtonActionsLayout = BentoButtonActionsLayoutBasic | BentoButtonActionsLayoutExtended; export interface BentoButtonActionsProps { /** * List of actions that will be used to render the buttons. * First element in the list will be rendered as "primary". * All elements after the first will be rendered as "secondary". * * Each object in the list should have a `title`, an `event` and * (optional) a Vue `icon` from the library `@adyen/ui-assets-icons-16`. * * `title`: Button's text. * * `event`: Event triggered when clicking the Button. * * `icon` (optional): Vue Icon from the library `@adyen/ui-assets-icons-16`. * * @see BentoButton for a list of all the other props supported by each button action. * * ``` * [{ * title: 'Primary', * event: () => { ... }, * icon: FourPeopleIcon, * link: { * variant: 'default', * external: true, * to: 'https://www.adyen.com/' * }, * }, { * title: 'Secondary', * event: () => { ... } * disabled: true, * variant: 'secondary', * critical: true * }] * ``` */ actions: BentoButtonActionsList; /** * Sets the Button Actions to color scheme inverse */ inverse?: boolean; /** * Defines the layout in which the button actions is presented. * It can be one of: * * @values BUTTONS_START * @values BUTTONS_END * @values FILL_CONTAINER * @values SPACE_BETWEEN * @values VERTICAL_STACK */ layout?: string; } /** * Private */ /** * Base interface for the Button and Menu objects */ export interface BentoButtonActionBase { title: string; icon?: VueNodeElement; iconLeft?: VueNodeElement; iconRight?: VueNodeElement; /** * Provides a template ref to the underlying component instance. */ elementRef?: Exclude<VNodeRef, string>; } /** * Type for the Buttons passed as objects to Button Actions */ export interface BentoActionButton extends BentoButtonActionBase, BentoButtonProps { event?: (event: Event) => void; state?: BentoLoadingButtonState | `${BentoLoadingButtonState}`; iconOnly?: boolean; } /** * Type for the Menus passed as objects to Button Actions */ export type BentoActionMenu = BentoButtonActionBase & OmitStrict<BentoMenuProps, 'button' | 'data'> & Partial<Pick<BentoMenuProps, 'data'>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
/** * @deprecated
|
|
1
|
+
/** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoCardBackground { PRIMARY = 'primary', SECONDARY = 'secondary', } /** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoCardTitleSize { DEFAULT = 'default', SMALL = 'small', }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <label class="b-checkbox" :class="conditionalClass" v-bind="styleAttrs" :aria-hidden="getAriaHidden($attrs)"> <checkbox-input v-bind="$attrs" ref="input" :aria-label="computedAriaLabel" :aria-describedby="ariaDescribedBy" :aria-required="required" :disabled="disabled" :has-error="shouldShowError" :indeterminate="isIndeterminate" :value="value" :model-value="modelValue" :readonly="readonly" @update:model-value="onChange" /> <span v-if="hasSlot('default') || shouldShowErrorMessage" class="b-checkbox__text"> <bento-typography v-if="hasSlot('default')" class="b-checkbox__label" el="span" variant="body"> <span><slot /></span> <bento-typography v-if="optional" class="b-checkbox__label-requirement" el="span" >({{ t('optional') }})</bento-typography > <bento-typography v-else-if="required" class="b-checkbox__label-requirement" el="span" >*</bento-typography > <bento-tag v-if="tag" class="b-checkbox__label-tag" v-bind="tag" /> </bento-typography> <bento-typography v-if="hasSlot('default') && hasSlot('description')" :id="descriptionId" class="b-checkbox__description" el="span" variant="body" > <slot name="description"></slot> </bento-typography> <error-message v-if="shouldShowErrorMessage" :id="errorId" :error-message="errorMessage" /> <bento-typography v-if="contentSlotAvailable" class="b-checkbox__content" el="span" variant="body"> <slot name="content"></slot> </bento-typography> </span> </label> </template> <script lang="ts"> import { computed, defineComponent, type HTMLAttributes, onMounted, type PropType, ref, toRefs, useAttrs, } from 'vue'; import { BentoCheckboxEvent, BentoCheckboxState, type BentoCheckboxTag, type BentoCheckboxType, type BentoMultiCheckboxType, } from './checkbox.types'; import { BentoTypography } from '@/components/typography'; import { CheckboxInput } from '@/components/internal/checkbox-input'; import { ErrorMessage } from '@/components/internal/error-message'; import { useDefaultSlotValidator, useHasSlot } from '@/composables'; import { generateUid } from '@/core/utils/ts'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { useI18n } from '@/utils/ts/i18n'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { BentoTag } from '@/components/tag'; import { type BentoCheckboxGroupItems } from './components/checkbox-group/checkbox-group.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; /** * Checkboxes are used to make a binary choice about an option. * The label tag area is also clickable. * * @usage * import { BentoCheckbox } from '@adyen/bento-vue2'; * * export default { * components: { BentoCheckbox }, * template: ` * <bento-checkbox> * Label text * <template #description>Sub label text</template> * </bento-checkbox> * `, * } */ export default defineComponent({ name: 'bento-checkbox', i18n: { messages }, components: { BentoTag, BentoTypography, CheckboxInput, ErrorMessage, }, inheritAttrs: false, model: { prop: 'modelValue' }, props: { /** * @deprecated
|
|
1
|
+
<template> <label class="b-checkbox" :class="conditionalClass" v-bind="styleAttrs" :aria-hidden="getAriaHidden($attrs)"> <checkbox-input v-bind="$attrs" ref="input" :aria-label="computedAriaLabel" :aria-describedby="ariaDescribedBy" :aria-required="required" :disabled="disabled" :has-error="shouldShowError" :indeterminate="isIndeterminate" :value="value" :model-value="modelValue" :readonly="readonly" @update:model-value="onChange" /> <span v-if="hasSlot('default') || shouldShowErrorMessage" class="b-checkbox__text"> <bento-typography v-if="hasSlot('default')" class="b-checkbox__label" el="span" variant="body"> <span><slot /></span> <bento-typography v-if="optional" class="b-checkbox__label-requirement" el="span" >({{ t('optional') }})</bento-typography > <bento-typography v-else-if="required" class="b-checkbox__label-requirement" el="span" >*</bento-typography > <bento-tag v-if="tag" class="b-checkbox__label-tag" v-bind="tag" /> </bento-typography> <bento-typography v-if="hasSlot('default') && hasSlot('description')" :id="descriptionId" class="b-checkbox__description" el="span" variant="body" > <slot name="description"></slot> </bento-typography> <error-message v-if="shouldShowErrorMessage" :id="errorId" :error-message="errorMessage" /> <bento-typography v-if="contentSlotAvailable" class="b-checkbox__content" el="span" variant="body"> <slot name="content"></slot> </bento-typography> </span> </label> </template> <script lang="ts"> import { computed, defineComponent, type HTMLAttributes, onMounted, type PropType, ref, toRefs, useAttrs, } from 'vue'; import { BentoCheckboxEvent, BentoCheckboxState, type BentoCheckboxTag, type BentoCheckboxType, type BentoMultiCheckboxType, } from './checkbox.types'; import { BentoTypography } from '@/components/typography'; import { CheckboxInput } from '@/components/internal/checkbox-input'; import { ErrorMessage } from '@/components/internal/error-message'; import { useDefaultSlotValidator, useHasSlot } from '@/composables'; import { generateUid } from '@/core/utils/ts'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { useI18n } from '@/utils/ts/i18n'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { BentoTag } from '@/components/tag'; import { type BentoCheckboxGroupItems } from './components/checkbox-group/checkbox-group.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; /** * Checkboxes are used to make a binary choice about an option. * The label tag area is also clickable. * * @usage * import { BentoCheckbox } from '@adyen/bento-vue2'; * * export default { * components: { BentoCheckbox }, * template: ` * <bento-checkbox> * Label text * <template #description>Sub label text</template> * </bento-checkbox> * `, * } */ export default defineComponent({ name: 'bento-checkbox', i18n: { messages }, components: { BentoTag, BentoTypography, CheckboxInput, ErrorMessage, }, inheritAttrs: false, model: { prop: 'modelValue' }, props: { /** * @deprecated Since v2.0.0. Use `aria-label` instead. * Accessible name for the radio button component. It is strongly recommended to pas this value in case this component is used without a label. */ ariaLabel: { type: String, default: null }, /** * Setting this property to true will disable the checkbox in your form. */ disabled: { type: Boolean, default: false }, /** * Renders the checkbox in an error state with an error message. */ errorMessage: { type: String, default: null }, /** * Setting this property to true will show the checkbox in error state. */ hasError: { type: Boolean, default: false }, /** * The :indeterminate CSS pseudo-class represents any form element * whose state is indeterminate, such as checkboxes which have their HTML indeterminate attribute * set to true, radio buttons which are members of a group in which all radio buttons are unchecked, * and indeterminate <progress> elements. https://developer.mozilla.org/en-US/docs/Web/CSS/:indeterminate * This property lets you control the input.indeterminate native property. */ indeterminate: { type: Boolean, default: false }, /** * When handling a single checkbox with a singular state, use a boolean. * * When handling multiple checkboxes, use an array of keys. * These keys exist as a set "value" prop for each checkbox. */ modelValue: { type: [Boolean, Array] as PropType<BentoCheckboxType | BentoCheckboxGroupItems>, default: false, }, /** * Highlights the checkbox is optional. */ optional: { type: Boolean, default: false }, /** * Setting this property to true will set the checkbox to read only state. */ readonly: { type: Boolean, default: false }, /** * Highlights the checkbox is required. */ required: { type: Boolean, default: false }, /** * Sets the properties of the BentoTag component that is placed next to the label. * {@see BentoTag} */ tag: { type: Object as PropType<BentoCheckboxTag>, default: undefined }, /** * The value of the component. The DOM API casts this to a string. * The browser uses "on" as the default value. * * In the case of multiple checkboxes this value is used as a unique key. */ value: { type: [String, Number], default: null }, }, emits: [BentoCheckboxEvent.UPDATE, BentoCheckboxEvent.INPUT], setup(props, { attrs, emit, slots }) { const { t } = useI18n<{ message: MessageSchema }>({ messages }); const descriptionId = generateUid('description'); const errorId = generateUid('error'); const hasSlot = useHasSlot(slots); const { disabled, indeterminate, modelValue, readonly, value } = toRefs(props); const defaultSlotValue = getSlotText(slots)('default'); const descriptionSlotValue = getSlotText(slots)('description'); useDefaultSlotValidator('bento-checkbox', slots.default, slots.description); const isCheckboxGroup = computed(() => Array.isArray(modelValue.value)); const isChecked = computed(() => { if (!isCheckboxGroup.value) { return modelValue.value as boolean; } return (modelValue.value as BentoMultiCheckboxType).some(item => item === value.value); }); const isIndeterminate = computed(() => indeterminate.value); const shouldShowError = computed( () => !props.disabled && !props.readonly && (props.hasError || !!props.errorMessage) ); const shouldShowErrorMessage = computed(() => !props.disabled && !props.readonly && !!props.errorMessage); // Content slot logic const hasContentSlot = computed(() => hasSlot('content')); const isCheckedOrIntermediate = computed(() => isIndeterminate?.value || isChecked?.value); const contentSlotAvailable = computed( () => hasContentSlot?.value && isCheckboxGroup?.value && isCheckedOrIntermediate?.value ); // ARIA fields const ariaDescribedBy = computed<HTMLAttributes['aria-describedby']>( () => `${shouldShowErrorMessage.value ? errorId : ''} ${ defaultSlotValue && descriptionSlotValue ? descriptionId : '' }`.trim() || null ); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel } = toRefs(props); const checkboxTranslation = ref(); // Translations used in the script tag need to be inside onMounted otherwise they fail in Vue 3 onMounted(() => { checkboxTranslation.value = t('checkbox'); }); // Only add aria-label if no label slot is present const computedAriaLabel = computed( () => useAriaLabel({ ariaLabel: ariaLabelAttribute.value as string, // TODO: take this line away when the aria-label props is removed label: ariaLabel, defaultFallback: hasSlot('default') ? (getSlotText(slots)('default') as string) : checkboxTranslation.value, }).value ); const getAriaHidden = (attributes: HTMLAttributes) => attributes?.['aria-hidden']; const styleAttrs = computed(() => { const { style, class: cls } = useAttrs(); return { class: cls as HTMLAttributes['class'], style: style as HTMLAttributes['style'], }; }); const conditionalClass = computed(() => ({ [`b-checkbox--${BentoCheckboxState.DISABLED}`]: disabled.value, [`b-checkbox--${BentoCheckboxState.READONLY}`]: readonly.value, })); const onChange = (newValue: BentoCheckboxType | BentoCheckboxGroupItems) => { if (disabled.value) { return; } emit(BentoCheckboxEvent.UPDATE, newValue); emit(BentoCheckboxEvent.INPUT, newValue); }; return { // Values descriptionId, errorId, ariaDescribedBy, computedAriaLabel, conditionalClass, defaultSlotValue, descriptionSlotValue, isIndeterminate, shouldShowError, shouldShowErrorMessage, contentSlotAvailable, hasContentSlot, styleAttrs, // Methods hasSlot, onChange, getAriaHidden, t, }; }, }); </script> <style lang="scss" scoped src="./checkbox.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { type FormFieldEmits } from '../../../../types/form-field'; import type { BentoCheckboxTag } from '../../checkbox.types'; /** * @deprecated
|
|
1
|
+
import { type FormFieldEmits } from '../../../../types/form-field'; import type { BentoCheckboxTag } from '../../checkbox.types'; /** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoCheckboxGroupVariant { HORIZONTAL = 'horizontal', VERTICAL = 'vertical', } export interface BentoCheckboxGroupItem { value: string | number; label: string; description?: string; disabled?: boolean; hasError?: boolean; errorMessage?: string; readonly?: boolean; tag?: BentoCheckboxTag; } export type BentoCheckboxGroupItems = Array<BentoCheckboxGroupItem>; export type BentoCheckboxGroupEmits = FormFieldEmits<any>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div class="b-checkbox-group"> <controls-group class="b-checkbox-group__checkboxes" :aria-describedby="shouldShowGroupErrorMessage ? errorId : ''" :aria-required="required" component-name="bento-checkbox-group" :class="conditionalClasses" :description="description" :disabled="disabled" :form="formId" :hide-label="hideLabel" :label="label" :optional="optional" :required="required" :tooltip-text="tooltipText" > <bento-checkbox v-for="item in items" :key="item.value" :model-value="inputValue" :value="item.value" :disabled="disabled || item.disabled" :has-error="shouldShowGroupError || item.hasError" :error-message="shouldShowGroupError ? null : item.errorMessage" :readonly="isReadOnly || item.readonly" :tag="item.tag" @update:model-value="onModelValueUpdate" > <template #default>{{ item.label }}</template> <template v-if="item.description" #description>{{ item.description }}</template> <template v-if="hasContentSlot('content')" #content> <slot name="content" v-bind="item"></slot> </template> </bento-checkbox> </controls-group> <error-message v-if="shouldShowGroupErrorMessage" :id="errorId" :error-message="errorMessage" class="b-checkbox-group__error-message" /> </div> </template> <script setup lang="ts"> import { computed, onMounted, type PropType, ref, toRef, useSlots, watch } from 'vue'; import { ControlsGroup } from '@/components/internal/controls-group'; import { ErrorMessage } from '@/components/internal/error-message'; import { type BentoCheckboxGroupEmits, type BentoCheckboxGroupItems, BentoCheckboxGroupVariant, } from './checkbox-group.types'; import BentoCheckbox from '@/components/checkbox/checkbox.vue'; import { generateUid } from '@/core/utils/ts'; import { deprecate } from '@/utils/ts/deprecate'; import { useFormLayoutFieldLoading } from '@/composables/use-form-layout-loading/use-form-layout-loading'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; const props = defineProps({ /** * Descriptive supporting text visbile underneath the label. */ description: { type: String, default: null }, /** * If set to true, renders all checkboxes in the group in a disabled state. */ disabled: { type: Boolean, default: false }, /** * Renders all checkboxes in an error state with an error message. */ errorMessage: { type: String, default: null }, /** * The form id the checkbox group is associated with. Defaults to null, as a checkbox group can also be used standalone. */ formId: { type: String, default: null }, /** * If set to true, renders all checkboxes in the group in an error state. * * @deprecated Use
|
|
1
|
+
<template> <div class="b-checkbox-group"> <controls-group class="b-checkbox-group__checkboxes" :aria-describedby="shouldShowGroupErrorMessage ? errorId : ''" :aria-required="required" component-name="bento-checkbox-group" :class="conditionalClasses" :description="description" :disabled="disabled" :form="formId" :hide-label="hideLabel" :label="label" :optional="optional" :required="required" :tooltip-text="tooltipText" > <bento-checkbox v-for="item in items" :key="item.value" :model-value="inputValue" :value="item.value" :disabled="disabled || item.disabled" :has-error="shouldShowGroupError || item.hasError" :error-message="shouldShowGroupError ? null : item.errorMessage" :readonly="isReadOnly || item.readonly" :tag="item.tag" @update:model-value="onModelValueUpdate" > <template #default>{{ item.label }}</template> <template v-if="item.description" #description>{{ item.description }}</template> <template v-if="hasContentSlot('content')" #content> <slot name="content" v-bind="item"></slot> </template> </bento-checkbox> </controls-group> <error-message v-if="shouldShowGroupErrorMessage" :id="errorId" :error-message="errorMessage" class="b-checkbox-group__error-message" /> </div> </template> <script setup lang="ts"> import { computed, onMounted, type PropType, ref, toRef, useSlots, watch } from 'vue'; import { ControlsGroup } from '@/components/internal/controls-group'; import { ErrorMessage } from '@/components/internal/error-message'; import { type BentoCheckboxGroupEmits, type BentoCheckboxGroupItems, BentoCheckboxGroupVariant, } from './checkbox-group.types'; import BentoCheckbox from '@/components/checkbox/checkbox.vue'; import { generateUid } from '@/core/utils/ts'; import { deprecate } from '@/utils/ts/deprecate'; import { useFormLayoutFieldLoading } from '@/composables/use-form-layout-loading/use-form-layout-loading'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; const props = defineProps({ /** * Descriptive supporting text visbile underneath the label. */ description: { type: String, default: null }, /** * If set to true, renders all checkboxes in the group in a disabled state. */ disabled: { type: Boolean, default: false }, /** * Renders all checkboxes in an error state with an error message. */ errorMessage: { type: String, default: null }, /** * The form id the checkbox group is associated with. Defaults to null, as a checkbox group can also be used standalone. */ formId: { type: String, default: null }, /** * If set to true, renders all checkboxes in the group in an error state. * * @deprecated Since v2.0.0. Use `errorMessage` instead. */ hasError: { type: Boolean, default: false }, /** * Shows/hides the group label. */ hideLabel: { type: Boolean, default: null }, /** * The checkbox group's items. */ items: { type: Array as PropType<BentoCheckboxGroupItems>, required: true }, /** * The name associated with the group. Required for accessibility purposes. */ label: { type: String, required: true }, /** * Determines the initially selected value of the group. Corresponds to the BentoCheckboxButtonItem's value. * Defaults to no value selected. */ modelValue: { type: Array as PropType<Array<number | string | null>>, default: () => [] }, /** * Indicates the field is optional. */ optional: { type: Boolean, default: false }, /** * If set to true, renders all checkboxes in the group in a readonly state. */ readonly: { type: Boolean, default: false }, /** * Indicates the field is required. */ required: { type: Boolean, default: false }, /** * Tooltip message. */ tooltipText: { type: String, default: null }, /** * The checkbox group's layout. It can be vertical or horizontal. Defaults to vertical. */ variant: { type: String as PropType<BentoCheckboxGroupVariant | `${BentoCheckboxGroupVariant}`>, default: BentoCheckboxGroupVariant.VERTICAL, validator: (value: BentoCheckboxGroupVariant) => Object.values(BentoCheckboxGroupVariant).includes(value), }, }); const emit = defineEmits<BentoCheckboxGroupEmits>(); const slots = useSlots(); const errorId = generateUid('error'); const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const { emitValue } = useFormFieldEmits(emit); const shouldShowGroupError = computed( () => !props.disabled && !isReadOnly.value && (!!props.errorMessage || props.hasError) ); const shouldShowGroupErrorMessage = computed(() => !props.disabled && !isReadOnly.value && !!props.errorMessage); const conditionalClasses = computed(() => ({ 'b-checkbox-group__checkboxes--horizontal': props.variant === BentoCheckboxGroupVariant.HORIZONTAL, })); // Slots logic // Temporary, use-has-slots composable doesn't currently work for // vue3 named scoped slots const hasContentSlot = (name: string) => !!slots[name]; const shouldShowSlotsError = computed(() => hasContentSlot('content') && props.variant !== 'vertical'); watch( shouldShowSlotsError, newValue => { if (newValue) { throw new Error('The content slot is only available for the vertical checkbox group layout'); } }, { immediate: true } ); const inputValue = ref(props.modelValue); watch( () => props.modelValue, () => { inputValue.value = props.modelValue; } ); const onModelValueUpdate = (value: any) => { inputValue.value = value; emitValue(value); }; onMounted(() => { if (props.hasError) { deprecate( 'BentoCheckboxGroup "hasError" property', `Use the BentoCheckboxGroup "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-checkbox-group errorMessage="Error message text" />`, '2.0.0' ); } }); </script> <script lang="ts"> const COMPONENT_NAME = 'bento-checkbox-group'; /** * A checkbox group component should be used whenever it is necessary to group a set of checkboxes that are related together * This component conforms to the official WAI-ARIA guidelines for grouping controls: https://www.w3.org/WAI/tutorials/forms/grouping/ * * import { BentoCheckboxGroup } from '@adyen/bento-vue2/'; * * export default { * components: { BentoCheckboxGroup }, * template: ` * <bento-checkbox-group * label="Your group label" * :items="[ * { value: 1, label: 'Your label' }, * { value: 2, label: 'Another label', description: 'Your sub label' }, * ]" * /> * ` * }; */ export default { name: COMPONENT_NAME, model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./checkbox-group.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
/** * @deprecated
|
|
1
|
+
/** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoCodeSnippetVariant { INLINE = 'inline', SINGLELINE = 'singleline', MULTILINE = 'multiline', } /** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoCodeSnippetSupportedLanguage { JSON = 'json', XML = 'xml', JAVA = 'java', JAVASCRIPT = 'javascript', PYTHON = 'python', CSV = 'csv', } export enum BentoCodeSnippetEvent { ACTION = 'action', CHANGE = 'change', } export interface BentoCodeSnippetProps { /** * Displays a button that when clicked will emit an action event * Leaves to the parent component the logic of what to do with it */ actionable?: boolean; /** * Displays a button that when clicked will emit a run event * Leaves to the parent component the logic of what to do with it */ actionableText?: string; /** * The code content */ content?: string; /** * Displays a button that can expand the codeblock into a modal to easily * view large blocks of code. */ expandable?: boolean; /** * Allows editing of the code-snippet content by the user */ editable?: boolean; /** * Language name for syntax highlighting rules */ lang?: BentoCodeSnippetSupportedLanguage | `${BentoCodeSnippetSupportedLanguage}`; /** * Label to show in the action-bar, describing the language */ langLabel?: string; /** * Toggles the loading indicator within the action bar */ loading?: boolean; /** * Defines the variant of code-snippet */ variant?: BentoCodeSnippetVariant | `${BentoCodeSnippetVariant}`; }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
/** * @deprecated
|
|
1
|
+
/** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoCountryVariant { ISO = 'iso', NAME = 'name', PHONE_CODE = 'phone_code', CAPITAL = 'capital', /** deprecated Use any of the other BentoCountry variants instead OR BentoTypography component for custom text */ CUSTOM = 'custom', } export enum BentoCountryCode { BD = 'BD', BE = 'BE', BF = 'BF', BG = 'BG', BA = 'BA', BB = 'BB', WF = 'WF', BL = 'BL', BM = 'BM', BN = 'BN', BO = 'BO', BH = 'BH', BI = 'BI', BJ = 'BJ', BT = 'BT', JM = 'JM', BV = 'BV', BW = 'BW', WS = 'WS', BQ = 'BQ', BR = 'BR', BS = 'BS', JE = 'JE', BY = 'BY', BZ = 'BZ', RU = 'RU', RW = 'RW', RS = 'RS', TL = 'TL', RE = 'RE', TM = 'TM', TJ = 'TJ', RO = 'RO', TK = 'TK', GW = 'GW', GU = 'GU', GT = 'GT', GS = 'GS', GR = 'GR', GQ = 'GQ', GP = 'GP', JP = 'JP', GY = 'GY', GG = 'GG', GF = 'GF', GE = 'GE', GD = 'GD', GB = 'GB', GA = 'GA', SV = 'SV', GN = 'GN', GM = 'GM', GL = 'GL', GI = 'GI', GH = 'GH', OM = 'OM', TN = 'TN', JO = 'JO', HR = 'HR', HT = 'HT', HU = 'HU', HK = 'HK', HN = 'HN', HM = 'HM', VE = 'VE', PR = 'PR', PS = 'PS', PW = 'PW', PT = 'PT', SJ = 'SJ', PY = 'PY', IQ = 'IQ', PA = 'PA', PF = 'PF', PG = 'PG', PE = 'PE', PK = 'PK', PH = 'PH', PN = 'PN', PL = 'PL', PM = 'PM', ZM = 'ZM', EH = 'EH', EE = 'EE', EG = 'EG', ZA = 'ZA', EC = 'EC', IT = 'IT', VN = 'VN', SB = 'SB', ET = 'ET', SO = 'SO', ZW = 'ZW', SA = 'SA', ES = 'ES', ER = 'ER', ME = 'ME', MD = 'MD', MG = 'MG', MF = 'MF', MA = 'MA', MC = 'MC', UZ = 'UZ', MM = 'MM', ML = 'ML', MO = 'MO', MN = 'MN', MH = 'MH', MK = 'MK', MU = 'MU', MT = 'MT', MW = 'MW', MV = 'MV', MQ = 'MQ', MP = 'MP', MS = 'MS', MR = 'MR', IM = 'IM', UG = 'UG', TZ = 'TZ', MY = 'MY', MX = 'MX', IL = 'IL', FR = 'FR', IO = 'IO', SH = 'SH', FI = 'FI', FJ = 'FJ', FK = 'FK', FM = 'FM', FO = 'FO', NI = 'NI', NL = 'NL', NO = 'NO', NA = 'NA', VU = 'VU', NC = 'NC', NE = 'NE', NF = 'NF', NG = 'NG', NZ = 'NZ', NP = 'NP', NR = 'NR', NU = 'NU', CK = 'CK', XK = 'XK', CI = 'CI', CH = 'CH', CO = 'CO', CN = 'CN', CM = 'CM', CL = 'CL', CC = 'CC', CA = 'CA', CG = 'CG', CF = 'CF', CD = 'CD', CZ = 'CZ', CY = 'CY', CX = 'CX', CR = 'CR', CW = 'CW', CV = 'CV', CU = 'CU', SZ = 'SZ', SY = 'SY', SX = 'SX', KG = 'KG', KE = 'KE', SS = 'SS', SR = 'SR', KI = 'KI', KH = 'KH', KN = 'KN', KM = 'KM', ST = 'ST', SK = 'SK', KR = 'KR', SI = 'SI', KP = 'KP', KW = 'KW', SN = 'SN', SM = 'SM', SL = 'SL', SC = 'SC', KZ = 'KZ', KY = 'KY', SG = 'SG', SE = 'SE', SD = 'SD', DO = 'DO', DM = 'DM', DJ = 'DJ', DK = 'DK', VG = 'VG', DE = 'DE', YE = 'YE', DZ = 'DZ', US = 'US', UY = 'UY', YT = 'YT', UM = 'UM', LB = 'LB', LC = 'LC', LA = 'LA', TV = 'TV', TW = 'TW', TT = 'TT', TR = 'TR', LK = 'LK', LI = 'LI', LV = 'LV', TO = 'TO', LT = 'LT', LU = 'LU', LR = 'LR', LS = 'LS', TH = 'TH', TF = 'TF', TG = 'TG', TD = 'TD', TC = 'TC', LY = 'LY', VA = 'VA', VC = 'VC', AE = 'AE', AD = 'AD', AG = 'AG', AF = 'AF', AI = 'AI', VI = 'VI', IS = 'IS', IR = 'IR', AM = 'AM', AL = 'AL', AO = 'AO', AQ = 'AQ', AS = 'AS', AR = 'AR', AU = 'AU', AT = 'AT', AW = 'AW', IN = 'IN', AX = 'AX', AZ = 'AZ', IE = 'IE', ID = 'ID', UA = 'UA', QA = 'QA', MZ = 'MZ', UNKNOWN = 'ZZ', } export interface BentoCountryProps { /** * Two character country code ([ISO 3166-1 alpha-2 format](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)). */ code?: BentoCountryCode | `${BentoCountryCode}`; /** * Custom label text. */ label?: string; /** * Which label variant to display. * @values iso, name, phone_code, capital */ variant?: BentoCountryVariant | `${BentoCountryVariant}`; }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import type { BentoButtonActionObject } from '@/components/button/components/button-actions/button-actions.types'; import type { BentoMenuItem } from '@/components/menu/components/menu-item/menu-item.types'; import type { BentoDatagridDataItem } from '@/components/data-grid/components/data-grid-row/data-grid-row.types'; import type { OmitStrict } from '@/types/omit-strict'; import { type BentoTooltipPosition } from '@/directives/tooltip/tooltip.types'; export type BentoColumnCellActionVariant = 'copy' | 'edit' | 'open' | 'more'; export type BentoColumnCellActionEvent = (rowDataItem: BentoDatagridDataItem, columnKey: string) => void; export type BentoColumnCellActionMenuItem = { /** * @deprecated
|
|
1
|
+
import type { BentoButtonActionObject } from '@/components/button/components/button-actions/button-actions.types'; import type { BentoMenuItem } from '@/components/menu/components/menu-item/menu-item.types'; import type { BentoDatagridDataItem } from '@/components/data-grid/components/data-grid-row/data-grid-row.types'; import type { OmitStrict } from '@/types/omit-strict'; import { type BentoTooltipPosition } from '@/directives/tooltip/tooltip.types'; export type BentoColumnCellActionVariant = 'copy' | 'edit' | 'open' | 'more'; export type BentoColumnCellActionEvent = (rowDataItem: BentoDatagridDataItem, columnKey: string) => void; export type BentoColumnCellActionMenuItem = { /** * @deprecated Since v2.0.0. Use `event` instead. */ handler?: BentoMenuItem['handler']; event?: BentoColumnCellActionEvent; } & OmitStrict<BentoMenuItem, 'handler'>; export type BentoColumnCellAction = OmitStrict< BentoButtonActionObject, 'variant' | 'event' | 'title' | 'icon' | 'state' > & { title?: string; event?: BentoColumnCellActionEvent; tooltipText?: string; tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; // Variant button properties isShort?: true; variant?: BentoColumnCellActionVariant; // Menu button properties menuData?: Array<BentoColumnCellActionMenuItem>; // Copy button properties getCopyData?: (rowDataItem: BentoDatagridDataItem, columnKey: string) => string; }; export type BentoColumnCellActions = Array<BentoColumnCellAction>; export type BentoColumnCellActionsByCellDataFn = ( rowDataItem: BentoDatagridDataItem, columnKey: string ) => BentoColumnCellActions; export type BentoColumnCellActionsProp = BentoColumnCellActions | BentoColumnCellActionsByCellDataFn; export type BentoDataGridRowActionsFn = (rowDataItem: BentoDatagridDataItem) => BentoColumnCellActions; export type BentoDataGridRowActionsProp = BentoColumnCellActions | BentoDataGridRowActionsFn;
|
|
@@ -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" :has-zebra-background="isRowEven" :is-highlighted="isHighlightedRow(rowData)" :is-selected="isSelectedRow(rowData)" teleport-menu @menu-open="onRowActionMenuOpen" @menu-close="onRowActionMenuClose" /> </div> </template> </div> <Transition v-if=" (hasNestedContent && hasSlot('nested-slot') && shouldShowNestedContent(rowData)) || (hasNestedRows && shouldShowNestedRowsBooleanResult) " :key="`${rowIndex}-nest-content`" name="b-data-grid-row-container__animation--nested-content" > <div v-if="isNestedContentShown && shouldShowRenderNestedRows" :class="conditionalNestedRowClasses" data-testid="nested-content-container" :style="nestedContentStyles" role="presentation" > <div ref="nestedContentRef"> <div v-if="isLoadingNestedRows" class="b-data-grid-row-container__row-nested-rows-loading-container" > <bento-loading-indicator /> </div> <data-grid-row v-for="(childRowData, childRowIndex) in rowData.children" v-bind="props" :id="getRowId(childRowIndex)" :key="`child-${childRowIndex}`" :ref="el => registerNestedRowRef(el, childRowIndex)" :row-data="childRowData" :row-index="childRowIndex" :row-set-size="rowData.children.length" is-nested-row @cell-hovered="emit('cell-hovered', $event)" @cell-focus="emit('cell-focus', $event)" @row-click="emitRowClick" @on-frozen-columns-key-press="emit('on-frozen-columns-key-press', $event)" @select-row="onNestedRowSelect(childRowData, $event)" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-row> </div> </div> <div v-else-if="isNestedContentShown" v-bind="nestedContentContainerDivAriaAttributes" :class="conditionalNestedRowClasses" data-testid="nested-content-container" :style="nestedContentStyles" role="row" tabindex="-1" > <div ref="nestedContentRef" role="gridcell"> <div v-if="isLoadingNestedRows" class="b-data-grid-row-container__row-nested-rows-loading-container" > <bento-loading-indicator /> </div> <div v-else-if="hasSlot('nested-slot') && shouldShowNestedContent(rowData)"> <slot :item="rowData" name="nested-slot" /> </div> </div> </div> </Transition> </div> </template> <script setup lang="ts"> import { type ComponentPublicInstance, computed, type ComputedRef, inject, onMounted, type PropType, ref, useSlots, watch, } from 'vue'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { focusOnInteractiveChild, makeAllInteractiveChildrenFocusable, makeAllInteractiveChildrenUnFocusable, } from '@/components/data-grid/utils'; import DataGridRow from '@/components/data-grid/components/data-grid-row/data-grid-row.vue'; import DataGridCell from '@/components/data-grid/components/data-grid-cell/data-grid-cell.vue'; import DataGridCellActions from '@/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.vue'; // Types and enums import { type BentoColumn, type BentoDatagridDataItem, type BentoDatagridDataItemId, type BentoDatagridGetDataItemIdFn, type BentoDatagridIsDataItemDisabledFn, type BentoDatagridShouldShowNestedContentFn, type BentoDatagridShouldShowNestedRowsFn, BentoDatagridStyleState, } from '@/components/data-grid/data-grid.types'; import type { DataGridCellColumnSet, DataGridCellDragEventsForColumn, } from '../data-grid-cell/data-grid-cell.types'; import type { BentoDatagridDraggingColumnState } from '../data-grid-columns/data-grid-columns.types'; import type { DatagridColumnHoveringState } from '@/components/data-grid/composables/use-column-hovering'; // Translations import { type Booleanish } from '@/types/prop-types'; import { useHasSlot } from '@/composables/use-has-slot/use-has-slot'; import { useExpandableContentHeight } from '@/composables/use-expandable-content-height/use-expandable-content-height'; import { observeSizeOfElement } from '@/utils/ts/resize'; import { DATAGRID_ACTIONS_COLUMN_FIELD_NAME, DATAGRID_CELL_OFFSET_PADDING, } from '@/components/data-grid/composables/use-calculate-column-width/use-calculate-column-width'; import { generateUid } from '@/core/utils/ts'; import { type BentoDataGridRowActionsProp } from '@/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.types'; import type { HTMLAttributes } from 'vue/types/jsx'; import { EventKey } from '@/types/utils-events'; import { DATA_GRID_SMALL_CONTAINER } from '@/components/data-grid/data-grid.keys'; const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = defineProps({ /** * Row actions property that generates which button to show in row actions area */ rowActions: { type: [Function, Array] as PropType<BentoDataGridRowActionsProp>, default: undefined, }, /** * When true, will emit a 'rowClick' event when a row is clicked. Will add cursor: pointer to rows. */ allowRowClicks: { type: Boolean, default: false }, /** * The columns sets including */ columnSets: { type: Array as PropType<Array<DataGridCellColumnSet>>, required: true, }, /** * Sets the row cells to the condensed padding state. */ condensed: { type: Boolean, default: false, }, /** * Columns being dragged, contains dragging column and a target column */ draggingColumns: { type: Object as PropType<BentoDatagridDraggingColumnState>, default: undefined, }, /** * Dragging event handlers for each row cell */ draggingEventsForRowCell: { type: Function as PropType<DataGridCellDragEventsForColumn>, required: true, }, /** * A customisable method to retrieve a unique identifier for a data item in the array of data supplied. * This is used selecting and performance reasons and must exist in each data item. */ getDataItemId: { type: Function as PropType<BentoDatagridGetDataItemIdFn>, default: (item: BentoDatagridDataItem) => item.id, }, /** * Set to true if the datagrid has been scrolled and the left side has overflowing space. */ hasLeftOverflow: { type: Boolean, default: false, }, /** * Enables the nested content slot functionality. */ hasNestedContent: { type: Boolean, default: false, }, /** * Enables the nested content slot functionality. */ hasNestedRows: { type: Boolean, default: false, }, /** * Set to true if the datagrid has been scrolled and the right side has overflowing space. */ hasRightOverflow: { type: Boolean, default: false, }, /** * Highlighted row IDs. */ highlightedRows: { type: Array as PropType<Array<BentoDatagridDataItemId>>, default: () => [], }, /** * The row index of a currently hovered row. */ hoveredRowIndex: { type: Number, default: -1, }, /** * The column hovered over state and whether the hovered over column should show a border right or left. */ hoveredOverColumnState: { type: Object as PropType<DatagridColumnHoveringState>, required: true, }, /** * A customisable method to determine if a row is not selectable. * The default is that every row is selectable. */ isDataItemDisabled: { type: Function as PropType<BentoDatagridIsDataItemDisabledFn>, default: () => false, }, /** * If the row is of an even number. * Rows are 0th based index e.g. index 0 is an odd-numbered row. */ isRowEven: { type: Boolean, default: false, }, /** * If the row is the last child in it's parent container. */ isRowLastChild: { type: Boolean, default: false, }, /** * If columns are being measured for the autoWidth functionality. */ isMeasuringColumnWidths: { type: Boolean, default: false, }, /** * Determines if this row is nested inside another row. */ isNestedRow: { type: Boolean, default: false, }, /** * The maximum width of the row actions in all rows */ rowActionsMaximumWidth: { type: Number, default: 0, }, /** * The row data object */ rowData: { type: Object as PropType<BentoDatagridDataItem>, required: true, }, /** * The row index. */ rowIndex: { type: Number, required: true, }, /** * The row ID to be using for the ID attribute. */ rowId: { type: Number, default: undefined, }, /** * The number of rows that this row is within. */ rowSetSize: { type: Number, default: 0, }, /** * Determines whether to render frozen seperator on the row. */ showFrozenColumnsSeparator: { type: Boolean, default: false, }, /** * Determines if the row selection is enabled. */ selectable: { type: Boolean, default: false, }, /** * The array of IDs for the selected rows. */ selectedItems: { type: Array as PropType<Array<BentoDatagridDataItemId>>, required: true, }, /** * Enables / Disables the nested-content slot */ shouldShowNestedContent: { type: Function as PropType<BentoDatagridShouldShowNestedContentFn>, default: (_item: BentoDatagridDataItem) => true, }, /** * Returns the nested rows for a given BentoDatagridDataItem. */ shouldShowNestedRows: { type: Function as PropType<BentoDatagridShouldShowNestedRowsFn>, default: (_item: BentoDatagridDataItem) => true, }, }); const emit = defineEmits<{ /** * Emitted when a cell has been hovered over. This is used to keep the row in a hovered state when hovering over a menu. */ (e: 'cell-hovered', value: number); /** * Emitted when an a frozen column cell fires a key up or down event. */ (e: 'on-frozen-columns-key-press', event: Event); /** * Emitted when a row has been selected. */ (e: 'select-row', value: Array<BentoDatagridDataItemId>, rowData: BentoDatagridDataItem); /** * Emitted when a nested row has been selected. */ ( e: 'select-nested-row', value: Array<BentoDatagridDataItemId>, parentRowData: BentoDatagridDataItem, rowData: BentoDatagridDataItem ); /** * Emitted when a row has been clicked. */ (e: 'row-click', rowData: BentoDatagridDataItem, event: MouseEvent | KeyboardEvent); /** * Emitted when a row with nested content is toggled */ (e: 'nested-content-toggled', rowData: BentoDatagridDataItem); /** * Emitted when a row cell is focused. */ ( e: 'cell-focus', value: { rowData: BentoDatagridDataItem; column: BentoColumn; rowIndex: number; colIndex: number } ); }>(); /** * Checks if the relatedTarget element appears after the reference element in DOM order. * Used to detect backward navigation (Shift+Tab). */ const isRelatedTargetAfterInDOM = (relatedTarget: HTMLElement | null, referenceEl: HTMLElement): boolean => { if (!relatedTarget || !referenceEl) { return false; } return !!(referenceEl.compareDocumentPosition(relatedTarget) & Node.DOCUMENT_POSITION_FOLLOWING); }; // Refs const gridcellRefs = ref<{ [key: string]: HTMLDivElement }>({}); const isNestedContentShown = ref(false); const nestedContentRef = ref(); const rowActionsRef = ref(); const nestedRowRefs = ref<Array<InstanceType<typeof DataGridRow>>>([]); const nestedContentSlotId = generateUid('nested-content-slot'); const isRowActionMenuOpen = ref(false); const isExitingRowActionInteraction = ref(false); const focusableItems = ref<Array<{ node: HTMLElement }>>(); /** * Returns the row-action container element, handling the case where Vue 2 * may wrap refs inside a v-for as an array. */ const getRowActionsElement = (): HTMLElement | undefined => { const val = rowActionsRef.value; return Array.isArray(val) ? val[0] : val; }; onMounted(() => { const container = getRowActionsElement(); if (container) { focusableItems.value = makeAllInteractiveChildrenUnFocusable(container); } }); const shouldShowNestedRowsBooleanResult = computed(() => props.shouldShowNestedRows(props.rowData)); const shouldShowRenderNestedRows = computed( () => !props.isNestedRow && props.hasNestedRows && shouldShowNestedRowsBooleanResult.value && props.rowData?.children?.length > 0 ); const isLoadingNestedRows = computed( () => !props.isNestedRow && props.hasNestedRows && shouldShowNestedRowsBooleanResult.value && (props.rowData?.children === undefined || props?.rowData?.children.length === 0) ); const shouldShowNestedContentToggle = computed( () => (props.hasNestedContent && props.shouldShowNestedContent(props.rowData)) || (props.hasNestedRows && shouldShowNestedRowsBooleanResult.value && !props.isNestedRow) ); const ariaRowAttributes = computed<HTMLAttributes>(() => { if (props.hasNestedContent || props.hasNestedRows) { const isNestedContentShownBooleanishVal = isNestedContentShown.value.toString() as Booleanish; return { 'aria-expanded': shouldShowNestedContentToggle.value ? isNestedContentShownBooleanishVal : undefined, 'aria-level': props.isNestedRow ? '2' : '1', 'aria-posinset': props.rowIndex + 1, 'aria-setsize': props.rowSetSize, }; } return { 'aria-rowindex': props.rowIndex + 1 }; }); const nestedContentContainerDivAriaAttributes = computed<HTMLAttributes>(() => { if (isLoadingNestedRows.value) { return { 'aria-busy': 'true', 'aria-live': 'polite', }; } if ( shouldShowRenderNestedRows.value || !props.hasNestedContent || !props.shouldShowNestedContent(props.rowData) ) { return undefined; } return { 'aria-level': props.isNestedRow ? '3' : '2', 'aria-posinset': '1', 'aria-setsize': '1', id: nestedContentSlotId, role: 'row', tabindex: '-1', }; }); const nestedRowIds = computed(() => { if (shouldShowRenderNestedRows.value) { return props.rowData?.children?.reduce((acc, _val, index) => { acc[index] = generateUid('nested-row'); return acc; }, {}); } return []; }); const allNestedRowIds = computed(() => { if (!isNestedContentShown.value) { return undefined; } if (props.hasNestedContent && props.shouldShowNestedContent(props.rowData)) { return [nestedContentSlotId]; } if (shouldShowRenderNestedRows.value && Object.keys(nestedRowIds.value)?.length > 0) { const generatedRowIds = Object.values(nestedRowIds.value); if (generatedRowIds?.length > 0) { return generatedRowIds; } } return []; }); const hasUnSelectedNestedRows = computed(() => { const childrendDataItemIds = props.rowData?.children?.map(nestedItem => props.getDataItemId(nestedItem)); if (childrendDataItemIds?.length > 0) { const everyChildIsSelected = childrendDataItemIds.every(childId => props?.selectedItems?.includes(childId)); const someItemsAreSelected = childrendDataItemIds.some(childId => props?.selectedItems?.includes(childId)); return !everyChildIsSelected && someItemsAreSelected; } return false; }); // Methods const registerRowCellRef = (el: Element | ComponentPublicInstance, field: string) => { // We always know this will be used on a DataGridCell component so we can cast it const gridCellComponent = el as InstanceType<typeof DataGridCell>; if (el !== null && gridCellComponent?.gridcellRef) { gridcellRefs.value[field] = gridCellComponent.gridcellRef as HTMLDivElement; } }; const registerNestedRowRef = (el: Element | ComponentPublicInstance, childRowIndex: number) => { if (el !== null) { nestedRowRefs.value[childRowIndex] = el as InstanceType<typeof DataGridRow>; } }; const getRowId = (childRowIndex: number) => { return nestedRowIds.value[childRowIndex]; }; const getRowAriaSelectedValue = (row: BentoDatagridDataItem): Booleanish => { if (!props.selectable) { return null; } const id = props.getDataItemId(row); return props.selectedItems.includes(id).toString() as Booleanish; }; const emitRowClick = (rowData: BentoDatagridDataItem, event: MouseEvent | KeyboardEvent) => { emit('row-click', rowData, event); }; /** * Handles row clicks when the `allowRowClicks` prop is true. * * This method also prevents the `row-click` event from firing if the click * originated from an interactive element (like a button, input, or link) * within the row. This is to avoid unintended behavior when a user * interacts with controls inside a row. * * @param event The mouse or keyboard event from the click or key press. */ const onRowClick = (event: MouseEvent | KeyboardEvent) => { // Only proceed if allowRowClicks is true if (!props.allowRowClicks) { return; } let isEventTargetInteractiveElement = false; const dataGridRowInteractiveTypes = ['button', 'input', 'a']; for (let i = 0; i < dataGridRowInteractiveTypes.length; i++) { if ((event?.target as HTMLElement)?.closest(dataGridRowInteractiveTypes[i])) { isEventTargetInteractiveElement = true; break; } } // Only trigger if element is NOT interactive if (!isEventTargetInteractiveElement) { emitRowClick(props.rowData, event); } }; const onRowSelect = (value: Array<BentoDatagridDataItemId>) => { emit('select-row', value, props.rowData); }; const onNestedRowSelect = (rowData: BentoDatagridDataItem, value: Array<BentoDatagridDataItemId>) => { emit('select-nested-row', value, props.rowData, rowData); }; /** * Sets the state to indicate that a row action menu is open. This is used to apply a higher * z-index to the row's action container, ensuring the menu displays correctly above * subsequent rows. */ const onRowActionMenuOpen = () => { isRowActionMenuOpen.value = true; }; /** * Resets the state when a row action menu is closed, removing the elevated z-index from the * row's action container. */ const onRowActionMenuClose = () => { isRowActionMenuOpen.value = false; }; /** * When the row actions cell is focused, determine navigation direction and focus * the appropriate interactive child. Backward navigation (Shift+Tab) focuses the * last child; forward navigation focuses the first. */ const onRowActionsCellFocus = (e: FocusEvent) => { if (isExitingRowActionInteraction.value) { isExitingRowActionInteraction.value = false; return; } const container = getRowActionsElement(); if (!container || !focusableItems.value?.length) { return; } const isBackwardNavigation = isRelatedTargetAfterInDOM(e.relatedTarget as HTMLElement, container); const lastIndex = focusableItems.value.length - 1; const targetItem = isBackwardNavigation ? focusableItems.value[lastIndex] : focusableItems.value[0]; makeAllInteractiveChildrenFocusable([targetItem]); focusOnInteractiveChild(container, { last: isBackwardNavigation }); }; /** * When focus leaves the row actions cell, make all interactive children unfocusable. * This ensures that tabbing through the grid skips over the individual actions within the cell. */ const onRowActionsCellFocusOut = () => { const container = getRowActionsElement(); if (container) { makeAllInteractiveChildrenUnFocusable(container); } }; /** * Handles keyboard navigation within the row actions cell. * Allows using left and right arrow keys to move between interactive elements (e.g., buttons) in the cell. * @param e The keyboard event. */ const onRowActionsCellOnKeyDown = (e: KeyboardEvent) => { const container = getRowActionsElement(); // Escape returns focus to the gridcell container if (e.key === EventKey.ESCAPE) { if (container) { makeAllInteractiveChildrenUnFocusable(container); } isExitingRowActionInteraction.value = true; container?.focus(); e.stopPropagation(); e.preventDefault(); return; } if (e.key !== EventKey.TAB && container) { makeAllInteractiveChildrenUnFocusable(container); } if ( e.key === EventKey.ARROW_RIGHT && focusableItems.value?.length > 1 && focusableItems.value[0].node === document.activeElement ) { if (container) { makeAllInteractiveChildrenUnFocusable(container); } focusableItems.value[1].node.setAttribute('tabindex', '0'); focusableItems.value[1].node.focus(); e.stopPropagation(); e.preventDefault(); } else if ( e.key === EventKey.ARROW_LEFT && focusableItems.value?.length > 1 && focusableItems.value[1].node === document.activeElement ) { if (container) { makeAllInteractiveChildrenUnFocusable(container); } focusableItems.value[0].node.setAttribute('tabindex', '0'); focusableItems.value[0].node.focus(); e.stopPropagation(); e.preventDefault(); } }; // Cell and Columns index calculation const sumColumnAmountsInColumnSets = () => props.columnSets.reduce((total, { columnSet }) => total + columnSet.length, 0); const getRowActionsCellIndex = () => sumColumnAmountsInColumnSets() * props.rowIndex + sumColumnAmountsInColumnSets(); // Nested content expanding logic const { updateContentHeight, contentHeight } = useExpandableContentHeight(nestedContentRef, isNestedContentShown); const nestedContentStyles = computed(() => { if (props.hasNestedContent || props.hasNestedRows) { // calculate the padding by getting the action column width and the first column left padding: const paddingLeft = shouldShowRenderNestedRows.value || isLoadingNestedRows.value ? 0 : gridcellRefs.value[DATAGRID_ACTIONS_COLUMN_FIELD_NAME].offsetWidth + DATAGRID_CELL_OFFSET_PADDING; return { paddingLeft: `${paddingLeft}px`, 'max-height': (props.hasNestedContent || props.hasNestedRows) && contentHeight.value && !contentHeight?.value?.includes('null') ? contentHeight.value : 0, }; } return undefined; }); watch( () => nestedContentRef.value, () => { let observer: ResizeObserver | null = null; if (!observer && nestedContentRef.value) { observer = observeSizeOfElement(nestedContentRef.value, () => { if (nestedContentRef.value) { updateContentHeight(); } }); } } ); const nestedContentToggled = () => { isNestedContentShown.value = !isNestedContentShown.value; updateContentHeight(); emit('nested-content-toggled', props.rowData); }; watch( () => props.rowData?.children, (_newChildren, oldChildren) => { if (props?.rowData?.children?.length > 0 && oldChildren === undefined && !isNestedContentShown.value) { updateContentHeight(); } } ); const isSmallContainer: ComputedRef<boolean> = inject( DATA_GRID_SMALL_CONTAINER, computed(() => false) ); const isHighlightedRow = row => props.highlightedRows.includes(props.getDataItemId(row)); const isSelectedRow = row => props.selectedItems.includes(props.getDataItemId(row)); // Styling const conditionalRowClasses = (row: BentoDatagridDataItem, rowIndex: number) => ({ 'b-data-grid-row-container__row--disabled': props.isDataItemDisabled(row), 'b-data-grid-row-container__row--row-clicks-enabled': props.allowRowClicks, 'b-data-grid-row-container__row--highlighted': isHighlightedRow(row), 'b-data-grid-row-container__row--selected': isSelectedRow(row), 'b-data-grid-row-container__row--hovered': props.hoveredRowIndex === rowIndex, 'b-data-grid-row-container__row--is-last-child': props.isRowLastChild && !isNestedContentShown.value, 'b-data-grid-row-container__row--nested-row': props.isNestedRow, 'b-data-grid-row-container__row--zebra': props.isRowEven, }); const conditionalRowSetClasses = (isFrozenRowSet: boolean, isSmallContainer: boolean) => ({ 'b-data-grid-row-container__row-cell-set--frozen': isFrozenRowSet && !isSmallContainer, [`b-data-grid-row-container__row-cell-set--${BentoDatagridStyleState.OVERFLOW_SHADOW_LEFT}`]: !isSmallContainer && isFrozenRowSet && props.hasLeftOverflow, 'b-data-grid-row-container__row-cell-set--frozen-separator': !isSmallContainer && isFrozenRowSet && props.showFrozenColumnsSeparator, 'b-data-grid-row-container__row-cell-set--unfrozen': !isFrozenRowSet || isSmallContainer, 'b-data-grid-row-container__row-cell-set--is-measuring': props.isMeasuringColumnWidths, }); const conditionalNestedRowClasses = computed(() => ({ 'b-data-grid-row-container__row-nested-slot--is-last-child': props.isRowLastChild, 'b-data-grid-row-container__row-nested-slot': !shouldShowRenderNestedRows.value && props.shouldShowNestedContent(props.rowData), })); const conditionalRowActionsContainerClasses = computed(() => ({ 'b-data-grid-row-container__row-actions-container--row-actions-menu-open': isRowActionMenuOpen.value, })); const computedRowActionsStyles = computed(() => ({ minWidth: `${props.rowActionsMaximumWidth}px`, })); // Exposed refs defineExpose({ gridcellRefs, isNestedContentShown, nestedRowRefs, rowActionsRef, }); </script> <script lang="ts"> /** * Internal component to display the BentoDataGrid data rows containig the data row cells. * * @example * * export default { * components: { DataGridRow }, * template: ` * <data-grid-row * :ref="el => registerRowCellRefs(el, rowIndex)" * :style="gridColumnWidthStyle" * :allow-row-clicks="allowRowClicks" * :column-sets="columnSets" * :condensed="condensed" * :dragging-columns="draggingColumns" * :dragging-events-for-row-cell="draggingEventsForRowCell" * :get-data-item-id="getDataItemId" * :has-left-overflow="hasLeftOverflow" * :highlighted-rows="highlightedRows" * :hovered-row-index="hoveredRowIndex" * :hovered-over-column-state="hoveredOverColumnState" * :is-data-item-disabled="isDataItemDisabled" * :is-measuring-column-widths="isMeasuringColumnWidths" * :row-data="rowData" * :row-index="rowIndex" * :show-frozen-columns-separator="showFrozenColumnsSeparator" * :selectable="selectable" * :selected-items="selectedItems" * @cell-hovered="onCellHover" * @row-click="onRowClick" * @on-frozen-columns-key-press="updateScrollPositionOnFrozenColumnsNavigation" * @select-row="onRowSelect" * > * <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> * <slot :name="innerSlot" v-bind="scope" /> * </template> * </data-grid-row> * ` */ export default { name: 'data-grid-row', }; </script> <style lang="scss" scoped src="./data-grid-row.scss" />
|
|
1
|
+
<template> <div class="b-data-grid-row-container" role="presentation"> <div v-bind="ariaRowAttributes" :id="rowId?.toString()" :key="rowIndex" class="b-data-grid-row-container__row" :class="conditionalRowClasses(rowData, rowIndex)" role="row" :aria-selected="getRowAriaSelectedValue(rowData)" @click="onRowClick" @keyup.enter="onRowClick" @keyup.space="onRowClick" > <template v-for="({ columnSet, id, isFrozenColumns }, columnSetIndex) of columnSets"> <div v-if="columnSet?.length" :key="`data-grid-rows-${id}`" class="b-data-grid-row-container__cells" :class="conditionalRowSetClasses(isFrozenColumns, isSmallContainer)" :data-testid="`data-grid-${id}-rows`" > <data-grid-cell v-for="(column, colIndex) in columnSet" :key="`cell-${column.field}`" :ref="el => registerRowCellRef(el, column.field)" class="b-data-grid-row-container__cell" :column="column" :column-set-index="columnSetIndex" :col-index="colIndex" :column-sets="columnSets" :condensed="condensed" :dragging-columns="draggingColumns" :dragging-events-for-row-cell="draggingEventsForRowCell" :get-data-item-id="getDataItemId" :has-zebra-background="isRowEven" :is-highlighted="isHighlightedRow(rowData)" :is-selected="isSelectedRow(rowData)" :hovered-over-column-state="hoveredOverColumnState" :is-data-item-disabled="isDataItemDisabled" :is-measuring-column-widths="isMeasuringColumnWidths" :is-nested-content-expanded="isNestedContentShown" :nested-content="shouldShowNestedContentToggle" :nested-rows-element-ids="allNestedRowIds" :row-data="rowData" :row-index="rowIndex" :selection-is-indeterminate="hasUnSelectedNestedRows" :selectable="selectable" :selected-items="selectedItems" @cell-hovered="emit('cell-hovered', $event)" @cell-focus="emit('cell-focus', $event)" @on-frozen-columns-key-press="emit('on-frozen-columns-key-press', $event)" @select-row="onRowSelect" @nested-content-toggled="nestedContentToggled" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-cell> </div> </template> <div v-if="rowActions" ref="rowActionsRef" :aria-colindex="sumColumnAmountsInColumnSets().toString()" class="b-data-grid-row-container__row-actions-container" :class="conditionalRowActionsContainerClasses" :style="computedRowActionsStyles" :data-cellindex="getRowActionsCellIndex()" tabindex="-1" role="gridcell" @focus="onRowActionsCellFocus" @focusOut="onRowActionsCellFocusOut" @keydown="onRowActionsCellOnKeyDown" > <data-grid-cell-actions class="b-data-grid-row-container__row-actions" disable-arrow-down-opening-menu :cell-actions="rowActions" :has-right-overflow="hasRightOverflow" :row-data-item="rowData" :has-zebra-background="isRowEven" :is-highlighted="isHighlightedRow(rowData)" :is-selected="isSelectedRow(rowData)" teleport-menu @menu-open="onRowActionMenuOpen" @menu-close="onRowActionMenuClose" /> </div> </div> <Transition v-if=" (hasNestedContent && hasSlot('nested-slot') && shouldShowNestedContent(rowData)) || (hasNestedRows && shouldShowNestedRowsBooleanResult) " :key="`${rowIndex}-nest-content`" name="b-data-grid-row-container__animation--nested-content" > <div v-if="isNestedContentShown && shouldShowRenderNestedRows" :class="conditionalNestedRowClasses" data-testid="nested-content-container" :style="nestedContentStyles" role="presentation" > <div ref="nestedContentRef"> <div v-if="isLoadingNestedRows" class="b-data-grid-row-container__row-nested-rows-loading-container" > <bento-loading-indicator /> </div> <data-grid-row v-for="(childRowData, childRowIndex) in rowData.children" v-bind="props" :id="getRowId(childRowIndex)" :key="`child-${childRowIndex}`" :ref="el => registerNestedRowRef(el, childRowIndex)" :row-data="childRowData" :row-index="childRowIndex" :row-set-size="rowData.children.length" is-nested-row @cell-hovered="emit('cell-hovered', $event)" @cell-focus="emit('cell-focus', $event)" @row-click="emitRowClick" @on-frozen-columns-key-press="emit('on-frozen-columns-key-press', $event)" @select-row="onNestedRowSelect(childRowData, $event)" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-row> </div> </div> <div v-else-if="isNestedContentShown" v-bind="nestedContentContainerDivAriaAttributes" :class="conditionalNestedRowClasses" data-testid="nested-content-container" :style="nestedContentStyles" role="row" tabindex="-1" > <div ref="nestedContentRef" role="gridcell"> <div v-if="isLoadingNestedRows" class="b-data-grid-row-container__row-nested-rows-loading-container" > <bento-loading-indicator /> </div> <div v-else-if="hasSlot('nested-slot') && shouldShowNestedContent(rowData)"> <slot :item="rowData" name="nested-slot" /> </div> </div> </div> </Transition> </div> </template> <script setup lang="ts"> import { type ComponentPublicInstance, computed, type ComputedRef, inject, onMounted, type PropType, ref, useSlots, 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" />
|