@adyen/bento-mcp 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +41 -9
  3. package/dist/assets/components/accordion/components/accordion-item.vue +1 -1
  4. package/dist/assets/components/card/card.vue +1 -1
  5. package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.vue +1 -1
  6. package/dist/assets/components/draggable/draggable.docs.mdx +76 -135
  7. package/dist/assets/components/draggable/draggable.stories.ts +1 -1
  8. package/dist/assets/components/draggable/draggable.types.ts +1 -1
  9. package/dist/assets/components/draggable/draggable.vue +1 -1
  10. package/dist/assets/components/drawer/drawer.docs.mdx +68 -0
  11. package/dist/assets/components/drawer/drawer.stories.ts +1 -0
  12. package/dist/assets/components/drawer/drawer.types.ts +1 -0
  13. package/dist/assets/components/drawer/drawer.vue +1 -0
  14. package/dist/assets/components/filter-bar/components/base-filter/base-filter.vue +1 -1
  15. package/dist/assets/components/input-field/input-field.types.ts +1 -1
  16. package/dist/assets/components/input-field/input-field.vue +1 -1
  17. package/dist/assets/components/internal/calendar/calendar.types.ts +1 -1
  18. package/dist/assets/components/internal/calendar/calendar.vue +1 -1
  19. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.stories.ts +1 -1
  20. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.types.ts +1 -1
  21. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.vue +1 -1
  22. package/dist/assets/components/internal/calendar/components/calendar-month/composables/granular-min-max-date.types.ts +1 -1
  23. package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.types.ts +1 -1
  24. package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.vue +1 -1
  25. package/dist/assets/components/internal/teleport/teleport.vue +1 -1
  26. package/dist/assets/components/modal/modal.vue +1 -1
  27. package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.stories.ts +1 -0
  28. package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.types.ts +1 -0
  29. package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.vue +1 -0
  30. package/dist/assets/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.stories.ts +1 -0
  31. package/dist/assets/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.types.ts +1 -0
  32. package/dist/assets/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.vue +1 -0
  33. package/dist/assets/components/navigation-menu/navigation-menu.docs.mdx +321 -0
  34. package/dist/assets/components/navigation-menu/navigation-menu.stories.ts +1 -0
  35. package/dist/assets/components/navigation-menu/navigation-menu.types.ts +1 -0
  36. package/dist/assets/components/navigation-menu/navigation-menu.vue +1 -0
  37. package/dist/assets/components/secondary-nav/components/secondary-nav-category/secondary-nav-category.vue +1 -1
  38. package/dist/assets/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue +1 -1
  39. package/dist/assets/components/secondary-nav/secondary-nav.stories.ts +1 -1
  40. package/dist/assets/components/secondary-nav/secondary-nav.types.ts +1 -1
  41. package/dist/assets/components/secondary-nav/secondary-nav.vue +1 -1
  42. package/dist/assets/components/tabs/components/tab.types.ts +1 -1
  43. package/dist/assets/components/tabs/components/tab.vue +1 -1
  44. package/dist/assets/components/tabs/tabs.stories.ts +1 -1
  45. package/dist/assets/components/tabs/tabs.types.ts +1 -1
  46. package/dist/assets/components/tabs/tabs.vue +1 -1
  47. package/dist/assets/components.json +5 -0
  48. package/dist/assets/index.ts +1 -1
  49. package/dist/assets/usage.json +13 -8
  50. package/dist/assets/variables.css +3 -0
  51. package/dist/main.js +1 -1
  52. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.2 (2026-05-06)
4
+
5
+ This was a version bump only for mcp to align it with other projects, there were no code changes.
6
+
7
+
8
+ ## 0.5.1 (2026-04-29)
9
+
10
+ ### Documentation
11
+
12
+ - updated mcp tools documentation with all new tools ([a027b8b03](https://github.com/Adyen/bento/commit/a027b8b03))
13
+
14
+ ### ❤️ Thank You
15
+
16
+ - gerald
17
+
18
+
3
19
  ## 0.5.0 (2026-04-22)
4
20
 
5
21
  ### Features
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, and Vue implementation for a list of components.
77
+ Retrieves the documentation, stories, Vue implementation, and type definitions for a list of components.
78
78
 
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`, and `.vue` files
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 extracted from the source
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
- <template> <div class="b-accordion-item"> <bento-typography el="h3" :variant="titleSize === 'small' ? 'body' : 'title'" :strongest="titleSize === 'small'" > <button :id="buttonId" :aria-controls="contentId" :aria-expanded="`${isOpen}`" type="button" class="b-accordion-item__button" @click="isOpen = !isOpen" > <chevron-up-icon v-if="isOpen" class="b-accordion-item__icon" :svg-title="t('collapse')" aria-hidden="true" /> <chevron-down-icon v-else class="b-accordion-item__icon" :svg-title="t('expand')" aria-hidden="true" /> {{ title }} </button> </bento-typography> <Transition :name="CONTENT_TRANSITION_NAME"> <div v-if="isOpen" :id="contentId" :aria-labelledby="buttonId" role="region" :style="contentMaxHeight"> <div ref="contentDiv" class="b-accordion-item__content"> <slot /> </div> </div> </Transition> </div> </template> <script setup lang="ts"> import { computed, inject, ref, watch } from 'vue'; import { BentoTypography } from '@/components/typography'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import ChevronUpIcon from '@adyen/ui-assets-icons-16/vue/chevron-up'; import { generateUid } from '@/core/utils/ts'; import { observeSizeOfElement } from '@/utils/ts/resize'; import { useI18n } from '@/utils/ts/i18n'; import { ACCORDION_TITLE_SIZE_INJECTION_KEY } from '../accordion.keys'; import type { BentoAccordionTitleSize } from '../accordion.types'; import { useExpandableContentHeight } from '@/composables/use-expandable-content-height/use-expandable-content-height'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const CONTENT_TRANSITION_NAME = 'b-accordion-item__animation--content'; const props = defineProps({ /** * Sets the title of the accordion item. */ title: { type: String, required: true, }, /** * Controls if the content is visible or hidden. */ isExpanded: { type: Boolean, default: false, }, }); const emit = defineEmits<{ /** * Emits update event when content is toggled */ (e: 'update:is-expanded', value: boolean): void; }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); // Refs const contentDiv = ref<HTMLDivElement>(null); const isOpen = ref(props.isExpanded); // Generating IDs const buttonId = generateUid('accordionItemButton'); const contentId = generateUid('accordionItemContent'); // Injecting prop from parent const titleSize = inject<`${BentoAccordionTitleSize}`>(ACCORDION_TITLE_SIZE_INJECTION_KEY, 'default'); // Getting and setting the item height to be able to handle height animation const { updateContentHeight, contentHeight } = useExpandableContentHeight(contentDiv, isOpen); const contentMaxHeight = computed(() => (contentHeight.value ? { 'max-height': contentHeight.value } : {})); watch( () => props.isExpanded, value => { isOpen.value = value; } ); watch( () => contentDiv.value, () => { let observer: ResizeObserver | null = null; if (!observer && contentDiv.value) { observer = observeSizeOfElement(contentDiv.value, () => { if (contentDiv.value) { updateContentHeight(); } }); } } ); watch( isOpen, value => { // Enable two-way data binding for `isExpanded` emit('update:is-expanded', value); updateContentHeight(); }, { immediate: true } ); </script> <script lang="ts"> /** * An accordion item is an interactive header used to reveal or hide a * section of content associated with this header. * * This component must always be wrapped by a `BentoAccordion` component. * * @example * import { BentoAccordion, BentoAccordionItem } from '@adyen/bento-vue2'; * * export default { * components: { BentoAccordion, BentoAccordionItem }, * template: ` * <bento-accordion> * <bento-accordion-item title="Item 1"> * Content for item 1 * </bento-accordion-item> * <bento-accordion-item title="Item 2"> * Content for item 2 * </bento-accordion-item> * </bento-accordion> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./accordion-item.scss" />
1
+ <template> <div class="b-accordion-item"> <bento-typography el="h3" :variant="titleSize === 'small' ? 'body' : 'title'" :strongest="titleSize === 'small'" > <button :id="buttonId" :aria-controls="contentId" :aria-expanded="`${isOpen}`" type="button" class="b-accordion-item__button" @click="isOpen = !isOpen" > <chevron-up-icon v-if="isOpen" class="b-accordion-item__icon" :svg-title="t('collapse')" aria-hidden="true" /> <chevron-down-icon v-else class="b-accordion-item__icon" :svg-title="t('expand')" aria-hidden="true" /> {{ title }} </button> </bento-typography> <Transition :name="CONTENT_TRANSITION_NAME"> <div v-if="isOpen" :id="contentId" :aria-labelledby="buttonId" role="region" :style="contentMaxHeight"> <div ref="contentDiv" class="b-accordion-item__content"> <slot /> </div> </div> </Transition> </div> </template> <script setup lang="ts"> import { computed, inject, ref, watch } from 'vue'; import { BentoTypography } from '@/components/typography'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import ChevronUpIcon from '@adyen/ui-assets-icons-16/vue/chevron-up'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { ACCORDION_TITLE_SIZE_INJECTION_KEY } from '../accordion.keys'; import type { BentoAccordionTitleSize } from '../accordion.types'; import { useExpandableContentHeight } from '@/composables/use-expandable-content-height/use-expandable-content-height'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const CONTENT_TRANSITION_NAME = 'b-accordion-item__animation--content'; const props = defineProps({ /** * Sets the title of the accordion item. */ title: { type: String, required: true, }, /** * Controls if the content is visible or hidden. */ isExpanded: { type: Boolean, default: false, }, }); const emit = defineEmits<{ /** * Emits update event when content is toggled */ (e: 'update:is-expanded', value: boolean): void; }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); // Refs const contentDiv = ref<HTMLDivElement>(null); const isOpen = ref(props.isExpanded); // Generating IDs const buttonId = generateUid('accordionItemButton'); const contentId = generateUid('accordionItemContent'); // Injecting prop from parent const titleSize = inject<`${BentoAccordionTitleSize}`>(ACCORDION_TITLE_SIZE_INJECTION_KEY, 'default'); // Getting and setting the item height to be able to handle height animation const { contentHeight } = useExpandableContentHeight(contentDiv, isOpen); const contentMaxHeight = computed(() => (contentHeight.value ? { 'max-height': contentHeight.value } : {})); watch( () => props.isExpanded, value => { isOpen.value = value; } ); watch( isOpen, value => { // Enable two-way data binding for `isExpanded` emit('update:is-expanded', value); }, { immediate: true } ); </script> <script lang="ts"> /** * An accordion item is an interactive header used to reveal or hide a * section of content associated with this header. * * This component must always be wrapped by a `BentoAccordion` component. * * @example * import { BentoAccordion, BentoAccordionItem } from '@adyen/bento-vue2'; * * export default { * components: { BentoAccordion, BentoAccordionItem }, * template: ` * <bento-accordion> * <bento-accordion-item title="Item 1"> * Content for item 1 * </bento-accordion-item> * <bento-accordion-item title="Item 2"> * Content for item 2 * </bento-accordion-item> * </bento-accordion> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./accordion-item.scss" />
@@ -1 +1 @@
1
- <template> <div class="b-card" :class="cardConditionalClasses" :tabindex="tabIndex" data-testid="card" @click="onClick" @keydown.enter="onKeyDown" @keydown.space="onKeyDown" > <div v-if="hasSlot('image')" class="b-card__image"> <slot name="image"></slot> </div> <div v-if="showHeader" class="b-card__header" :class="cardHeaderConditionalClasses" :tabindex="headerTabIndex" :aria-controls="expandable && showCardBody ? cardId : null" :aria-expanded="expandable ? `${showContent}` : null" :role="expandable ? 'button' : null" @click="toggleExpansion" @keypress.enter="toggleExpansion" @keypress.space="toggleExpansion" > <div class="b-card__header-wrapper"> <div v-if="showTitleWrapper" class="b-card__header-slot-wrapper"> <div class="b-card__title-wrapper" :class="cardTitleWrapperConditionalClasses"> <div v-if="expandable" class="b-card__toggle" :class="cardToggleConditionalClasses"> <chevron-up-icon v-if="showContent" :svg-title="t('hideContent')" aria-hidden="true" /> <chevron-down-icon v-else :svg-title="t('showContent')" aria-hidden="true" /> </div> <bento-typography v-if="hasSlot('default')" class="b-card__title" v-bind="typography"> <slot /> </bento-typography> </div> <template v-if="hasSlot('header')"> <slot name="header"></slot> </template> </div> <bento-typography v-if="hasSlot('description')" class="b-card__description" el="div" variant="body"> <slot name="description" /> </bento-typography> </div> </div> <Transition name="b-card__animation"> <div v-if="showCardBody" class="b-card__wrapper" :style="contentMaxHeight"> <div :id="expandable ? cardId : null" ref="contentDiv" class="b-card__body" :class="cardBodyConditionalClasses" > <div v-if="hasSlot('content')" class="b-card__content" :class="cardContentConditionalClasses"> <slot name="content" /> </div> <div v-if="actions" class="b-card__actions" :class="cardActionsConditionalClasses"> <bento-button-actions :actions="actions" :layout="actionsLayout"></bento-button-actions> </div> </div> </div> </Transition> </div> </template> <script setup lang="ts"> import { computed, ref, useSlots, watch } from 'vue'; // Components import { BentoTypography } from '@/components/typography'; import { BentoButtonActions, BentoButtonActionsLayout, type BentoButtonActionsList } from '@/components/button'; import { BentoCardBackground, BentoCardTitleSize } from '@/components/card/card.types'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import ChevronUpIcon from '@adyen/ui-assets-icons-16/vue/chevron-up'; // Composables import { useExpandableContentHeight } from '@/composables/use-expandable-content-height/use-expandable-content-height'; // Utils import { useHasSlot } from '@/composables'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { observeSizeOfElement } from '@/utils/ts/resize'; import messages from './messages.json'; import { deprecate } from '@/utils/ts/deprecate'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = withDefaults( defineProps<{ /** * 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`: Vue Icon from the library `@adyen/ui-assets-icons-16`. * * ``` * [{ * title: 'Primary', * event: () => { ... }, * icon: FourPeopleIcon * }, { * title: 'Secondary', * event: () => { ... } * }] * ``` */ actions?: BentoButtonActionsList; /** * 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 */ actionsLayout?: BentoButtonActionsLayout | `${BentoButtonActionsLayout}`; /** * Defines the color of card background */ background?: BentoCardBackground | `${BentoCardBackground}`; /** * Makes the whole card clickable */ clickable?: boolean; /** * Sets the expandable card in a closed state */ closed?: boolean; /** * Disables the expandable card */ disabled?: boolean; /** * The whole content of the card will be expandable in accordion manner */ expandable?: boolean; /** * Defines the title size of the card header */ titleSize?: BentoCardTitleSize | `${BentoCardTitleSize}`; }>(), { actions: null, actionsLayout: BentoButtonActionsLayout.BUTTONS_END, background: BentoCardBackground.PRIMARY, clickable: false, closed: false, disabled: false, expandable: false, titleSize: BentoCardTitleSize.DEFAULT, } ); const emit = defineEmits<{ (e: 'click'); (e: 'expanded', isExpanded: boolean); }>(); const contentDiv = ref<HTMLDivElement>(null); const cardId = generateUid('card'); const showContent = ref(!props.closed); watch( () => props.closed, value => { showContent.value = !value; } ); const showHeader = computed(() => hasSlot('default') || hasSlot('description') || hasSlot('header')); const showTitleWrapper = computed(() => hasSlot('default') || hasSlot('header')); const showCardBody = computed( () => (hasSlot('content') || props.actions?.length) && (!props.expandable || (props.expandable && showContent.value)) ); const tabIndex = computed(() => (props.clickable && !props.disabled ? 0 : -1)); const headerTabIndex = computed(() => (props.expandable && !props.disabled ? 0 : -1)); const cardConditionalClasses = computed(() => ({ [`b-card--${props.background}`]: props.background, 'b-card--clickable': props.clickable, 'b-card--expandable': props.expandable, 'b-card--disabled': (props.clickable || props.expandable) && props.disabled, })); const cardHeaderConditionalClasses = computed(() => ({ 'b-card__header--expandable': props.expandable, 'b-card__header--without-body': !props.expandable && !showCardBody.value, })); const cardToggleConditionalClasses = computed(() => ({ 'b-card__toggle--small-text': props.titleSize === BentoCardTitleSize.SMALL, })); const cardBodyConditionalClasses = computed(() => ({ 'b-card__body--expandable': props.expandable && hasSlot('content'), 'b-card__body--without-header': !props.expandable && !showHeader.value, })); const cardContentConditionalClasses = computed(() => ({ 'b-card__content--without-header': !props.expandable && !showHeader.value, 'b-card__content--expandable': props.expandable, })); const cardActionsConditionalClasses = computed(() => { const isNotExpandableWithoutContent = !props.expandable && props.actions && !hasSlot('content'); return { 'b-card__actions--without-content': isNotExpandableWithoutContent, 'b-card__actions--without-content-and-with-header': isNotExpandableWithoutContent && showHeader.value, }; }); const cardTitleWrapperConditionalClasses = computed(() => ({ 'b-card__title-wrapper--truncated': hasSlot('header'), })); const typography = computed( () => ({ el: 'div', variant: props.titleSize === BentoCardTitleSize.DEFAULT ? 'title' : 'body', strongest: props.titleSize === BentoCardTitleSize.SMALL, wide: props.titleSize === BentoCardTitleSize.SMALL, }) as InstanceType<typeof BentoTypography>['$props'] ); // Toggle open and close logic and animation const { updateContentHeight, contentHeight } = useExpandableContentHeight(contentDiv, showContent); const contentMaxHeight = computed(() => props.expandable && contentHeight.value ? { 'max-height': contentHeight.value } : {} ); watch( () => contentDiv.value, () => { let observer: ResizeObserver | null = null; if (!observer && contentDiv.value) { observer = observeSizeOfElement(contentDiv.value, () => { if (contentDiv.value) { updateContentHeight(); } }); } } ); const toggleExpansion = () => { if (!props.disabled && props.expandable) { showContent.value = !showContent.value; emit('expanded', showContent.value); updateContentHeight(); } }; const onClick = () => { if (!props.disabled) { emit('click'); if (!props.clickable) { deprecate( 'BentoCard "@click" property', `Use the BentoCard "@click" with the "clickable" property to enable accessible styles.`, '2.0.0' ); } } }; const onKeyDown = () => { if (props.clickable && !props.disabled) { emit('click'); } }; </script> <script lang="ts"> /** * A card is a flexible and composable container. * Use cards to represent a single object or to visually group certain content, like legal information within company settings. * * @example * import { BentoCard } from '@adyen/bento-vue2'; * * export default { * components: { BentoCard }, * template: ` * <bento-card> * Title of the Card * </bento-card> * ` * }; */ export default { i18n: { messages }, name: 'bento-card', }; </script> <style lang="scss" scoped src="./card.scss" />
1
+ <template> <div class="b-card" :class="cardConditionalClasses" :tabindex="tabIndex" data-testid="card" @click="onClick" @keydown.enter="onKeyDown" @keydown.space="onKeyDown" > <div v-if="hasSlot('image')" class="b-card__image"> <slot name="image"></slot> </div> <div v-if="showHeader" class="b-card__header" :class="cardHeaderConditionalClasses" :tabindex="headerTabIndex" :aria-controls="expandable && showCardBody ? cardId : null" :aria-expanded="expandable ? `${showContent}` : null" :role="expandable ? 'button' : null" @click="toggleExpansion" @keypress.enter="toggleExpansion" @keypress.space="toggleExpansion" > <div class="b-card__header-wrapper"> <div v-if="showTitleWrapper" class="b-card__header-slot-wrapper"> <div class="b-card__title-wrapper" :class="cardTitleWrapperConditionalClasses"> <div v-if="expandable" class="b-card__toggle" :class="cardToggleConditionalClasses"> <chevron-up-icon v-if="showContent" :svg-title="t('hideContent')" aria-hidden="true" /> <chevron-down-icon v-else :svg-title="t('showContent')" aria-hidden="true" /> </div> <bento-typography v-if="hasSlot('default')" class="b-card__title" v-bind="typography"> <slot /> </bento-typography> </div> <template v-if="hasSlot('header')"> <slot name="header"></slot> </template> </div> <bento-typography v-if="hasSlot('description')" class="b-card__description" el="div" variant="body"> <slot name="description" /> </bento-typography> </div> </div> <Transition name="b-card__animation"> <div v-if="showCardBody" class="b-card__wrapper" :style="contentMaxHeight"> <div :id="expandable ? cardId : null" ref="contentDiv" class="b-card__body" :class="cardBodyConditionalClasses" > <div v-if="hasSlot('content')" class="b-card__content" :class="cardContentConditionalClasses"> <slot name="content" /> </div> <div v-if="actions" class="b-card__actions" :class="cardActionsConditionalClasses"> <bento-button-actions :actions="actions" :layout="actionsLayout"></bento-button-actions> </div> </div> </div> </Transition> </div> </template> <script setup lang="ts"> import { computed, ref, useSlots, watch } from 'vue'; // Components import { BentoTypography } from '@/components/typography'; import { BentoButtonActions, BentoButtonActionsLayout, type BentoButtonActionsList } from '@/components/button'; import { BentoCardBackground, BentoCardTitleSize } from '@/components/card/card.types'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; import ChevronUpIcon from '@adyen/ui-assets-icons-16/vue/chevron-up'; // Composables import { useExpandableContentHeight } from '@/composables/use-expandable-content-height/use-expandable-content-height'; // Utils import { useHasSlot } from '@/composables'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import messages from './messages.json'; import { deprecate } from '@/utils/ts/deprecate'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = withDefaults( defineProps<{ /** * 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`: Vue Icon from the library `@adyen/ui-assets-icons-16`. * * ``` * [{ * title: 'Primary', * event: () => { ... }, * icon: FourPeopleIcon * }, { * title: 'Secondary', * event: () => { ... } * }] * ``` */ actions?: BentoButtonActionsList; /** * 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 */ actionsLayout?: BentoButtonActionsLayout | `${BentoButtonActionsLayout}`; /** * Defines the color of card background */ background?: BentoCardBackground | `${BentoCardBackground}`; /** * Makes the whole card clickable */ clickable?: boolean; /** * Sets the expandable card in a closed state */ closed?: boolean; /** * Disables the expandable card */ disabled?: boolean; /** * The whole content of the card will be expandable in accordion manner */ expandable?: boolean; /** * Defines the title size of the card header */ titleSize?: BentoCardTitleSize | `${BentoCardTitleSize}`; }>(), { actions: null, actionsLayout: BentoButtonActionsLayout.BUTTONS_END, background: BentoCardBackground.PRIMARY, clickable: false, closed: false, disabled: false, expandable: false, titleSize: BentoCardTitleSize.DEFAULT, } ); const emit = defineEmits<{ (e: 'click'); (e: 'expanded', isExpanded: boolean); }>(); const contentDiv = ref<HTMLDivElement>(null); const cardId = generateUid('card'); const showContent = ref(!props.closed); watch( () => props.closed, value => { showContent.value = !value; } ); const showHeader = computed(() => hasSlot('default') || hasSlot('description') || hasSlot('header')); const showTitleWrapper = computed(() => hasSlot('default') || hasSlot('header')); const showCardBody = computed( () => (hasSlot('content') || props.actions?.length) && (!props.expandable || (props.expandable && showContent.value)) ); const tabIndex = computed(() => (props.clickable && !props.disabled ? 0 : -1)); const headerTabIndex = computed(() => (props.expandable && !props.disabled ? 0 : -1)); const cardConditionalClasses = computed(() => ({ [`b-card--${props.background}`]: props.background, 'b-card--clickable': props.clickable, 'b-card--expandable': props.expandable, 'b-card--disabled': (props.clickable || props.expandable) && props.disabled, })); const cardHeaderConditionalClasses = computed(() => ({ 'b-card__header--expandable': props.expandable, 'b-card__header--without-body': !props.expandable && !showCardBody.value, })); const cardToggleConditionalClasses = computed(() => ({ 'b-card__toggle--small-text': props.titleSize === BentoCardTitleSize.SMALL, })); const cardBodyConditionalClasses = computed(() => ({ 'b-card__body--expandable': props.expandable && hasSlot('content'), 'b-card__body--without-header': !props.expandable && !showHeader.value, })); const cardContentConditionalClasses = computed(() => ({ 'b-card__content--without-header': !props.expandable && !showHeader.value, 'b-card__content--expandable': props.expandable, })); const cardActionsConditionalClasses = computed(() => { const isNotExpandableWithoutContent = !props.expandable && props.actions && !hasSlot('content'); return { 'b-card__actions--without-content': isNotExpandableWithoutContent, 'b-card__actions--without-content-and-with-header': isNotExpandableWithoutContent && showHeader.value, }; }); const cardTitleWrapperConditionalClasses = computed(() => ({ 'b-card__title-wrapper--truncated': hasSlot('header'), })); const typography = computed( () => ({ el: 'div', variant: props.titleSize === BentoCardTitleSize.DEFAULT ? 'title' : 'body', strongest: props.titleSize === BentoCardTitleSize.SMALL, wide: props.titleSize === BentoCardTitleSize.SMALL, }) as InstanceType<typeof BentoTypography>['$props'] ); // Toggle open and close logic and animation const { contentHeight } = useExpandableContentHeight(contentDiv, showContent); const contentMaxHeight = computed(() => props.expandable && contentHeight.value ? { 'max-height': contentHeight.value } : {} ); const toggleExpansion = () => { if (!props.disabled && props.expandable) { showContent.value = !showContent.value; emit('expanded', showContent.value); } }; const onClick = () => { if (!props.disabled) { emit('click'); if (!props.clickable) { deprecate( 'BentoCard "@click" property', `Use the BentoCard "@click" with the "clickable" property to enable accessible styles.`, '2.0.0' ); } } }; const onKeyDown = () => { if (props.clickable && !props.disabled) { emit('click'); } }; </script> <script lang="ts"> /** * A card is a flexible and composable container. * Use cards to represent a single object or to visually group certain content, like legal information within company settings. * * @example * import { BentoCard } from '@adyen/bento-vue2'; * * export default { * components: { BentoCard }, * template: ` * <bento-card> * Title of the Card * </bento-card> * ` * }; */ export default { i18n: { messages }, name: 'bento-card', }; </script> <style lang="scss" scoped src="./card.scss" />
@@ -1 +1 @@
1
- <template> <div class="b-data-grid-row-container" role="presentation"> <div v-bind="ariaRowAttributes" :id="rowId?.toString()" :key="rowIndex" class="b-data-grid-row-container__row" :class="conditionalRowClasses(rowData, rowIndex)" role="row" :aria-selected="getRowAriaSelectedValue(rowData)" @click="onRowClick" @keyup.enter="onRowClick" @keyup.space="onRowClick" > <template v-for="({ columnSet, id, isFrozenColumns }, columnSetIndex) of columnSets"> <div v-if="columnSet?.length" :key="`data-grid-rows-${id}`" class="b-data-grid-row-container__cells" :class="conditionalRowSetClasses(isFrozenColumns, isSmallContainer)" :data-testid="`data-grid-${id}-rows`" > <data-grid-cell v-for="(column, colIndex) in columnSet" :key="`cell-${column.field}`" :ref="el => registerRowCellRef(el, column.field)" class="b-data-grid-row-container__cell" :column="column" :column-set-index="columnSetIndex" :col-index="colIndex" :column-sets="columnSets" :condensed="condensed" :dragging-columns="draggingColumns" :dragging-events-for-row-cell="draggingEventsForRowCell" :get-data-item-id="getDataItemId" :has-zebra-background="isRowEven" :is-highlighted="isHighlightedRow(rowData)" :is-selected="isSelectedRow(rowData)" :hovered-over-column-state="hoveredOverColumnState" :is-data-item-disabled="isDataItemDisabled" :is-measuring-column-widths="isMeasuringColumnWidths" :is-nested-content-expanded="isNestedContentShown" :nested-content="shouldShowNestedContentToggle" :nested-rows-element-ids="allNestedRowIds" :row-data="rowData" :row-index="rowIndex" :selection-is-indeterminate="hasUnSelectedNestedRows" :selectable="selectable" :selected-items="selectedItems" @cell-hovered="emit('cell-hovered', $event)" @cell-focus="emit('cell-focus', $event)" @on-frozen-columns-key-press="emit('on-frozen-columns-key-press', $event)" @select-row="onRowSelect" @nested-content-toggled="nestedContentToggled" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-cell> </div> </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" />
1
+ <template> <div class="b-data-grid-row-container" role="presentation"> <div v-bind="ariaRowAttributes" :id="rowId?.toString()" :key="rowIndex" class="b-data-grid-row-container__row" :class="conditionalRowClasses(rowData, rowIndex)" role="row" :aria-selected="getRowAriaSelectedValue(rowData)" @click="onRowClick" @keyup.enter="onRowClick" @keyup.space="onRowClick" > <template v-for="({ columnSet, id, isFrozenColumns }, columnSetIndex) of columnSets"> <div v-if="columnSet?.length" :key="`data-grid-rows-${id}`" class="b-data-grid-row-container__cells" :class="conditionalRowSetClasses(isFrozenColumns, isSmallContainer)" :data-testid="`data-grid-${id}-rows`" > <data-grid-cell v-for="(column, colIndex) in columnSet" :key="`cell-${column.field}`" :ref="el => registerRowCellRef(el, column.field)" class="b-data-grid-row-container__cell" :column="column" :column-set-index="columnSetIndex" :col-index="colIndex" :column-sets="columnSets" :condensed="condensed" :dragging-columns="draggingColumns" :dragging-events-for-row-cell="draggingEventsForRowCell" :get-data-item-id="getDataItemId" :has-zebra-background="isRowEven" :is-highlighted="isHighlightedRow(rowData)" :is-selected="isSelectedRow(rowData)" :hovered-over-column-state="hoveredOverColumnState" :is-data-item-disabled="isDataItemDisabled" :is-measuring-column-widths="isMeasuringColumnWidths" :is-nested-content-expanded="isNestedContentShown" :nested-content="shouldShowNestedContentToggle" :nested-rows-element-ids="allNestedRowIds" :row-data="rowData" :row-index="rowIndex" :selection-is-indeterminate="hasUnSelectedNestedRows" :selectable="selectable" :selected-items="selectedItems" @cell-hovered="emit('cell-hovered', $event)" @cell-focus="emit('cell-focus', $event)" @on-frozen-columns-key-press="emit('on-frozen-columns-key-press', $event)" @select-row="onRowSelect" @nested-content-toggled="nestedContentToggled" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-cell> </div> </template> <div v-if="rowActions" ref="rowActionsRef" :aria-colindex="sumColumnAmountsInColumnSets().toString()" class="b-data-grid-row-container__row-actions-container" :class="conditionalRowActionsContainerClasses" :style="computedRowActionsStyles" :data-cellindex="getRowActionsCellIndex()" tabindex="-1" role="gridcell" @focus="onRowActionsCellFocus" @focusOut="onRowActionsCellFocusOut" @keydown="onRowActionsCellOnKeyDown" > <data-grid-cell-actions class="b-data-grid-row-container__row-actions" disable-arrow-down-opening-menu :cell-actions="rowActions" :has-right-overflow="hasRightOverflow" :row-data-item="rowData" :has-zebra-background="isRowEven" :is-highlighted="isHighlightedRow(rowData)" :is-selected="isSelectedRow(rowData)" teleport-menu @menu-open="onRowActionMenuOpen" @menu-close="onRowActionMenuClose" /> </div> </div> <Transition v-if=" (hasNestedContent && hasSlot('nested-slot') && shouldShowNestedContent(rowData)) || (hasNestedRows && shouldShowNestedRowsBooleanResult) " :key="`${rowIndex}-nest-content`" name="b-data-grid-row-container__animation--nested-content" > <div v-if="isNestedContentShown && shouldShowRenderNestedRows" :class="conditionalNestedRowClasses" data-testid="nested-content-container" :style="nestedContentStyles" role="presentation" > <div ref="nestedContentRef"> <div v-if="isLoadingNestedRows" class="b-data-grid-row-container__row-nested-rows-loading-container" > <bento-loading-indicator /> </div> <data-grid-row v-for="(childRowData, childRowIndex) in rowData.children" v-bind="props" :id="getRowId(childRowIndex)" :key="`child-${childRowIndex}`" :ref="el => registerNestedRowRef(el, childRowIndex)" :row-data="childRowData" :row-index="childRowIndex" :row-set-size="rowData.children.length" is-nested-row @cell-hovered="emit('cell-hovered', $event)" @cell-focus="emit('cell-focus', $event)" @row-click="emitRowClick" @on-frozen-columns-key-press="emit('on-frozen-columns-key-press', $event)" @select-row="onNestedRowSelect(childRowData, $event)" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-row> </div> </div> <div v-else-if="isNestedContentShown" v-bind="nestedContentContainerDivAriaAttributes" :class="conditionalNestedRowClasses" data-testid="nested-content-container" :style="nestedContentStyles" role="row" tabindex="-1" > <div ref="nestedContentRef" role="gridcell"> <div v-if="isLoadingNestedRows" class="b-data-grid-row-container__row-nested-rows-loading-container" > <bento-loading-indicator /> </div> <div v-else-if="hasSlot('nested-slot') && shouldShowNestedContent(rowData)"> <slot :item="rowData" name="nested-slot" /> </div> </div> </div> </Transition> </div> </template> <script setup lang="ts"> import { type ComponentPublicInstance, computed, type ComputedRef, inject, onMounted, type PropType, ref, useSlots, } from 'vue'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { focusOnInteractiveChild, makeAllInteractiveChildrenFocusable, makeAllInteractiveChildrenUnFocusable, } from '@/components/data-grid/utils'; import 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 { 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 { contentHeight } = useExpandableContentHeight(nestedContentRef, isNestedContentShown); const nestedContentStyles = computed(() => { if (props.hasNestedContent || props.hasNestedRows) { // calculate the padding by getting the action column width and the first column left padding: const paddingLeft = shouldShowRenderNestedRows.value || isLoadingNestedRows.value ? 0 : gridcellRefs.value[DATAGRID_ACTIONS_COLUMN_FIELD_NAME].offsetWidth + DATAGRID_CELL_OFFSET_PADDING; return { paddingLeft: `${paddingLeft}px`, 'max-height': (props.hasNestedContent || props.hasNestedRows) && contentHeight.value && !contentHeight?.value?.includes('null') ? contentHeight.value : 0, }; } return undefined; }); const nestedContentToggled = () => { isNestedContentShown.value = !isNestedContentShown.value; emit('nested-content-toggled', props.rowData); }; const isSmallContainer: ComputedRef<boolean> = inject( DATA_GRID_SMALL_CONTAINER, computed(() => false) ); const isHighlightedRow = row => props.highlightedRows.includes(props.getDataItemId(row)); const isSelectedRow = row => props.selectedItems.includes(props.getDataItemId(row)); // Styling const conditionalRowClasses = (row: BentoDatagridDataItem, rowIndex: number) => ({ 'b-data-grid-row-container__row--disabled': props.isDataItemDisabled(row), 'b-data-grid-row-container__row--row-clicks-enabled': props.allowRowClicks, 'b-data-grid-row-container__row--highlighted': isHighlightedRow(row), 'b-data-grid-row-container__row--selected': isSelectedRow(row), 'b-data-grid-row-container__row--hovered': props.hoveredRowIndex === rowIndex, 'b-data-grid-row-container__row--is-last-child': props.isRowLastChild && !isNestedContentShown.value, 'b-data-grid-row-container__row--nested-row': props.isNestedRow, 'b-data-grid-row-container__row--zebra': props.isRowEven, }); const conditionalRowSetClasses = (isFrozenRowSet: boolean, isSmallContainer: boolean) => ({ 'b-data-grid-row-container__row-cell-set--frozen': isFrozenRowSet && !isSmallContainer, [`b-data-grid-row-container__row-cell-set--${BentoDatagridStyleState.OVERFLOW_SHADOW_LEFT}`]: !isSmallContainer && isFrozenRowSet && props.hasLeftOverflow, 'b-data-grid-row-container__row-cell-set--frozen-separator': !isSmallContainer && isFrozenRowSet && props.showFrozenColumnsSeparator, 'b-data-grid-row-container__row-cell-set--unfrozen': !isFrozenRowSet || isSmallContainer, 'b-data-grid-row-container__row-cell-set--is-measuring': props.isMeasuringColumnWidths, }); const conditionalNestedRowClasses = computed(() => ({ 'b-data-grid-row-container__row-nested-slot--is-last-child': props.isRowLastChild, 'b-data-grid-row-container__row-nested-slot': !shouldShowRenderNestedRows.value && props.shouldShowNestedContent(props.rowData), })); const conditionalRowActionsContainerClasses = computed(() => ({ 'b-data-grid-row-container__row-actions-container--row-actions-menu-open': isRowActionMenuOpen.value, })); const computedRowActionsStyles = computed(() => ({ minWidth: `${props.rowActionsMaximumWidth}px`, })); // Exposed refs defineExpose({ gridcellRefs, isNestedContentShown, nestedRowRefs, rowActionsRef, }); </script> <script lang="ts"> /** * Internal component to display the BentoDataGrid data rows containig the data row cells. * * @example * * export default { * components: { DataGridRow }, * template: ` * <data-grid-row * :ref="el => registerRowCellRefs(el, rowIndex)" * :style="gridColumnWidthStyle" * :allow-row-clicks="allowRowClicks" * :column-sets="columnSets" * :condensed="condensed" * :dragging-columns="draggingColumns" * :dragging-events-for-row-cell="draggingEventsForRowCell" * :get-data-item-id="getDataItemId" * :has-left-overflow="hasLeftOverflow" * :highlighted-rows="highlightedRows" * :hovered-row-index="hoveredRowIndex" * :hovered-over-column-state="hoveredOverColumnState" * :is-data-item-disabled="isDataItemDisabled" * :is-measuring-column-widths="isMeasuringColumnWidths" * :row-data="rowData" * :row-index="rowIndex" * :show-frozen-columns-separator="showFrozenColumnsSeparator" * :selectable="selectable" * :selected-items="selectedItems" * @cell-hovered="onCellHover" * @row-click="onRowClick" * @on-frozen-columns-key-press="updateScrollPositionOnFrozenColumnsNavigation" * @select-row="onRowSelect" * > * <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> * <slot :name="innerSlot" v-bind="scope" /> * </template> * </data-grid-row> * ` */ export default { name: 'data-grid-row', }; </script> <style lang="scss" scoped src="./data-grid-row.scss" />
@@ -6,33 +6,23 @@ import * as DraggableStories from './draggable.stories';
6
6
  # Draggable
7
7
 
8
8
  The `bento-draggable` component allows you to create lists of items that can be reordered by dragging and dropping. It
9
- is a wrapper around the popular [SortableJS](https://github.com/SortableJS/Sortable) library.
9
+ is a wrapper around [SortableJS](https://github.com/SortableJS/Sortable).
10
10
 
11
11
  ## Usage
12
12
 
13
- To create a draggable list, you provide a list of `items` to the component. You can then use the `default` slot to
14
- define how each item in the list should be rendered.
15
-
16
- For accessibility and to provide a handle for dragging, each draggable item should include a `bento-draggable-handle`
17
- component. This handle provides a grab icon and a menu with actions for reordering items using a keyboard.
13
+ Provide a list of `items` (or use `v-model`) and render each item via the `default` slot. Each item should include a
14
+ `bento-draggable-handle` for both drag-and-drop and keyboard reordering.
18
15
 
19
16
  <Canvas withSource="closed">
20
17
  <Story of={DraggableStories.Default} />
21
18
  </Canvas>
22
19
 
23
- ## Customization
24
-
25
- You can render any custom component or HTML structure within the default slot. The slot provides the `item` and its
26
- `index` from the `items` array, allowing you to build complex draggable elements.
27
-
28
20
  ## Nested Draggable Lists
29
21
 
30
- You can create nested draggable lists by recursively rendering `bento-draggable` components. This is useful for
31
- tree-like structures. To enable dragging between different levels, all `bento-draggable` components must share the same
32
- `group` name in their `options` prop.
22
+ Recursively render `bento-draggable` components for tree-like structures. All instances must share the same `group` name
23
+ in their `options` prop to allow dragging between levels.
33
24
 
34
- The component itself does not handle the `children` prop directly. Instead, you should handle the rendering of nested
35
- items within the default slot, as shown in this example.
25
+ > The component does not handle `children` directly render nested items yourself within the default slot.
36
26
 
37
27
  <Canvas withSource="closed">
38
28
  <Story of={DraggableStories.Nested} />
@@ -40,137 +30,88 @@ items within the default slot, as shown in this example.
40
30
 
41
31
  ## Grid Layout
42
32
 
43
- The component can also be used to create draggable grids. To do this, you can style the draggable container with CSS
44
- Flexbox or Grid.
45
-
46
- For the `bento-draggable-handle` to display all movement options (including left and right), set its `variant` prop to
47
- `all-directions`.
33
+ Style the draggable container with CSS Flexbox or Grid to create draggable grids. Set the handle's `variant` to
34
+ `all-directions` to display left/right movement options.
48
35
 
49
36
  <Canvas withSource="closed">
50
37
  <Story of={DraggableStories.Grid} />
51
38
  </Canvas>
52
39
 
53
- ## Exposed Methods
54
-
55
- You can get a `ref` to the Draggable component to access the underlying SortableJS instance and other properties.
56
-
57
- <Source
58
- dark
59
- language="html"
60
- code={`
61
- <template>
62
- <bento-draggable ref="draggableRef" :items="items" />
63
- </template>
64
-
65
- <script setup lang="ts">
66
- import { ref, onMounted } from 'vue';
40
+ ## Multi-Draggable (Board)
67
41
 
68
- const draggableRef = ref(null);
42
+ Multiple `bento-draggable` instances can share a SortableJS `group` to allow items to be dragged between lists (e.g. a
43
+ Kanban board). This requires additional setup because `bento-draggable` only manages its own list — cross-list transfers
44
+ must be handled manually.
69
45
 
70
- onMounted(() => {
71
- // Access the SortableJS instance
72
- const sortableInstance = draggableRef.value?.draggableElement;
73
-
74
- // Example: get the order of items
75
- const order = sortableInstance.toArray();
76
- console.log(order);
77
- });
78
- </script>
79
-
80
- `} />
81
-
82
- The following methods and properties are available on the component's `ref`:
46
+ <Canvas withSource="closed">
47
+ <Story of={DraggableStories.Board} />
48
+ </Canvas>
83
49
 
84
- | Name | Type | Description |
85
- | ------------------ | --------------------------- | ------------------------------------------------------------------------------------------------------- |
86
- | `isDragging` | `ref<boolean>` | A boolean ref that is `true` when an item is being dragged. |
87
- | `containerRef` | `ref<HTMLElement>` | A ref to the container DOM element. |
88
- | `draggableElement` | `Sortable` | The underlying SortableJS instance, which provides methods like `toArray()`, `sort()`, and `destroy()`. |
89
- | `announce` | `(message: string) => void` | Used to announce reordering or movement actions to screen reader users. |
50
+ When building a multi-draggable layout, keep in mind:
51
+
52
+ - **Provide a custom `onSort`:** When a custom `onSort` is passed via `options`, the component's built-in auto-sort is
53
+ bypassed. You are responsible for updating the reactive list yourself to prevent double DOM reorders (SortableJS
54
+ mutates the DOM, then Vue would re-render from the model).
55
+ - **Handle `onAdd` / `onRemove` yourself:** SortableJS fires `onAdd` on the receiving list and `onRemove` on the source
56
+ list, but neither event carries the item data. Track the dragged item manually via `onStart` and splice it into the
57
+ target list in `onAdd`.
58
+ - **`moveLeft` / `moveRight` require custom implementation:** The built-in `moveLeft` and `moveRight` slot helpers only
59
+ move items within the same list (they are aliases of `moveUp` / `moveDown`). For cross-list keyboard movement,
60
+ override `disabledMenus` to enable the direction, then bind the handle's `@move-right` or `@move-left` to your own
61
+ transfer function.
62
+ - **Route announcements to the receiving list's ref:** Each `bento-draggable` instance has its own `announce` method
63
+ exposed via `ref`. When moving an item to another list, call `announce` on the **target** instance so the announcement
64
+ appears in the correct live region.
65
+
66
+ ## Properties
67
+
68
+ | Property | Type | Default | Description |
69
+ | ----------------- | --------------------------- | ------- | -------------------------------------------------------------------------------------------------------- |
70
+ | `items` | `Array<BentoDraggableItem>` | `[]` | List of items to render. Supports `v-model`. |
71
+ | `el` | `string` | `div` | HTML tag for the container element. |
72
+ | `options` | `BentoDraggableOptions` | `{}` | Options passed to SortableJS. [See SortableJS docs](https://github.com/SortableJS/Sortable#readme). |
73
+ | `announcement` | `string \| 'disabled'` | `''` | Custom announcement text override. Set to `'disabled'` to suppress announcements. |
74
+ | `disableAutoSort` | `boolean` | `false` | Prevents automatic list reordering on sort. Use when providing a custom `onSort` callback via `options`. |
75
+
76
+ ## Slots
77
+
78
+ The `default` slot provides the following properties:
79
+
80
+ | Name | Type | Description |
81
+ | --------------- | ------------------------- | ------------------------------------------------------------------------------ |
82
+ | `item` | `BentoDraggableItem` | The current item from the `items` array. |
83
+ | `index` | `number` | The index of the current item. |
84
+ | `moveUp` | `() => void` | Move the item one position up. Includes announcement. |
85
+ | `moveDown` | `() => void` | Move the item one position down. Includes announcement. |
86
+ | `moveLeft` | `() => void` | Alias for `moveUp`. Override for cross-list movement. |
87
+ | `moveRight` | `() => void` | Alias for `moveDown`. Override for cross-list movement. |
88
+ | `moveTop` | `() => void` | Move the item to the first position. Includes announcement. |
89
+ | `moveBottom` | `() => void` | Move the item to the last position. Includes announcement. |
90
+ | `disabledMenus` | `Record<string, boolean>` | Pre-calculated disabled state for each direction based on the item's position. |
91
+
92
+ ## Exposed
93
+
94
+ Access the following via a template `ref` on the component:
95
+
96
+ | Name | Type | Description |
97
+ | ------------------ | ------------------------------------ | ---------------------------------------------------------------- |
98
+ | `isDragging` | `Ref<boolean>` | `true` while an item is being dragged. |
99
+ | `containerRef` | `Ref<HTMLElement>` | The container DOM element. |
100
+ | `draggableElement` | `Sortable` | The SortableJS instance (`toArray()`, `sort()`, `destroy()`, …). |
101
+ | `announce` | `(message: string) => void` | Trigger a screen reader announcement on this instance. |
102
+ | `moveItem` | `(from: number, to: number) => void` | Move an item between indices and emit the updated list. |
103
+ | `focus` | `(index: number) => void` | Focus the draggable handle at the given index. |
90
104
 
91
- ### SortableJS Methods (`draggableElement`)
105
+ ## Accessibility
92
106
 
93
- | Method | Description |
94
- | --------------------- | --------------------------------------------------------------------------------- |
95
- | `option(name, value)` | Get or set a SortableJS option. |
96
- | `destroy()` | Removes the sortable functionality completely. |
97
- | `toArray()` | Returns an array of the `data-id` attributes of the items in their current order. |
98
- | `sort(order)` | Sorts the items according to a specified array of `data-id`s. |
107
+ The `bento-draggable-handle` provides keyboard-accessible controls ("Move up", "Move down", "Move to top", "Move to
108
+ bottom") and an optional `all-directions` variant that adds "Move left" and "Move right".
99
109
 
100
- ## Accessibility
110
+ `bento-draggable` **automatically** announces reordering actions to screen reader users via a built-in live region
111
+ (`role="status"`). Announcements fire both after drag-and-drop and after keyboard-initiated moves.
101
112
 
102
- The `bento-draggable-handle` is essential for accessibility. It provides keyboard-accessible controls to reorder items,
103
- ensuring that users who cannot use a mouse can still interact with the list. The handle includes a menu with "Move up",
104
- "Move down", "Move to top", and "Move to bottom" actions.
105
-
106
- ### Screen Reader Announcements
107
-
108
- To provide a fully accessible experience, it is **strongly recommended** to announce reordering or movement actions to
109
- screen reader users. The `bento-draggable` component exposes an `announce` function that can be used to provide these
110
- updates.
111
-
112
- **Note:** The component does not automatically announce movements. It is your responsibility to call this function
113
- whenever the list order changes or an item is moved:
114
-
115
- - **After dragging with the mouse:** When an item is dropped and the list is reordered within the same list, or
116
- moved/cloned to another list.
117
- - **After using keyboard controls:** When a user selects a reordering action from the `bento-draggable-handle` menu.
118
-
119
- These announcements help users who rely on assistive technologies to understand the result of their actions and the
120
- current state of the list.
121
-
122
- <Source
123
- dark
124
- language="html"
125
- code={`
126
- <template>
127
- <bento-draggable
128
- ref="draggableRef"
129
- :items="items"
130
- :options="{ onSort }"
131
- >
132
- <template #default="{ item, index }">
133
- <bento-draggable-handle
134
- :label="item.label"
135
- @move-up="moveUp(index)"
136
- @move-down="moveDown(index)"
137
- />
138
- <span>{{ item.label }}</span>
139
- </template>
140
- </bento-draggable>
141
- </template>
142
-
143
- <script setup lang="ts">
144
- import { ref } from 'vue';
145
-
146
- const draggableRef = ref(null);
147
- const items = ref([
148
- { id: '1', label: 'Item 1' },
149
- { id: '2', label: 'Item 2' },
150
- { id: '3', label: 'Item 3' },
151
- ]);
152
-
153
- const announceMove = (itemLabel: string, fromIndex: number, toIndex: number) => {
154
- draggableRef.value?.announce(
155
- \`Moved \${itemLabel} from position \${fromIndex + 1} to position \${toIndex + 1} of \${items.value.length}.\`
156
- );
157
- };
158
-
159
- const onSort = (event) => {
160
- const { oldIndex, newIndex } = event;
161
- const item = items.value[oldIndex];
162
- // ... update the items list ...
163
- announceMove(item.label, oldIndex, newIndex);
164
- };
165
-
166
- const moveUp = (index) => {
167
- const item = items.value[index];
168
- // ... update the items list ...
169
- announceMove(item.label, index, index - 1);
170
- };
171
- </script>
172
-
173
- `} />
113
+ Use the `announcement` prop to override the default message, or set it to `'disabled'` to suppress announcements when
114
+ you handle them externally (e.g. via the exposed `announce` method in a multi-draggable setup).
174
115
 
175
116
  ## Resources
176
117