@adyen/bento-mcp 0.5.2 → 0.5.4

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 (85) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/assets/components/alert/alert.docs.mdx +17 -17
  3. package/dist/assets/components/alert/alert.vue +1 -1
  4. package/dist/assets/components/country/country.vue +1 -1
  5. package/dist/assets/components/dashed-underline/dashed-underline.vue +1 -1
  6. package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.types.ts +1 -1
  7. package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.vue +1 -1
  8. package/dist/assets/components/data-grid/data-grid.docs.mdx +3 -0
  9. package/dist/assets/components/data-grid/data-grid.vue +1 -1
  10. package/dist/assets/components/date/date.vue +1 -1
  11. package/dist/assets/components/date-picker/composables/use-date-picker-single-calendar-text.types.ts +1 -1
  12. package/dist/assets/components/date-picker/date-picker.docs.mdx +6 -0
  13. package/dist/assets/components/date-picker/date-picker.stories.ts +1 -1
  14. package/dist/assets/components/date-picker/date-picker.types.ts +1 -1
  15. package/dist/assets/components/date-picker/date-picker.vue +1 -1
  16. package/dist/assets/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.vue +1 -1
  17. package/dist/assets/components/date-range-picker/components/date-time-input/date-time-input.types.ts +1 -0
  18. package/dist/assets/components/date-range-picker/components/date-time-input/date-time-input.vue +1 -0
  19. package/dist/assets/components/date-range-picker/date-range-picker.docs.mdx +6 -0
  20. package/dist/assets/components/date-range-picker/date-range-picker.stories.ts +1 -1
  21. package/dist/assets/components/date-range-picker/date-range-picker.types.ts +1 -1
  22. package/dist/assets/components/date-range-picker/date-range-picker.vue +1 -1
  23. package/dist/assets/components/date-time-picker/date-time-picker.docs.mdx +129 -0
  24. package/dist/assets/components/date-time-picker/date-time-picker.stories.ts +1 -0
  25. package/dist/assets/components/date-time-picker/date-time-picker.types.ts +1 -0
  26. package/dist/assets/components/date-time-picker/date-time-picker.vue +1 -0
  27. package/dist/assets/components/draggable/draggable.vue +1 -1
  28. package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue +1 -1
  29. package/dist/assets/components/dropdown/components/dropdown-options-container/dropdown-options-container.vue +1 -1
  30. package/dist/assets/components/dropdown/components/dropdown-small-textbox/dropdown-small-textbox.vue +1 -1
  31. package/dist/assets/components/dropdown/components/variants/dropdown-country/dropdown-country.types.ts +1 -1
  32. package/dist/assets/components/dropdown/dropdown.types.ts +1 -1
  33. package/dist/assets/components/dropdown/dropdown.vue +1 -1
  34. package/dist/assets/components/file-uploader/file-uploader.vue +1 -1
  35. package/dist/assets/components/filter-bar/components/checkbox-group-filter/checkbox-group-filter.vue +1 -1
  36. package/dist/assets/components/filter-bar/components/date-filter/date-filter.vue +1 -1
  37. package/dist/assets/components/filter-bar/components/date-range-filter/date-range-filter.vue +1 -1
  38. package/dist/assets/components/filter-bar/filter-bar.docs.mdx +8 -2
  39. package/dist/assets/components/filter-bar/filter-bar.stories.ts +1 -1
  40. package/dist/assets/components/filter-bar/filter-bar.types.ts +1 -1
  41. package/dist/assets/components/filter-bar/filter-bar.vue +1 -1
  42. package/dist/assets/components/input-field/input-field.vue +1 -1
  43. package/dist/assets/components/input-field-password/input-field-password.vue +1 -1
  44. package/dist/assets/components/internal/calendar/calendar.stories.ts +1 -1
  45. package/dist/assets/components/internal/calendar/calendar.types.ts +1 -1
  46. package/dist/assets/components/internal/calendar/calendar.vue +1 -1
  47. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.types.ts +1 -1
  48. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.vue +1 -1
  49. package/dist/assets/components/internal/calendar/components/calendar-month/components/calendar-month-day/calendar-month-day.vue +1 -1
  50. package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.types.ts +1 -1
  51. package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.vue +1 -1
  52. package/dist/assets/components/internal/calendar/components/calendar-year-selector/calendar-year-selector.types.ts +1 -0
  53. package/dist/assets/components/internal/calendar/components/calendar-year-selector/calendar-year-selector.vue +1 -0
  54. package/dist/assets/components/internal/header-meta/header-meta.vue +1 -1
  55. package/dist/assets/components/internal/info-icon-with-popover/info-icon-with-popover.vue +1 -1
  56. package/dist/assets/components/internal/listbox/components/listbox-multi-select/listbox-multi-select.vue +1 -1
  57. package/dist/assets/components/internal/listbox/components/listbox-single-select/listbox-single-select.vue +1 -1
  58. package/dist/assets/components/link/link.vue +1 -1
  59. package/dist/assets/components/list/components/list-item/list-item.types.ts +1 -1
  60. package/dist/assets/components/list/components/list-item/list-item.vue +1 -1
  61. package/dist/assets/components/list/list.docs.mdx +62 -0
  62. package/dist/assets/components/list/list.stories.ts +1 -1
  63. package/dist/assets/components/list/list.types.ts +1 -1
  64. package/dist/assets/components/list/list.vue +1 -1
  65. package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.stories.ts +1 -1
  66. package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.vue +1 -1
  67. package/dist/assets/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.stories.ts +1 -1
  68. package/dist/assets/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.vue +1 -1
  69. package/dist/assets/components/navigation-menu/navigation-menu.docs.mdx +7 -43
  70. package/dist/assets/components/navigation-menu/navigation-menu.stories.ts +1 -1
  71. package/dist/assets/components/navigation-menu/navigation-menu.types.ts +1 -1
  72. package/dist/assets/components/navigation-menu/navigation-menu.vue +1 -1
  73. package/dist/assets/components/popover/popover.docs.mdx +78 -15
  74. package/dist/assets/components/popover/popover.stories.ts +1 -1
  75. package/dist/assets/components/selection-card/components/selection-card-group/selection-card-group.types.ts +1 -1
  76. package/dist/assets/components/summary-grid/summary-grid.vue +1 -1
  77. package/dist/assets/components/tabs/tabs.docs.mdx +6 -1
  78. package/dist/assets/components.json +1 -0
  79. package/dist/assets/composables/use-bento-delayed-hover/use-bento-delayed-hover.docs.mdx +77 -0
  80. package/dist/assets/composables/use-bento-delayed-hover/use-bento-delayed-hover.stories.ts +1 -0
  81. package/dist/assets/deprecations.md +156 -156
  82. package/dist/assets/index.ts +1 -1
  83. package/dist/assets/usage.json +75 -74
  84. package/dist/main.js +135 -48
  85. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.4 (2026-06-03)
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.3 (2026-05-12)
9
+
10
+ This was a version bump only for mcp to align it with other projects, there were no code changes.
11
+
12
+
3
13
  ## 0.5.2 (2026-05-06)
4
14
 
5
15
  This was a version bump only for mcp to align it with other projects, there were no code changes.
@@ -16,11 +16,11 @@ Use alerts for important messages, explanations or errors that require user to t
16
16
 
17
17
  Use alerts for:
18
18
 
19
- - Warning users about potential problems, risks, or actions required like update their password.
20
- - Informing users with helpful information or tips, such as feature announcements, usage guidelines, or updates about
21
- system status.
22
- - Displaying system-generated notifications, such as scheduled maintenance, security alerts, or updates about the
23
- availability of new features or versions of the software.
19
+ - Warning users about potential problems, risks, or actions required like update their password.
20
+ - Informing users with helpful information or tips, such as feature announcements, usage guidelines, or updates about
21
+ system status.
22
+ - Displaying system-generated notifications, such as scheduled maintenance, security alerts, or updates about the
23
+ availability of new features or versions of the software.
24
24
 
25
25
  ### Do not use
26
26
 
@@ -28,7 +28,7 @@ Do not use alerts for non-critical feedback about the app’s processes after us
28
28
 
29
29
  For example:
30
30
 
31
- - confirmation message that a setting has been saved. In this case, prefer the `toast` component instead
31
+ - confirmation message that a setting has been saved. In this case, prefer the `toast` component instead
32
32
 
33
33
  ### Guidelines
34
34
 
@@ -36,8 +36,8 @@ The component allows you to pass `actions` slot where action buttons may be plac
36
36
 
37
37
  According to the Design system guidelines:
38
38
 
39
- - You are restricted to use only `Button` component with `tertiary` variant
40
- - The amount of buttons may be other 1 or 2, not more
39
+ - You are restricted to use only `Button` component with `tertiary` variant
40
+ - The amount of buttons may be other 1 or 2, not more
41
41
 
42
42
  ## Variants
43
43
 
@@ -51,8 +51,8 @@ above a form. It has optional title, buttons and dismiss icon.
51
51
  ### Tip
52
52
 
53
53
  For cramped spaces and short tips, use tip variant instead. It’s like an exposed tooltip that, for example, can warn
54
- user of a specific setting’s consequences. It doesn’t have a title, buttons or dismiss icon, however, you can link parts
55
- of the text, if needed.
54
+ user of a specific setting’s consequences. It doesn’t have a title or buttons, however, you can link parts of the text,
55
+ if needed.
56
56
 
57
57
  ## Modifiers
58
58
 
@@ -63,10 +63,10 @@ corresponding visual representation of the alert type with predefined icon and b
63
63
 
64
64
  It accepts one of the following values:
65
65
 
66
- - `'success'`
67
- - `'highlight'`
68
- - `'warning'`
69
- - `'critical'`
66
+ - `'success'`
67
+ - `'highlight'`
68
+ - `'warning'`
69
+ - `'critical'`
70
70
 
71
71
  ### Close Button
72
72
 
@@ -85,8 +85,8 @@ when alert appears).
85
85
 
86
86
  When a user uses the feature that triggers the alert:
87
87
 
88
- - User can HEAR the alert is read when it appears (but focus DOES NOT transfer automatically when alert appears).
89
- - `Success`, `Warning` and `Critical` types will be identified by an alert sound before the alert is read.
88
+ - User can HEAR the alert is read when it appears (but focus DOES NOT transfer automatically when alert appears).
89
+ - `Success`, `Warning` and `Critical` types will be identified by an alert sound before the alert is read.
90
90
 
91
91
  ### Colors
92
92
 
@@ -103,4 +103,4 @@ Adhering to the [WAI-ARIA 1.2](https://www.w3.org/TR/wai-aria-1.2/) standard we
103
103
 
104
104
  ## Resources
105
105
 
106
- - [Figma link](https://www.figma.com/file/uLabwF3243jdMDsNSP7U9I/Bento---Components?node-id=2748-62103&t=88jDJJzCcALwP5KP-0)
106
+ - [Figma link](https://www.figma.com/file/uLabwF3243jdMDsNSP7U9I/Bento---Components?node-id=2748-62103&t=88jDJJzCcALwP5KP-0)
@@ -1 +1 @@
1
- <template> <div class="b-alert" :role="computedRole"> <div class="b-alert__inner-container" :class="alertConditionalClasses"> <div class="b-alert__icon"> <component :is="iconComponent" aria-hidden="true" :svg-title="iconSvgTitle" /> </div> <div class="b-alert__content"> <bento-typography v-if="hasSlot('default') && !isTipVariant" class="b-alert__title" el="div" variant="body" strongest wide > <slot /> </bento-typography> <bento-typography v-if="hasSlot('description')" class="b-alert__description" v-bind="descriptionTypography" data-testid="alert-description" > <slot name="description" /> </bento-typography> <div v-if="hasSlot('actions') && !isTipVariant" class="b-alert__actions" data-testid="alert-actions"> <slot name="actions" /> </div> </div> <bento-button v-if="closeButton && !isTipVariant" class="b-alert__close-button" variant="tertiary" type="button" @click="onCloseButtonClick" > <cross-small :svg-title="t('close')" /> </bento-button> </div> </div> </template> <script setup lang="ts"> import { computed, onMounted, useSlots } from 'vue'; // Component import { BentoAlertEvent, BentoAlertTypeOption, BentoAlertVariant } from '@/components/alert/alert.types'; import { BentoTypography, BentoTypographyElement, BentoTypographyVariant } from '@/components/typography'; import { BentoButton } from '@/components/button'; import CrossSmall from '@adyen/ui-assets-icons-16/vue/cross-small'; import Warning from '@adyen/ui-assets-icons-16/vue/warning-filled'; import CrossCircle from '@adyen/ui-assets-icons-16/vue/cross-circle-fill'; import CheckedCircle from '@adyen/ui-assets-icons-16/vue/checkmark-circle-fill'; import Info from '@adyen/ui-assets-icons-16/vue/info-filled'; // Utils import { useHasSlot } from '@/composables'; import { useI18n } from '@/utils/ts/i18n'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const slots = useSlots(); const hasSlot = useHasSlot(slots); const emit = defineEmits<{ /** * Emits event when the alert is closed. `closeButton` must be `true` */ (e: 'close-alert'); }>(); const props = withDefaults( defineProps<{ /** * Adds a close button on the right side of the alert */ closeButton?: boolean; /** * Type of a alert; set a distinctive icon and color. Defaults to highlight. * @values HIGHLIGHT, SUCCESS, CRITICAL, WARNING */ type?: BentoAlertTypeOption | `${BentoAlertTypeOption}`; /** * Variant for the alert * @values DEFAULT, TIP */ variant?: BentoAlertVariant | `${BentoAlertVariant}`; }>(), { closeButton: false, type: BentoAlertTypeOption.HIGHLIGHT, variant: BentoAlertVariant.DEFAULT, } ); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const isTipVariant = computed(() => props.variant === BentoAlertVariant.TIP); const alertConditionalClasses = computed(() => ({ [`b-alert--${props.type}`]: props.type, [`b-alert--${BentoAlertVariant.TIP}`]: isTipVariant.value, })); const iconSvgTitle = computed(() => t(`${props.type}Icon`)); const iconComponent = computed(() => { switch (props.type) { case BentoAlertTypeOption.CRITICAL: return CrossCircle; case BentoAlertTypeOption.SUCCESS: return CheckedCircle; case BentoAlertTypeOption.WARNING: return Warning; case BentoAlertTypeOption.HIGHLIGHT: default: return Info; } }); const descriptionTypography = computed(() => ({ el: BentoTypographyElement.DIV, variant: isTipVariant.value ? BentoTypographyVariant.CAPTION : BentoTypographyVariant.BODY, wide: true, })); const computedRole = computed(() => (props.type === BentoAlertTypeOption.HIGHLIGHT ? 'status' : 'alert')); const onCloseButtonClick = () => { emit(BentoAlertEvent.CLOSE_ALERT); }; onMounted(() => { if (!slots.default && !slots.description) { throw new Error('Alert Component requires a title or a description'); } }); </script> <script lang="ts"> /** * An Alert component is used to display a persistent * notification containing information important to the user * * @example * import { BentoAlert, BentoAlertTypeOption } from '@adyen/bento-vue2'; * * export default { * components: { BentoAlert }, * template: ` * <bento-alert type="highlight"> * Title of the Alert * </bento-alert> * ` * } */ export default { i18n: { messages }, name: 'bento-alert', }; </script> <style lang="scss" scoped src="./alert.scss" />
1
+ <template> <div class="b-alert" :role="computedRole"> <div class="b-alert__inner-container" :class="alertConditionalClasses"> <div class="b-alert__icon"> <component :is="iconComponent" aria-hidden="true" :svg-title="iconSvgTitle" /> </div> <div class="b-alert__content"> <bento-typography v-if="hasSlot('default') && !isTipVariant" class="b-alert__title" el="div" variant="body" strongest wide > <slot /> </bento-typography> <bento-typography v-if="hasSlot('description')" class="b-alert__description" v-bind="descriptionTypography" data-testid="alert-description" > <slot name="description" /> </bento-typography> <div v-if="hasSlot('actions') && !isTipVariant" class="b-alert__actions" data-testid="alert-actions"> <slot name="actions" /> </div> </div> <bento-button v-if="closeButton" class="b-alert__close-button" variant="tertiary" type="button" @click="onCloseButtonClick" > <cross-small :svg-title="t('close')" /> </bento-button> </div> </div> </template> <script setup lang="ts"> import { computed, onMounted, useSlots } from 'vue'; // Component import { BentoAlertEvent, BentoAlertTypeOption, BentoAlertVariant } from '@/components/alert/alert.types'; import { BentoTypography, BentoTypographyElement, BentoTypographyVariant } from '@/components/typography'; import { BentoButton } from '@/components/button'; import CrossSmall from '@adyen/ui-assets-icons-16/vue/cross-small'; import Warning from '@adyen/ui-assets-icons-16/vue/warning-filled'; import CrossCircle from '@adyen/ui-assets-icons-16/vue/cross-circle-fill'; import CheckedCircle from '@adyen/ui-assets-icons-16/vue/checkmark-circle-fill'; import Info from '@adyen/ui-assets-icons-16/vue/info-filled'; // Utils import { useHasSlot } from '@/composables'; import { useI18n } from '@/utils/ts/i18n'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const slots = useSlots(); const hasSlot = useHasSlot(slots); const emit = defineEmits<{ /** * Emits event when the alert is closed. `closeButton` must be `true` */ (e: 'close-alert'); }>(); const props = withDefaults( defineProps<{ /** * Adds a close button on the right side of the alert */ closeButton?: boolean; /** * Type of a alert; set a distinctive icon and color. Defaults to highlight. * @values HIGHLIGHT, SUCCESS, CRITICAL, WARNING */ type?: BentoAlertTypeOption | `${BentoAlertTypeOption}`; /** * Variant for the alert * @values DEFAULT, TIP */ variant?: BentoAlertVariant | `${BentoAlertVariant}`; }>(), { closeButton: false, type: BentoAlertTypeOption.HIGHLIGHT, variant: BentoAlertVariant.DEFAULT, } ); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const isTipVariant = computed(() => props.variant === BentoAlertVariant.TIP); const alertConditionalClasses = computed(() => ({ [`b-alert--${props.type}`]: props.type, [`b-alert--${BentoAlertVariant.TIP}`]: isTipVariant.value, })); const iconSvgTitle = computed(() => t(`${props.type}Icon`)); const iconComponent = computed(() => { switch (props.type) { case BentoAlertTypeOption.CRITICAL: return CrossCircle; case BentoAlertTypeOption.SUCCESS: return CheckedCircle; case BentoAlertTypeOption.WARNING: return Warning; case BentoAlertTypeOption.HIGHLIGHT: default: return Info; } }); const descriptionTypography = computed(() => ({ el: BentoTypographyElement.DIV, variant: isTipVariant.value ? BentoTypographyVariant.CAPTION : BentoTypographyVariant.BODY, wide: true, })); const computedRole = computed(() => (props.type === BentoAlertTypeOption.HIGHLIGHT ? 'status' : 'alert')); const onCloseButtonClick = () => { emit(BentoAlertEvent.CLOSE_ALERT); }; onMounted(() => { if (!slots.default && !slots.description) { throw new Error('Alert Component requires a title or a description'); } }); </script> <script lang="ts"> /** * An Alert component is used to display a persistent * notification containing information important to the user * * @example * import { BentoAlert, BentoAlertTypeOption } from '@adyen/bento-vue2'; * * export default { * components: { BentoAlert }, * template: ` * <bento-alert type="highlight"> * Title of the Alert * </bento-alert> * ` * } */ export default { i18n: { messages }, name: 'bento-alert', }; </script> <style lang="scss" scoped src="./alert.scss" />
@@ -1 +1 @@
1
- <template> <div class="b-country"> <div class="b-country__label"> <span v-if="isSummaryGridLayout">{{ countryLabel }}</span> <bento-typography v-else el="span">{{ countryLabel }}</bento-typography> </div> </div> </template> <script setup lang="ts"> import { inject, ref, toRefs, watch } from 'vue'; import { BentoTypography } from '@/components/typography'; import { useCountryCapital, useCountryName, useCountryPhoneCode } from './composables'; import { BentoCountryCode, type BentoCountryProps, BentoCountryVariant } from './country.types'; import { useI18n } from '@/utils/ts/i18n'; import { deprecate } from '@/utils/ts/deprecate'; import { type BentoSummaryGridLayout } from '@/components/summary-grid'; import { SUMMARY_GRID_LAYOUT_INJECTION_KEY } from '@/components/summary-grid/summary-grid.keys'; const props = withDefaults(defineProps<BentoCountryProps>(), { code: BentoCountryCode.UNKNOWN, label: undefined, variant: BentoCountryVariant.NAME, }); const { code, variant, label: customLabel } = toRefs(props); const { locale } = useI18n(undefined, true); const countryName = useCountryName(code, locale); const countryLabel = ref(''); const getLabel = () => { switch (variant.value) { case BentoCountryVariant.ISO: return code.value; case BentoCountryVariant.PHONE_CODE: return `${code.value} (${useCountryPhoneCode(code).value})`; case BentoCountryVariant.CAPITAL: // TODO: do we need an API OR get a longer translations list??? // Need to be able TRANSLATE as well as map... return `${useCountryCapital(code, locale).value} (${countryName.value})`; case BentoCountryVariant.CUSTOM: return customLabel.value; case BentoCountryVariant.NAME: default: return countryName.value; } }; // Update label if variant prop changes watch( [variant, locale, code], () => { countryLabel.value = getLabel(); }, { immediate: true } ); if (variant.value === 'custom' || props.label) { deprecate( 'BentoCountry "custom" variant and "label" prop', 'Use any of the other BentoCountry variants instead OR BentoTypography component for custom text', '2.0.0' ); } /* == Summary-grid styles == */ const registerPaymentMethodInSummaryGrid: BentoSummaryGridLayout = inject( SUMMARY_GRID_LAYOUT_INJECTION_KEY, () => ({ isSummaryGridLayout: false, }) ); const { isSummaryGridLayout } = registerPaymentMethodInSummaryGrid(); </script> <script lang="ts"> /** * A component to display a country's label. * The label comes in four variates: iso code, country name, phone number code or capital city. * * @usage * import { BentoCountry, BentoCountryCode } from '@adyen/bento-vue2'; * * export default { * components: { BentoCountry }, * template: ` * <bento-country :code="BentoCountryCode.NL" /> * ` * } */ export default { name: 'bento-country', }; </script> <style lang="scss" scoped src="./country.scss" />
1
+ <template> <div class="b-country"> <div class="b-country__label"> <span v-if="isSummaryGridLayout">{{ countryLabel }}</span> <bento-typography v-else el="span">{{ countryLabel }}</bento-typography> </div> </div> </template> <script setup lang="ts"> import { inject, ref, toRefs, watch } from 'vue'; import { BentoTypography } from '@/components/typography'; import { useCountryCapital, useCountryName, useCountryPhoneCode } from './composables'; import { BentoCountryCode, type BentoCountryProps, BentoCountryVariant } from './country.types'; import { useI18n } from '@/utils/ts/i18n'; import { deprecate } from '@/utils/ts/deprecate'; import { type BentoSummaryGridLayout } from '@/components/summary-grid/summary-grid.types'; import { SUMMARY_GRID_LAYOUT_INJECTION_KEY } from '@/components/summary-grid/summary-grid.keys'; const props = withDefaults(defineProps<BentoCountryProps>(), { code: BentoCountryCode.UNKNOWN, label: undefined, variant: BentoCountryVariant.NAME, }); const { code, variant, label: customLabel } = toRefs(props); const { locale } = useI18n(undefined, true); const countryName = useCountryName(code, locale); const countryLabel = ref(''); const getLabel = () => { switch (variant.value) { case BentoCountryVariant.ISO: return code.value; case BentoCountryVariant.PHONE_CODE: return `${code.value} (${useCountryPhoneCode(code).value})`; case BentoCountryVariant.CAPITAL: // TODO: do we need an API OR get a longer translations list??? // Need to be able TRANSLATE as well as map... return `${useCountryCapital(code, locale).value} (${countryName.value})`; case BentoCountryVariant.CUSTOM: return customLabel.value; case BentoCountryVariant.NAME: default: return countryName.value; } }; // Update label if variant prop changes watch( [variant, locale, code], () => { countryLabel.value = getLabel(); }, { immediate: true } ); if (variant.value === 'custom' || props.label) { deprecate( 'BentoCountry "custom" variant and "label" prop', 'Use any of the other BentoCountry variants instead OR BentoTypography component for custom text', '2.0.0' ); } /* == Summary-grid styles == */ const registerPaymentMethodInSummaryGrid: BentoSummaryGridLayout = inject( SUMMARY_GRID_LAYOUT_INJECTION_KEY, () => ({ isSummaryGridLayout: false, }) ); const { isSummaryGridLayout } = registerPaymentMethodInSummaryGrid(); </script> <script lang="ts"> /** * A component to display a country's label. * The label comes in four variates: iso code, country name, phone number code or capital city. * * @usage * import { BentoCountry, BentoCountryCode } from '@adyen/bento-vue2'; * * export default { * components: { BentoCountry }, * template: ` * <bento-country :code="BentoCountryCode.NL" /> * ` * } */ export default { name: 'bento-country', }; </script> <style lang="scss" scoped src="./country.scss" />
@@ -1 +1 @@
1
- <template> <span ref="element" v-bento-tooltip-directive:[tooltipPosition]="tooltipText" class="b-dashed-underline" tabindex="0" @mouseenter="openPopover" @mouseleave="closePopover" @focus="dashedUnderlineOnFocus" @focusout="dashedUnderlineOnFocusOut" > <span class="b-dashed-underline__content"> <slot></slot> </span> <bento-teleport v-if="element && hasPopover" :disabled="teleport?.disabled" :to="teleport?.to"> <span v-on="computedPopoverContainerEvents"> <bento-popover :open="isOpen" :target-element="element" :position="popoverPosition" role="tooltip" :fit-content="popoverSize === 'fit-content' || undefined" :large="popoverSize === 'large' || undefined" :small="popoverSize === 'small' || undefined" :title="popoverTitle" disable-focus-trap > <slot name="popover-content"> <bento-typography> {{ popoverText }} </bento-typography> </slot> </bento-popover> </span> </bento-teleport> </span> </template> <script lang="ts" setup> import { computed, ref, useSlots, watch } from 'vue'; import { BentoPopover } from '@/components/popover'; import { BentoTooltipDirective as vBentoTooltipDirective } from '@/directives/tooltip/index'; import { BentoTypography } from '@/components/typography'; import { useHasSlot } from '@/composables'; import { BentoTeleport } from '@/internal'; import { type BentoDashedUnderlineProps } from './dashed-underline.types'; const CLOSING_POPOVER_DELAY_MS = 200; const props = withDefaults(defineProps<BentoDashedUnderlineProps>(), { popoverText: null, popoverTitle: null, popoverPosition: 'top', popoverSize: 'fit-content', teleport: () => ({ disabled: true, }), tooltipText: null, tooltipPosition: 'top', }); const slots = useSlots(); const hasSlot = useHasSlot(slots); const element = ref<HTMLSpanElement>(null); const isOpen = ref(false); const closingTimeOutId = ref(); const openPopover = () => { isOpen.value = true; if (closingTimeOutId.value) { clearTimeout(closingTimeOutId.value); } }; const closePopover = () => { // We want to delay closing for a couple of seconds so // it doesn't flicker if a user hovers over the icon and then inside the popover. const timeOutId = setTimeout(() => { isOpen.value = false; }, CLOSING_POPOVER_DELAY_MS); closingTimeOutId.value = timeOutId; }; const dashedUnderlineOnFocus = () => { openPopover(); }; /** * Only close popover on blur if the newly focus element is not a child of this element * @param e - the event that contains the newly focused element */ const dashedUnderlineOnFocusOut = (e: FocusEvent) => { if (!element.value || !(e?.relatedTarget instanceof Element) || !element.value.contains(e.relatedTarget)) { closePopover(); } }; const hasPopover = computed(() => props.popoverText || hasSlot('popover-content')); const shouldShowTooltipPopoverError = computed(() => !props.tooltipText && !hasPopover.value); watch( shouldShowTooltipPopoverError, value => { if (value) { throw new Error( 'BentoDashedUnderline requires either a tooltip or a popover to be configured. Check the docs for more info.' ); } }, { immediate: true } ); /** * When the popover is teleported, it is no longer a DOM descendant of the trigger element. * We add these event listeners to the popover's container to keep the popover open when * the user moves their mouse or focuses out from the trigger to the popover BUT then mouses over * or focuses on the popover content. */ const computedPopoverContainerEvents = computed(() => props.teleport?.disabled === false ? { mouseenter: openPopover, mouseleave: closePopover, focus: dashedUnderlineOnFocus, focusout: dashedUnderlineOnFocusOut, } : {} ); </script> <script lang="ts"> /** * Provides a dashed underline to text, indicating that more information is available on * hover or focus. The additional information is displayed in a tooltip or a popover. * * @example * import { BentoDashedUnderline } from '@adyen/bento-vue2'; * * export default { * components: { BentoDashedUnderline }, * template: ` * <p> * This is some <bento-dashed-underline tooltip-text="A short explanation">annotated text</bento-dashed-underline>. * </p> * ` * } */ export default {}; </script> <style lang="scss" scoped src="./dashed-underline.scss" />
1
+ <template> <span ref="element" v-bento-tooltip-directive:[tooltipPosition]="tooltipText" class="b-dashed-underline" tabindex="0" v-on="hoverEvents" > <span class="b-dashed-underline__content"> <slot></slot> </span> <bento-teleport v-if="element && hasPopover" :disabled="teleport?.disabled" :to="teleport?.to"> <span v-on="hoverEvents"> <bento-popover :open="isOpen" :target-element="element" :position="popoverPosition" role="tooltip" :fit-content="popoverSize === 'fit-content' || undefined" :large="popoverSize === 'large' || undefined" :small="popoverSize === 'small' || undefined" :title="popoverTitle" disable-focus-trap > <slot name="popover-content"> <bento-typography> {{ popoverText }} </bento-typography> </slot> </bento-popover> </span> </bento-teleport> </span> </template> <script lang="ts" setup> import { computed, ref, useSlots, watch } from 'vue'; import { BentoPopover } from '@/components/popover'; import { useBentoDelayedHover } from '@/composables'; import { BentoTooltipDirective as vBentoTooltipDirective } from '@/directives/tooltip/index'; import { BentoTypography } from '@/components/typography'; import { useHasSlot } from '@/composables'; import { BentoTeleport } from '@/internal'; import { type BentoDashedUnderlineProps } from './dashed-underline.types'; const props = withDefaults(defineProps<BentoDashedUnderlineProps>(), { popoverText: null, popoverTitle: null, popoverPosition: 'top', popoverSize: 'fit-content', teleport: () => ({ disabled: true, }), tooltipText: null, tooltipPosition: 'top', }); const slots = useSlots(); const hasSlot = useHasSlot(slots); const element = ref<HTMLSpanElement>(null); const { isOpen, hoverEvents } = useBentoDelayedHover(); const hasPopover = computed(() => props.popoverText || hasSlot('popover-content')); const shouldShowTooltipPopoverError = computed(() => !props.tooltipText && !hasPopover.value); watch( shouldShowTooltipPopoverError, value => { if (value) { throw new Error( 'BentoDashedUnderline requires either a tooltip or a popover to be configured. Check the docs for more info.' ); } }, { immediate: true } ); </script> <script lang="ts"> /** * Provides a dashed underline to text, indicating that more information is available on * hover or focus. The additional information is displayed in a tooltip or a popover. * * @example * import { BentoDashedUnderline } from '@adyen/bento-vue2'; * * export default { * components: { BentoDashedUnderline }, * template: ` * <p> * This is some <bento-dashed-underline tooltip-text="A short explanation">annotated text</bento-dashed-underline>. * </p> * ` * } */ export default {}; </script> <style lang="scss" scoped src="./dashed-underline.scss" />
@@ -1 +1 @@
1
- export type BentoDatagridDataItemId = string | number; export interface BentoDatagridDataItem { id?: BentoDatagridDataItemId; children?: Array<BentoDatagridDataItem>; [fieldName: string]: unknown; }
1
+ export type BentoDatagridDataItemId = string | number; export interface BentoDatagridDataItem { id?: BentoDatagridDataItemId; children?: Array<BentoDatagridDataItem>; [fieldName: string]: unknown; } /** * Public surface of a `data-grid-row` component instance, mirroring the * properties exposed via `defineExpose` (with Vue's automatic Ref unwrapping). * * Extends the DOM `Element` type so it stays structurally compatible with * one branch of the `Element | ComponentPublicInstance` union produced by * Vue template refs, allowing the cast at the registration site to succeed * without going through `unknown`. `Element` is non-generic, so the * recursive `nestedRowRefs: Array<BentoDataGridRowInstance>` field does not * trigger TS2589 ("Type instantiation is excessively deep and possibly * infinite") the way `ComponentPublicInstance` would. * * Used as a structural alternative to `InstanceType<typeof DataGridRow>` so * that consumers (parent rows, composables) can type child references * without importing the recursive `.vue` file, which would create a * circular module dependency. */ export interface BentoDataGridRowInstance extends Element { gridcellRefs: { [key: string]: HTMLDivElement }; rowActionsRef?: HTMLDivElement; isNestedContentShown: boolean; nestedRowRefs: Array<BentoDataGridRowInstance>; }
@@ -1 +1 @@
1
- <template> <div class="b-data-grid-row-container" role="presentation"> <div v-bind="ariaRowAttributes" :id="rowId?.toString()" :key="rowIndex" class="b-data-grid-row-container__row" :class="conditionalRowClasses(rowData, rowIndex)" role="row" :aria-selected="getRowAriaSelectedValue(rowData)" @click="onRowClick" @keyup.enter="onRowClick" @keyup.space="onRowClick" > <template v-for="({ columnSet, id, isFrozenColumns }, columnSetIndex) of columnSets"> <div v-if="columnSet?.length" :key="`data-grid-rows-${id}`" class="b-data-grid-row-container__cells" :class="conditionalRowSetClasses(isFrozenColumns, isSmallContainer)" :data-testid="`data-grid-${id}-rows`" > <data-grid-cell v-for="(column, colIndex) in columnSet" :key="`cell-${column.field}`" :ref="el => registerRowCellRef(el, column.field)" class="b-data-grid-row-container__cell" :column="column" :column-set-index="columnSetIndex" :col-index="colIndex" :column-sets="columnSets" :condensed="condensed" :dragging-columns="draggingColumns" :dragging-events-for-row-cell="draggingEventsForRowCell" :get-data-item-id="getDataItemId" :has-zebra-background="isRowEven" :is-highlighted="isHighlightedRow(rowData)" :is-selected="isSelectedRow(rowData)" :hovered-over-column-state="hoveredOverColumnState" :is-data-item-disabled="isDataItemDisabled" :is-measuring-column-widths="isMeasuringColumnWidths" :is-nested-content-expanded="isNestedContentShown" :nested-content="shouldShowNestedContentToggle" :nested-rows-element-ids="allNestedRowIds" :row-data="rowData" :row-index="rowIndex" :selection-is-indeterminate="hasUnSelectedNestedRows" :selectable="selectable" :selected-items="selectedItems" @cell-hovered="emit('cell-hovered', $event)" @cell-focus="emit('cell-focus', $event)" @on-frozen-columns-key-press="emit('on-frozen-columns-key-press', $event)" @select-row="onRowSelect" @nested-content-toggled="nestedContentToggled" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-cell> </div> </template> <div v-if="rowActions" ref="rowActionsRef" :aria-colindex="sumColumnAmountsInColumnSets().toString()" class="b-data-grid-row-container__row-actions-container" :class="conditionalRowActionsContainerClasses" :style="computedRowActionsStyles" :data-cellindex="getRowActionsCellIndex()" tabindex="-1" role="gridcell" @focus="onRowActionsCellFocus" @focusOut="onRowActionsCellFocusOut" @keydown="onRowActionsCellOnKeyDown" > <data-grid-cell-actions class="b-data-grid-row-container__row-actions" disable-arrow-down-opening-menu :cell-actions="rowActions" :has-right-overflow="hasRightOverflow" :row-data-item="rowData" :has-zebra-background="isRowEven" :is-highlighted="isHighlightedRow(rowData)" :is-selected="isSelectedRow(rowData)" teleport-menu @menu-open="onRowActionMenuOpen" @menu-close="onRowActionMenuClose" /> </div> </div> <Transition v-if=" (hasNestedContent && hasSlot('nested-slot') && shouldShowNestedContent(rowData)) || (hasNestedRows && shouldShowNestedRowsBooleanResult) " :key="`${rowIndex}-nest-content`" name="b-data-grid-row-container__animation--nested-content" > <div v-if="isNestedContentShown && shouldShowRenderNestedRows" :class="conditionalNestedRowClasses" data-testid="nested-content-container" :style="nestedContentStyles" role="presentation" > <div ref="nestedContentRef"> <div v-if="isLoadingNestedRows" class="b-data-grid-row-container__row-nested-rows-loading-container" > <bento-loading-indicator /> </div> <data-grid-row v-for="(childRowData, childRowIndex) in rowData.children" v-bind="props" :id="getRowId(childRowIndex)" :key="`child-${childRowIndex}`" :ref="el => registerNestedRowRef(el, childRowIndex)" :row-data="childRowData" :row-index="childRowIndex" :row-set-size="rowData.children.length" is-nested-row @cell-hovered="emit('cell-hovered', $event)" @cell-focus="emit('cell-focus', $event)" @row-click="emitRowClick" @on-frozen-columns-key-press="emit('on-frozen-columns-key-press', $event)" @select-row="onNestedRowSelect(childRowData, $event)" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-row> </div> </div> <div v-else-if="isNestedContentShown" v-bind="nestedContentContainerDivAriaAttributes" :class="conditionalNestedRowClasses" data-testid="nested-content-container" :style="nestedContentStyles" role="row" tabindex="-1" > <div ref="nestedContentRef" role="gridcell"> <div v-if="isLoadingNestedRows" class="b-data-grid-row-container__row-nested-rows-loading-container" > <bento-loading-indicator /> </div> <div v-else-if="hasSlot('nested-slot') && shouldShowNestedContent(rowData)"> <slot :item="rowData" name="nested-slot" /> </div> </div> </div> </Transition> </div> </template> <script setup lang="ts"> import { type ComponentPublicInstance, computed, type ComputedRef, inject, onMounted, type PropType, ref, useSlots, } from 'vue'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { focusOnInteractiveChild, makeAllInteractiveChildrenFocusable, makeAllInteractiveChildrenUnFocusable, } from '@/components/data-grid/utils'; import 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" />
1
+ <template> <div class="b-data-grid-row-container" role="presentation"> <div v-bind="ariaRowAttributes" :id="rowId?.toString()" :key="rowIndex" class="b-data-grid-row-container__row" :class="conditionalRowClasses(rowData, rowIndex)" role="row" :aria-selected="getRowAriaSelectedValue(rowData)" @click="onRowClick" @keyup.enter="onRowClick" @keyup.space="onRowClick" > <template v-for="({ columnSet, id, isFrozenColumns }, columnSetIndex) of columnSets"> <div v-if="columnSet?.length" :key="`data-grid-rows-${id}`" class="b-data-grid-row-container__cells" :class="conditionalRowSetClasses(isFrozenColumns, isSmallContainer)" :data-testid="`data-grid-${id}-rows`" > <data-grid-cell v-for="(column, colIndex) in columnSet" :key="`cell-${column.field}`" :ref="el => registerRowCellRef(el, column.field)" class="b-data-grid-row-container__cell" :column="column" :column-set-index="columnSetIndex" :col-index="colIndex" :column-sets="columnSets" :condensed="condensed" :dragging-columns="draggingColumns" :dragging-events-for-row-cell="draggingEventsForRowCell" :get-data-item-id="getDataItemId" :has-zebra-background="isRowEven" :is-highlighted="isHighlightedRow(rowData)" :is-selected="isSelectedRow(rowData)" :hovered-over-column-state="hoveredOverColumnState" :is-data-item-disabled="isDataItemDisabled" :is-measuring-column-widths="isMeasuringColumnWidths" :is-nested-content-expanded="isNestedContentShown" :nested-content="shouldShowNestedContentToggle" :nested-rows-element-ids="allNestedRowIds" :row-data="rowData" :row-index="rowIndex" :selection-is-indeterminate="hasUnSelectedNestedRows" :selectable="selectable" :selected-items="selectedItems" @cell-hovered="emit('cell-hovered', $event)" @cell-focus="emit('cell-focus', $event)" @on-frozen-columns-key-press="emit('on-frozen-columns-key-press', $event)" @select-row="onRowSelect" @nested-content-toggled="nestedContentToggled" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-cell> </div> </template> <div v-if="rowActions" ref="rowActionsRef" :aria-colindex="sumColumnAmountsInColumnSets().toString()" class="b-data-grid-row-container__row-actions-container" :class="conditionalRowActionsContainerClasses" :style="computedRowActionsStyles" :data-cellindex="getRowActionsCellIndex()" tabindex="-1" role="gridcell" @focus="onRowActionsCellFocus" @focusOut="onRowActionsCellFocusOut" @keydown="onRowActionsCellOnKeyDown" > <data-grid-cell-actions class="b-data-grid-row-container__row-actions" disable-arrow-down-opening-menu :cell-actions="rowActions" :has-right-overflow="hasRightOverflow" :row-data-item="rowData" :has-zebra-background="isRowEven" :is-highlighted="isHighlightedRow(rowData)" :is-selected="isSelectedRow(rowData)" teleport-menu @menu-open="onRowActionMenuOpen" @menu-close="onRowActionMenuClose" /> </div> </div> <Transition v-if=" (hasNestedContent && hasSlot('nested-slot') && shouldShowNestedContent(rowData)) || (hasNestedRows && shouldShowNestedRowsBooleanResult) " :key="`${rowIndex}-nest-content`" name="b-data-grid-row-container__animation--nested-content" > <div v-if="isNestedContentShown && shouldShowRenderNestedRows" :class="conditionalNestedRowClasses" data-testid="nested-content-container" :style="nestedContentStyles" role="presentation" > <div ref="nestedContentRef"> <div v-if="isLoadingNestedRows" class="b-data-grid-row-container__row-nested-rows-loading-container" > <bento-loading-indicator /> </div> <data-grid-row v-for="(childRowData, childRowIndex) in rowData.children" v-bind="props" :id="getRowId(childRowIndex)" :key="`child-${childRowIndex}`" :ref="el => registerNestedRowRef(el, childRowIndex)" :row-data="childRowData" :row-index="childRowIndex" :row-set-size="rowData.children.length" is-nested-row @cell-hovered="emit('cell-hovered', $event)" @cell-focus="emit('cell-focus', $event)" @row-click="emitRowClick" @on-frozen-columns-key-press="emit('on-frozen-columns-key-press', $event)" @select-row="onNestedRowSelect(childRowData, $event)" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-row> </div> </div> <div v-else-if="isNestedContentShown" v-bind="nestedContentContainerDivAriaAttributes" :class="conditionalNestedRowClasses" data-testid="nested-content-container" :style="nestedContentStyles" role="row" tabindex="-1" > <div ref="nestedContentRef" role="gridcell"> <div v-if="isLoadingNestedRows" class="b-data-grid-row-container__row-nested-rows-loading-container" > <bento-loading-indicator /> </div> <div v-else-if="hasSlot('nested-slot') && shouldShowNestedContent(rowData)"> <slot :item="rowData" name="nested-slot" /> </div> </div> </div> </Transition> </div> </template> <script setup lang="ts"> import { type ComponentPublicInstance, computed, type ComputedRef, inject, onMounted, type PropType, ref, useSlots, } from 'vue'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { focusOnInteractiveChild, makeAllInteractiveChildrenFocusable, makeAllInteractiveChildrenUnFocusable, } from '@/components/data-grid/utils'; import DataGridCell from '@/components/data-grid/components/data-grid-cell/data-grid-cell.vue'; import DataGridCellActions from '@/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.vue'; // Types and enums import { type BentoColumn, type BentoDatagridDataItem, type BentoDatagridDataItemId, type BentoDatagridGetDataItemIdFn, type BentoDatagridIsDataItemDisabledFn, type BentoDatagridShouldShowNestedContentFn, type BentoDatagridShouldShowNestedRowsFn, BentoDatagridStyleState, } from '@/components/data-grid/data-grid.types'; import type { BentoDataGridRowInstance } from './data-grid-row.types'; import type { DataGridCellColumnSet, DataGridCellDragEventsForColumn, } from '../data-grid-cell/data-grid-cell.types'; import type { BentoDatagridDraggingColumnState } from '../data-grid-columns/data-grid-columns.types'; import type { DatagridColumnHoveringState } from '@/components/data-grid/composables/use-column-hovering'; // Translations import { type Booleanish } from '@/types/prop-types'; import { useHasSlot } from '@/composables/use-has-slot/use-has-slot'; import { useExpandableContentHeight } from '@/composables/use-expandable-content-height/use-expandable-content-height'; import { DATAGRID_ACTIONS_COLUMN_FIELD_NAME, DATAGRID_CELL_OFFSET_PADDING, } from '@/components/data-grid/composables/use-calculate-column-width/use-calculate-column-width'; import { generateUid } from '@/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<BentoDataGridRowInstance>>([]); const nestedContentSlotId = generateUid('nested-content-slot'); const isRowActionMenuOpen = ref(false); const isExitingRowActionInteraction = ref(false); const focusableItems = ref<Array<{ node: HTMLElement }>>(); /** * Returns the row-action container element, handling the case where Vue 2 * may wrap refs inside a v-for as an array. */ const getRowActionsElement = (): HTMLElement | undefined => { const val = rowActionsRef.value; return Array.isArray(val) ? val[0] : val; }; onMounted(() => { const container = getRowActionsElement(); if (container) { focusableItems.value = makeAllInteractiveChildrenUnFocusable(container); } }); const shouldShowNestedRowsBooleanResult = computed(() => props.shouldShowNestedRows(props.rowData)); const shouldShowRenderNestedRows = computed( () => !props.isNestedRow && props.hasNestedRows && shouldShowNestedRowsBooleanResult.value && props.rowData?.children?.length > 0 ); const isLoadingNestedRows = computed( () => !props.isNestedRow && props.hasNestedRows && shouldShowNestedRowsBooleanResult.value && (props.rowData?.children === undefined || props?.rowData?.children.length === 0) ); const shouldShowNestedContentToggle = computed( () => (props.hasNestedContent && props.shouldShowNestedContent(props.rowData)) || (props.hasNestedRows && shouldShowNestedRowsBooleanResult.value && !props.isNestedRow) ); const ariaRowAttributes = computed<HTMLAttributes>(() => { if (props.hasNestedContent || props.hasNestedRows) { const isNestedContentShownBooleanishVal = isNestedContentShown.value.toString() as Booleanish; return { 'aria-expanded': shouldShowNestedContentToggle.value ? isNestedContentShownBooleanishVal : undefined, 'aria-level': props.isNestedRow ? '2' : '1', 'aria-posinset': props.rowIndex + 1, 'aria-setsize': props.rowSetSize, }; } return { 'aria-rowindex': props.rowIndex + 1 }; }); const nestedContentContainerDivAriaAttributes = computed<HTMLAttributes>(() => { if (isLoadingNestedRows.value) { return { 'aria-busy': 'true', 'aria-live': 'polite', }; } if ( shouldShowRenderNestedRows.value || !props.hasNestedContent || !props.shouldShowNestedContent(props.rowData) ) { return undefined; } return { 'aria-level': props.isNestedRow ? '3' : '2', 'aria-posinset': '1', 'aria-setsize': '1', id: nestedContentSlotId, role: 'row', tabindex: '-1', }; }); const nestedRowIds = computed(() => { if (shouldShowRenderNestedRows.value) { return props.rowData?.children?.reduce((acc, _val, index) => { acc[index] = generateUid('nested-row'); return acc; }, {}); } return []; }); const allNestedRowIds = computed(() => { if (!isNestedContentShown.value) { return undefined; } if (props.hasNestedContent && props.shouldShowNestedContent(props.rowData)) { return [nestedContentSlotId]; } if (shouldShowRenderNestedRows.value && Object.keys(nestedRowIds.value)?.length > 0) { const generatedRowIds = Object.values(nestedRowIds.value); if (generatedRowIds?.length > 0) { return generatedRowIds; } } return []; }); const hasUnSelectedNestedRows = computed(() => { const childrendDataItemIds = props.rowData?.children?.map(nestedItem => props.getDataItemId(nestedItem)); if (childrendDataItemIds?.length > 0) { const everyChildIsSelected = childrendDataItemIds.every(childId => props?.selectedItems?.includes(childId)); const someItemsAreSelected = childrendDataItemIds.some(childId => props?.selectedItems?.includes(childId)); return !everyChildIsSelected && someItemsAreSelected; } return false; }); // Methods const registerRowCellRef = (el: Element | ComponentPublicInstance, field: string) => { // We always know this will be used on a DataGridCell component so we can cast it const gridCellComponent = el as InstanceType<typeof DataGridCell>; if (el !== null && gridCellComponent?.gridcellRef) { gridcellRefs.value[field] = gridCellComponent.gridcellRef as HTMLDivElement; } }; const registerNestedRowRef = (el: Element | ComponentPublicInstance, childRowIndex: number) => { if (el !== null) { nestedRowRefs.value[childRowIndex] = el as BentoDataGridRowInstance; } }; const getRowId = (childRowIndex: number) => { return nestedRowIds.value[childRowIndex]; }; const getRowAriaSelectedValue = (row: BentoDatagridDataItem): Booleanish => { if (!props.selectable) { return null; } const id = props.getDataItemId(row); return props.selectedItems.includes(id).toString() as Booleanish; }; const emitRowClick = (rowData: BentoDatagridDataItem, event: MouseEvent | KeyboardEvent) => { emit('row-click', rowData, event); }; /** * Handles row clicks when the `allowRowClicks` prop is true. * * This method also prevents the `row-click` event from firing if the click * originated from an interactive element (like a button, input, or link) * within the row. This is to avoid unintended behavior when a user * interacts with controls inside a row. * * @param event The mouse or keyboard event from the click or key press. */ const onRowClick = (event: MouseEvent | KeyboardEvent) => { // Only proceed if allowRowClicks is true if (!props.allowRowClicks) { return; } let isEventTargetInteractiveElement = false; const dataGridRowInteractiveTypes = ['button', 'input', 'a']; for (let i = 0; i < dataGridRowInteractiveTypes.length; i++) { if ((event?.target as HTMLElement)?.closest(dataGridRowInteractiveTypes[i])) { isEventTargetInteractiveElement = true; break; } } // Only trigger if element is NOT interactive if (!isEventTargetInteractiveElement) { emitRowClick(props.rowData, event); } }; const onRowSelect = (value: Array<BentoDatagridDataItemId>) => { emit('select-row', value, props.rowData); }; const onNestedRowSelect = (rowData: BentoDatagridDataItem, value: Array<BentoDatagridDataItemId>) => { emit('select-nested-row', value, props.rowData, rowData); }; /** * Sets the state to indicate that a row action menu is open. This is used to apply a higher * z-index to the row's action container, ensuring the menu displays correctly above * subsequent rows. */ const onRowActionMenuOpen = () => { isRowActionMenuOpen.value = true; }; /** * Resets the state when a row action menu is closed, removing the elevated z-index from the * row's action container. */ const onRowActionMenuClose = () => { isRowActionMenuOpen.value = false; }; /** * When the row actions cell is focused, determine navigation direction and focus * the appropriate interactive child. Backward navigation (Shift+Tab) focuses the * last child; forward navigation focuses the first. */ const onRowActionsCellFocus = (e: FocusEvent) => { if (isExitingRowActionInteraction.value) { isExitingRowActionInteraction.value = false; return; } const container = getRowActionsElement(); if (!container || !focusableItems.value?.length) { return; } const isBackwardNavigation = isRelatedTargetAfterInDOM(e.relatedTarget as HTMLElement, container); const lastIndex = focusableItems.value.length - 1; const targetItem = isBackwardNavigation ? focusableItems.value[lastIndex] : focusableItems.value[0]; makeAllInteractiveChildrenFocusable([targetItem]); focusOnInteractiveChild(container, { last: isBackwardNavigation }); }; /** * When focus leaves the row actions cell, make all interactive children unfocusable. * This ensures that tabbing through the grid skips over the individual actions within the cell. */ const onRowActionsCellFocusOut = () => { const container = getRowActionsElement(); if (container) { makeAllInteractiveChildrenUnFocusable(container); } }; /** * Handles keyboard navigation within the row actions cell. * Allows using left and right arrow keys to move between interactive elements (e.g., buttons) in the cell. * @param e The keyboard event. */ const onRowActionsCellOnKeyDown = (e: KeyboardEvent) => { const container = getRowActionsElement(); // Escape returns focus to the gridcell container if (e.key === EventKey.ESCAPE) { if (container) { makeAllInteractiveChildrenUnFocusable(container); } isExitingRowActionInteraction.value = true; container?.focus(); e.stopPropagation(); e.preventDefault(); return; } if (e.key !== EventKey.TAB && container) { makeAllInteractiveChildrenUnFocusable(container); } if ( e.key === EventKey.ARROW_RIGHT && focusableItems.value?.length > 1 && focusableItems.value[0].node === document.activeElement ) { if (container) { makeAllInteractiveChildrenUnFocusable(container); } focusableItems.value[1].node.setAttribute('tabindex', '0'); focusableItems.value[1].node.focus(); e.stopPropagation(); e.preventDefault(); } else if ( e.key === EventKey.ARROW_LEFT && focusableItems.value?.length > 1 && focusableItems.value[1].node === document.activeElement ) { if (container) { makeAllInteractiveChildrenUnFocusable(container); } focusableItems.value[0].node.setAttribute('tabindex', '0'); focusableItems.value[0].node.focus(); e.stopPropagation(); e.preventDefault(); } }; // Cell and Columns index calculation const sumColumnAmountsInColumnSets = () => props.columnSets.reduce((total, { columnSet }) => total + columnSet.length, 0); const getRowActionsCellIndex = () => sumColumnAmountsInColumnSets() * props.rowIndex + sumColumnAmountsInColumnSets(); // Nested content expanding logic const { contentHeight } = useExpandableContentHeight(nestedContentRef, isNestedContentShown); const nestedContentStyles = computed(() => { if (props.hasNestedContent || props.hasNestedRows) { // calculate the padding by getting the action column width and the first column left padding: const paddingLeft = shouldShowRenderNestedRows.value || isLoadingNestedRows.value ? 0 : gridcellRefs.value[DATAGRID_ACTIONS_COLUMN_FIELD_NAME].offsetWidth + DATAGRID_CELL_OFFSET_PADDING; return { paddingLeft: `${paddingLeft}px`, 'max-height': (props.hasNestedContent || props.hasNestedRows) && contentHeight.value && !contentHeight?.value?.includes('null') ? contentHeight.value : 0, }; } return undefined; }); const nestedContentToggled = () => { isNestedContentShown.value = !isNestedContentShown.value; emit('nested-content-toggled', props.rowData); }; const isSmallContainer: ComputedRef<boolean> = inject( DATA_GRID_SMALL_CONTAINER, computed(() => false) ); const isHighlightedRow = row => props.highlightedRows.includes(props.getDataItemId(row)); const isSelectedRow = row => props.selectedItems.includes(props.getDataItemId(row)); // Styling const conditionalRowClasses = (row: BentoDatagridDataItem, rowIndex: number) => ({ 'b-data-grid-row-container__row--disabled': props.isDataItemDisabled(row), 'b-data-grid-row-container__row--row-clicks-enabled': props.allowRowClicks, 'b-data-grid-row-container__row--highlighted': isHighlightedRow(row), 'b-data-grid-row-container__row--selected': isSelectedRow(row), 'b-data-grid-row-container__row--hovered': props.hoveredRowIndex === rowIndex, 'b-data-grid-row-container__row--is-last-child': props.isRowLastChild && !isNestedContentShown.value, 'b-data-grid-row-container__row--nested-row': props.isNestedRow, 'b-data-grid-row-container__row--zebra': props.isRowEven, }); const conditionalRowSetClasses = (isFrozenRowSet: boolean, isSmallContainer: boolean) => ({ 'b-data-grid-row-container__row-cell-set--frozen': isFrozenRowSet && !isSmallContainer, [`b-data-grid-row-container__row-cell-set--${BentoDatagridStyleState.OVERFLOW_SHADOW_LEFT}`]: !isSmallContainer && isFrozenRowSet && props.hasLeftOverflow, 'b-data-grid-row-container__row-cell-set--frozen-separator': !isSmallContainer && isFrozenRowSet && props.showFrozenColumnsSeparator, 'b-data-grid-row-container__row-cell-set--unfrozen': !isFrozenRowSet || isSmallContainer, 'b-data-grid-row-container__row-cell-set--is-measuring': props.isMeasuringColumnWidths, }); const conditionalNestedRowClasses = computed(() => ({ 'b-data-grid-row-container__row-nested-slot--is-last-child': props.isRowLastChild, 'b-data-grid-row-container__row-nested-slot': !shouldShowRenderNestedRows.value && props.shouldShowNestedContent(props.rowData), })); const conditionalRowActionsContainerClasses = computed(() => ({ 'b-data-grid-row-container__row-actions-container--row-actions-menu-open': isRowActionMenuOpen.value, })); const computedRowActionsStyles = computed(() => ({ minWidth: `${props.rowActionsMaximumWidth}px`, })); // Exposed refs defineExpose({ gridcellRefs, isNestedContentShown, nestedRowRefs, rowActionsRef, }); </script> <script lang="ts"> /** * Internal component to display the BentoDataGrid data rows containig the data row cells. * * @example * * export default { * components: { DataGridRow }, * template: ` * <data-grid-row * :ref="el => registerRowCellRefs(el, rowIndex)" * :style="gridColumnWidthStyle" * :allow-row-clicks="allowRowClicks" * :column-sets="columnSets" * :condensed="condensed" * :dragging-columns="draggingColumns" * :dragging-events-for-row-cell="draggingEventsForRowCell" * :get-data-item-id="getDataItemId" * :has-left-overflow="hasLeftOverflow" * :highlighted-rows="highlightedRows" * :hovered-row-index="hoveredRowIndex" * :hovered-over-column-state="hoveredOverColumnState" * :is-data-item-disabled="isDataItemDisabled" * :is-measuring-column-widths="isMeasuringColumnWidths" * :row-data="rowData" * :row-index="rowIndex" * :show-frozen-columns-separator="showFrozenColumnsSeparator" * :selectable="selectable" * :selected-items="selectedItems" * @cell-hovered="onCellHover" * @row-click="onRowClick" * @on-frozen-columns-key-press="updateScrollPositionOnFrozenColumnsNavigation" * @select-row="onRowSelect" * > * <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> * <slot :name="innerSlot" v-bind="scope" /> * </template> * </data-grid-row> * ` */ export default { name: 'data-grid-row', }; </script> <style lang="scss" scoped src="./data-grid-row.scss" />
@@ -257,6 +257,9 @@ The Data Grid provides the following customizeable slots:
257
257
 
258
258
  > Please note if you use a `BentoLink` inside a table cell its style will automatically be adjusted for you.
259
259
 
260
+ When using overlays inside a table cell slot, such as `BentoDropdown`, enable the component's teleport option so the
261
+ overlay is not clipped by the data grid body.
262
+
260
263
  With descriptions:
261
264
 
262
265
  All descriptions must use the color **Label secondary** (`--b-color-label-secondary`). The use of CSS variables is
@@ -1 +1 @@
1
- <template> <div ref="dataGridRef" class="b-data-grid" data-testid="data-grid" :class="conditionalGridClasses" aria-live="polite" :aria-busy="loading" @copy="handleClipboard" > <div v-if="filters" ref="filtersRef" class="b-data-grid__filters" :class="conditionalFilterClasses" :style="{ top: computedStickyFiltersTopPosition }" :aria-controls="dataGridUid" :aria-label="t('filterBar')" role="region" > <bento-filter-bar v-if="filters" ref="filterBarRef" :config="filters" :container-size-layout="filtersContainerSizeLayout" :filter-values="filterValues" :filter-values-object="filterValuesObject" :search="filterSearchConfig" :search-term="filterSearchTerm" @search-input="onSearchTermInput" @search-clear="onSearchTermCleared" @input="filters => $emit(BentoDatagridEvent.UPDATE_FILTERS, filters)" @update:filter-values="filterValues => $emit(BentoDatagridEvent.UPDATE_FILTER_VALUES, filterValues)" @update:filter-values-object=" filterValueObject => $emit(BentoDatagridEvent.UPDATE_FILTER_VALUES_OBJECT, filterValueObject) " /> </div> <div v-if="$slots.header" class="b-data-grid__header"> <slot name="header"></slot> </div> <div :id="dataGridUid" :role="ariaGridRole" :aria-rowcount="data.length" :aria-colcount="columns.length" :aria-label="emptyState ? t('noResults') : null" class="b-data-grid__wrapper" :style="{ top: computedColumnHeadersTopPosition }" > <div class="b-data-grid__column-headers" :style="{ top: computedColumnHeadersTopPosition }"> <div ref="columnsRef" role="rowgroup" class="b-data-grid__columns-row" @scroll="updateScrollPosition" @keydown="navigationInput" > <div v-if="columnPanel" role="row"> <bento-data-grid-config-settings class="b-data-grid__config-settings" role="gridcell" :columns="localColumnsState" :condensed="condensed" :zebra-rows="internalZebraRows" @update:condensed="onConfigSettingsCondensedUpdate" @update:zebra-rows="onConfigSettingsZebraRowsUpdate" @update:columns="onConfigSettingsColumnsUpdate" /> </div> <div :style="gridRowWidthStyle" class="b-data-grid__columns" role="row"> <template v-for="({ columnSet, id, isFrozenColumns }, index) of columnSets"> <data-grid-columns v-if="columnSet?.length || (!isFrozenColumns && rowActionsMaxWidth)" :key="`data-grid-columns-${id}`" :ref="el => registerColumnHeaderRefs(el)" :columns="columnSet" :has-resizable-columns="hasResizableColumns" :sort-by="sortBy" :all-items-selected-state="allItemsSelectedState" :is-frozen-columns="isFrozenColumns" :show-frozen-columns-separator="showFrozenColumnsSeparator" :dragging-columns="draggingColumns" :has-draggable-columns="allowColumnDragAndDrop" :has-column-panel="columnPanel" :has-left-overflow="hasLeftOverflow" :has-first-column-panel-right-aligned="index === computedColumnPanelAlignment" :hovered-over-column-state="hoveredOverColumnState" :is-measuring-column-widths="isMeasuringColumnWidths" :is-only-one-visible-column="isOnlyOneVisibleColumn" :selectable="data?.length > 0 && selectable" :selectable-across-pages="selectableAcrossPagesComputed" :selected-items="selectedItems" :visible-rows-state="visibleRowsState" :selection-state="selectionState" :truncate-column-headers="truncateColumnHeaders" :row-actions-maximum-width="rowActionsMaxWidth" v-on="draggingEventsForColumns" @column-hover="hoveredOverColumn = $event" @column-move="onMoveColumnByOffset" @column-update="onConfigSettingsColumnUpdate" @column-resize="onColumnResized" @column-resizing="onColumnWidthOverride" @select-all-rows="onAllRowSelect" @select-all-across-pages="emit(BentoDatagridEvent.SELECT_ALL_ACROSS_PAGES)" @select-all-current-results="emit(BentoDatagridEvent.SELECT_ALL_CURRENT_RESULTS)" @deselect-all-current-results="emit(BentoDatagridEvent.DESELECT_ALL_CURRENT_RESULTS)" @deselect-all="onDeselectAll" @sort="onSort" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-columns> </template> </div> </div> </div> <div ref="body" data-testid="data-grid-body" class="b-data-grid__body" :class="conditionalGridBodyClasses" @scroll="updateScrollPosition" > <div v-if="loading && !isLazyLoad" class="b-data-grid__loading"> <bento-loading-indicator class="b-data-grid__loading-indicator" large /> <bento-typography el="span" variant="caption">{{ t('gettingYourData') }}</bento-typography> </div> <div v-else role="rowgroup" @keydown="navigationInput"> <template v-if="data?.length === 0 && emptyState"> <div role="row" tabindex="-1"> <div role="gridcell" :aria-colspan="columns.length" tabindex="-1"> <bento-empty-state class="b-data-grid__empty-state" v-bind="limitedEmptyStateProps" /> </div> </div> </template> <data-grid-row v-for="(rowData, rowIndex) in data" v-else :key="rowIndex" :ref="el => registerRowCellRefs(el, rowIndex)" :style="gridRowWidthStyle" :allow-row-clicks="allowRowClicks" :column-sets="columnSets" :condensed="condensed" :dragging-columns="draggingColumns" :dragging-events-for-row-cell="draggingEventsForRowCell" :get-data-item-id="getDataItemId" :has-nested-content="hasNestedContent" :has-nested-rows="hasNestedRows" :has-left-overflow="hasLeftOverflow" :has-right-overflow="hasRightOverflow" :highlighted-rows="highlightedRows" :hovered-row-index="hoveredRowIndex" :hovered-over-column-state="hoveredOverColumnState" :is-data-item-disabled="isDataItemDisabled" :is-row-even="internalZebraRows ? rowIndex % 2 !== 0 : false" :is-row-last-child="rowIndex + 1 === data?.length" :is-measuring-column-widths="isMeasuringColumnWidths" :row-actions-maximum-width="rowActionsMaxWidth" :row-actions="rowActions" :row-data="rowData" :row-index="rowIndex" :row-set-size="data?.length" :show-frozen-columns-separator="showFrozenColumnsSeparator" :should-show-nested-content="shouldShowNestedContent" :should-show-nested-rows="shouldShowNestedRows" :selectable="selectable" :selected-items="selectedItems" @cell-hovered="onCellHover" @cell-focus="emit(BentoDatagridEvent.CELL_FOCUS, $event)" @nested-content-toggled="onNestedContentToggled($event)" @row-click="onRowClick" @on-frozen-columns-key-press="updateScrollPositionOnFrozenColumnsNavigation" @select-row="onRowSelect" @select-nested-row="onNestedRowSelect" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-row> </div> </div> </div> <data-grid-lazy-load v-if="isLazyLoad" :loading="loading" :is-infinite-scroll="isInfiniteScroll" @update:show-more="emitLazyLoadEvent" /> <div v-if="stickyHorizontalScrollbar" ref="virtualHorizontalScrollbarRef" class="b-data-grid__horizontal-scrollbar" aria-hidden="true" data-testid="sticky-horizontal-scrollbar" :style="horizontalScrollbarConditionalStyles" @scroll="syncVirtualScrollerWithBody" > <div :style="{ width: `${totalWidthOfAllColumns}px`, height: '15px' }"></div> </div> <div v-if="pagination && !isLazyLoad" ref="paginationContainerRef" data-testid="pagination-container" class="b-data-grid__pagination" :class="conditionalPaginationClasses" > <bento-pagination v-bind="pagination" :aria-label="$t('pagination')" :aria-controls="dataGridUid" @navigate="onPaginationNavigate" @items-page="onItemsPerPageChange" > <slot v-if="hasSlot('pagination-slot')" name="pagination-slot"></slot> </bento-pagination> </div> <div v-if="$slots.footer" class="b-data-grid__footer"><slot name="footer"></slot></div> <bento-action-bar v-if="showActionBar" :actions="bulkActions" :item-counter="itemsSelectedCounter" :extra-bottom-spacing="stickyPagination" @close="onDeselectAll" /> </div> </template> <script lang="ts" setup> import { type ComponentPublicInstance, computed, inject, nextTick, type PropType, provide, ref, toRef, toRefs, useSlots, } from 'vue'; // Components import { BentoActionBar } from '@/components/action-bar'; import { BentoPagination } from '@/components/pagination'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoTypography } from '@/components/typography'; import { BentoDataGridConfigSettings, DataGridColumns, DataGridLazyLoad, DataGridRow } from './components'; // Composables import { useCalculateColumnWidth, useColumnHovering, useColumnReorderer, useConfigSettings, useKeyboardNavigator, useLazyLoading, useScroller, useSelector, } from './composables'; import { useRowCellRefs } from './composables/use-row-cell-refs/use-row-cell-refs'; import { useCalculateStickyElementHeight, useHasSlot, useTouchDevice } from '@/composables'; // Utils import { generateUid } from '@/core/utils/ts/generate-uid'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { handleClipboard } from '@/components/data-grid/utils/handle-clipboard/handle-clipboard'; // Types import { type BentoColumn, type BentoDatagridData, type BentoDatagridDataItem, type BentoDatagridDataItemId, type BentoDatagridEmptyStateProps, BentoDatagridEvent, type BentoDatagridGetDataItemIdFn, type BentoDatagridIsDataItemDisabledFn, type BentoDatagridPaginationProps, type BentoDatagridShouldShowNestedContentFn, type BentoDatagridShouldShowNestedRowsFn, type BentoDatagridSortByColumn, BentoDatagridStyleState, type BentoDataGridVisibleRowsState, } from './data-grid.types'; import { type BentoTabProvidedTitle } from '../tabs/components/tab.types'; import { type BentoDataGridTemplateDataGrid } from '@/templates/data-grid-template/data-grid-template.types'; import { type BentoLazyLoadType } from '@/types/lazy-load'; import type { BentoButtonActionsList } from '@/components/button'; import BentoFilterBar from '@/components/filter-bar/filter-bar.vue'; import { type BentoFilterBarContainerSizeLayout, type BentoFilterBarModel, type BentoFilterBarSearchBarProps, type BentoFilterBarSearchTerm, type BentoFilterValueObject, type BentoFilterValues, } from '@/components/filter-bar'; import { BentoEmptyState } from '@/components/empty-state'; import type { DatagridColumnsHeaderReferences } from '@/components/data-grid/components/data-grid-columns/data-grid-columns.types'; import type { BentoDataGridRowActionsProp } from '@/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.types'; // Keys import { EXTERNAL_TITLE_REGISTER_ENTRY_INJECTION_KEY } from '@/components/tabs/tabs.keys'; import { DATA_GRID_REGISTER_TEMPLATE_INJECTION_KEY } from '@/templates/data-grid-template/data-grid-template.keys'; import { DATA_GRID_SLASHED_ZERO, DATA_GRID_SMALL_CONTAINER, DATA_GRID_TOUCH_DEVICE } from './data-grid.keys'; // Translations import messages from './messages.json'; import { useElementSize } from '@vueuse/core'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * When true, will emit a 'rowClick' event when a row is clicked. Will add cursor: pointer to rows. */ allowRowClicks: { type: Boolean, default: false }, /** * When true, column headers will become draggable, allowing users to rearrange columns. */ allowColumnDragAndDrop: { type: Boolean, default: true }, /** * List of actions that will be rendered in the action bar when user selects items. * * Each object in the list should have a `title`, an `event` and, * optionally, an `icon`. * * The component will display 4 actions and the hide the rest inside a menu. * * @see BentoButton for a list of all the other props supported by each button action. */ bulkActions: { type: Array as PropType<BentoButtonActionsList>, default: null, }, /** * Sets the row cells to the condensed padding state. */ condensed: { type: Boolean, default: false, }, /** * Column definitions using 'key' to relate to data object. */ columns: { type: Array as PropType<Array<BentoColumn>>, required: true, }, /** * Enables / hides the display options panel and the column contextual menu functionality. */ columnPanel: { type: Boolean, default: false }, /** * An array of data objects to display. */ data: { type: Array as PropType<BentoDatagridData>, required: true, }, /** * Empty state props that will be passed down to inner empty-state component */ emptyState: { type: Object as PropType<BentoDatagridEmptyStateProps>, default: undefined, }, /** * filter-bar value (i.e. filters) that will be passed down to inner filter-bar component * {@see BentoFilterBarModel} */ filters: { // TODO: Change this to BentoFilterBarProps['value'] when the interface is available type: Array as PropType<BentoFilterBarModel>, default: undefined, }, /** * Defines the layout (multi-line or one-line) to be used in the filter bar for different * container sizes: * - Large (>=700px): Always multi-line * - Medium (>=430px & <700px) * - Small (<430px) * * @default * { * medium: 'multi-line', * small: 'one-line' * } */ filtersContainerSizeLayout: { type: Object as PropType<BentoFilterBarContainerSizeLayout>, default: undefined, }, /** * An array of the values for each filter set in `value`. * {@see BentoFilterValues} */ filterValues: { type: Array as PropType<BentoFilterValues>, default: () => [], }, /** * An object of the values for each filter in the filter bar where key is a the field name and the value is the BentoFilterBarValue. * * @example * { * 'company': '200', * 'name': 'some text' * }, */ filterValuesObject: { type: Object as PropType<BentoFilterValueObject>, default: () => undefined, }, // TODO: Move this prop as a part of "config" BentoFilterBarModel = Array<BentoFilterModel | BentoFilterSearchModel> in Bento 2.0. // It was not done now because it's a breaking change for the types. /** * Filter's search bar properties with which the placeholder, hint and other * "bento-search-bar" properties can be set. @see {@link BentoFilterBarSearchBarProps} */ filterSearchConfig: { type: Object as PropType<BentoFilterBarSearchBarProps>, default: undefined, }, /** * Filter bar search term useful to filter all the data by text */ filterSearchTerm: { type: String as PropType<BentoFilterBarSearchTerm>, default: undefined, }, /** * Allows the grid to be rendered inside a confined space. * This makes the data-grid expand to fill the height of its parent container, with the grid body becoming scrollable. */ fitContent: { type: Boolean, default: false }, /** * A customisable method to retrieve a unique identifier for a data item in the array of data supplied. * This is used selecting and performance reasons and must exist in each data item. */ getDataItemId: { type: Function as PropType<BentoDatagridGetDataItemIdFn>, default: (item: BentoDatagridDataItem) => item.id, }, /** * Row actions propery that generates which button to show in row actions area */ rowActions: { type: [Function, Array] as PropType<BentoDataGridRowActionsProp>, default: undefined, }, /** * If resize is enabled */ hasResizableColumns: { type: Boolean, default: true, }, /** * Enables the nested content slot functionality. */ hasNestedContent: { type: Boolean, default: false, }, /** * Enables the nested content slot functionality. */ hasNestedRows: { type: Boolean, default: false, }, /** * Highlighted items */ highlightedRows: { type: Array as PropType<Array<BentoDatagridDataItemId>>, default: () => [], }, /** * A customisable method to determine if a row is not selectable. * The default is that every row is selectable. */ isDataItemDisabled: { type: Function as PropType<BentoDatagridIsDataItemDisabledFn>, default: () => false, }, /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button * @values automatic, button, none */ lazyLoadType: { type: String as PropType<BentoLazyLoadType>, default: 'none', validator: (value: BentoLazyLoadType) => ['automatic', 'button', 'none'].includes(value), }, /** * If the loading indicator should be shown. */ loading: { type: Boolean, default: false, }, /** * Enables / Disables the table's surrounding border. */ outline: { type: Boolean, default: false, }, /** * Renders the data grid with no top spacing, to render inside containers that already have spacing */ noSpacingTop: { type: Boolean, default: false, }, /** * Pagination props that will be passed down to inner pagination component */ pagination: { type: Object as PropType<BentoDatagridPaginationProps>, default: undefined, }, /** * Adds an extra option when selecting all items that allows user to select items in other pages too. * Needs pagination.totalCount to be set. */ selectableAcrossPages: { type: Boolean, default: false, }, /** * Renders a font numeric style with slashed zeros instead of normal ones. */ slashedZero: { type: Boolean, default: false }, /** * Sticks the horizontal scrollbar to the bottom of the screen if the data-grid is larger than the screen height. */ stickyHorizontalScrollbar: { type: Boolean, default: false, }, /** * Make pagination stick to the bottom of the page when the data-grid is bigger than the screen. * Because styles have to be applied to pagination container element, this has to be a data-grid prop (rather than a pagination prop). */ stickyPagination: { type: Boolean, default: false, }, /** * Make data-grid filters stick to the top of the page along with the data-grid headers. * Defaults to true. */ stickyFilters: { type: Boolean, default: true, }, /** * Enables / Disables rows to be selected via a checkbox. On select the "select" event will be emitted. */ selectable: { type: Boolean, default: false, }, /** * Selected items */ selection: { type: Array as PropType<Array<BentoDatagridDataItemId>>, default: () => [], }, /** * Enables / Disables the nested-content slot */ shouldShowNestedContent: { type: Function as PropType<BentoDatagridShouldShowNestedContentFn>, default: (_item: BentoDatagridDataItem) => true, }, /** * Returns the nested rows for a given BentoDatagridDataItem. */ shouldShowNestedRows: { type: Function as PropType<BentoDatagridShouldShowNestedRowsFn>, default: (_item: BentoDatagridDataItem) => true, }, /** * Sortby handles proper rendering of sort icon in header. Sorting currently is only supported server side. * It does make sure the currently sorted column has a more visible sort icon, and the sort icon points in the right direction. */ sortBy: { type: Array as PropType<Array<BentoDatagridSortByColumn>>, // NOTE: is an array to allow multi-sorting in the future default: () => [], }, /** * Title of the data grid to be rendered as tab title when a data grid is passed in a tab layout. */ tabTitle: { type: String, default: null, }, /** * Truncates all data grid column headers */ truncateColumnHeaders: { type: Boolean, default: false, }, /** * Toggles the alternating background color of rows. */ zebraRows: { type: Boolean, default: false, }, }); const slots = useSlots(); const hasSlot = useHasSlot(slots); /* == Variables and references == */ // Create refs for elements const body = ref<HTMLDivElement>(null); const columnsRef = ref<HTMLDivElement>(); const columnHeaderRefs = ref<DatagridColumnsHeaderReferences>({}); const dataGridUid = ref<string>(generateUid('data-grid')); const gridcellRefs = ref<{ [key: string]: Array<HTMLDivElement> }>(null); const filtersRef = ref<HTMLDivElement>(null); const filterBarRef = ref<InstanceType<typeof BentoFilterBar>>(null); const paginationContainerRef = ref<HTMLDivElement>(null); const verticalScrollPositionRef = ref(null); const virtualHorizontalScrollbarRef = ref<HTMLDivElement>(null); const { allowColumnDragAndDrop, columns, columnPanel, data, lazyLoadType, loading, hasNestedContent, hasNestedRows, selectable, selection, selectableAcrossPages, stickyPagination, outline, pagination, zebraRows, } = toRefs(props); const hoveredRowIndex = ref(-1); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const frozenColumnsLength = computed(() => columnDefs.value.filter(column => column.frozen).length); /* == Responsive behavior == */ // Identifies if the data-grid is in a container smaller than 550px const MOBILE_BREAKPOINT = 550; const dataGridRef = ref(); const { width } = useElementSize(dataGridRef); const isSmallContainer = computed(() => width.value <= MOBILE_BREAKPOINT); provide(DATA_GRID_SMALL_CONTAINER, isSmallContainer); /* == Touch device behavior == */ const { isTouchDevice } = useTouchDevice(); provide(DATA_GRID_TOUCH_DEVICE, isTouchDevice); /* == Data-grid template == */ const registerDatagridInDataGridTemplate: BentoDataGridTemplateDataGrid = inject( DATA_GRID_REGISTER_TEMPLATE_INJECTION_KEY, () => ({ additionalContentSlotRef: ref(null), isDataGridTemplate: false, stickyAditionalContent: ref(false), stickyTabs: ref(false), tabsRef: ref(null), }) ); const { additionalContentSlotRef, isDataGridTemplate, stickyTabs, tabsRef, stickyAdditionalContent } = registerDatagridInDataGridTemplate(); /* == Composable logic == */ // Column width calculation const { columnDefs, onColumnWidthOverride, gridRowWidthStyle, isMeasuringColumnWidths, isTotalWidthOfColumnsTakingUpAllAvailableGridWidth, columns: localColumnsState, totalWidthOfAllColumns, rowActionsMaxWidth, } = useCalculateColumnWidth(columns, data, body, columnHeaderRefs, gridcellRefs, { hasColumnPanel: columnPanel, hasSelectableColumn: selectable, hasSelectableAcrossPages: selectableAcrossPages, hasNestedContent, hasNestedRows, allowColumnDragAndDrop, isDataGridTemplate: isDataGridTemplate, }); const hasRowActions = computed(() => props.rowActions !== undefined); const { registerRowCellRefs, rebuildGridcellRefs } = useRowCellRefs( gridcellRefs, rowActionsMaxWidth, hasRowActions ); /* == Emitting logic == */ const definedEmitsFunction = defineEmits([ BentoDatagridEvent.COLUMNS, BentoDatagridEvent.COLUMN_RESIZE, BentoDatagridEvent.ITEMS_PAGE, BentoDatagridEvent.FILTER_SEARCH_INPUT, BentoDatagridEvent.FILTER_SEARCH_CLEAR, BentoDatagridEvent.NAVIGATE, BentoDatagridEvent.NESTED_CONTENT_TOGGLED, BentoDatagridEvent.UPDATE_COLUMNS, BentoDatagridEvent.UPDATE_CONDENSED, BentoDatagridEvent.UPDATE_FILTER_VALUES, BentoDatagridEvent.UPDATE_FILTER_VALUES_OBJECT, BentoDatagridEvent.UPDATE_FILTERS, BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM, BentoDatagridEvent.UPDATE_SHOW_MORE, BentoDatagridEvent.UPDATE_PAGINATION, BentoDatagridEvent.UPDATE_SELECTION, BentoDatagridEvent.UPDATE_SORT_BY, BentoDatagridEvent.ROW_CLICK, BentoDatagridEvent.SELECT, BentoDatagridEvent.SELECT_ALL_ACROSS_PAGES, BentoDatagridEvent.SELECT_ALL_CURRENT_RESULTS, BentoDatagridEvent.DESELECT_ALL_CURRENT_RESULTS, BentoDatagridEvent.SORT, BentoDatagridEvent.CELL_FOCUS, ]); /** * Hooks into the emit function and updates the local columns state if we're updating columns. * This is so if users use a computed ref with the columns property the ordering / toggling visibility will still function correctly. */ const emit: typeof definedEmitsFunction = (event, ...args) => { // If we updating columns update the local columns state if (event === BentoDatagridEvent.COLUMNS || event === BentoDatagridEvent.UPDATE_COLUMNS) { [localColumnsState.value] = args; } definedEmitsFunction(event, ...args); }; // Calculate dynamic top position of sticky filters const stickyFiltersHeight = useCalculateStickyElementHeight( tabsRef, stickyTabs, stickyAdditionalContent, additionalContentSlotRef ); const computedStickyFiltersTopPosition = computed(() => props.stickyFilters ? `${stickyFiltersHeight.value}px` : null ); // Calculate dynamic top position of column headers const columnHeadersHeight = useCalculateStickyElementHeight(filtersRef, toRef(props, 'stickyFilters')); const computedColumnHeadersTopPosition = computed( () => `${columnHeadersHeight.value + stickyFiltersHeight.value}px` ); // Config Settings (condensed, visbility and freezing settings) const { sortedColumns, columnSets, isOnlyOneVisibleColumn, internalZebraRows, onConfigSettingsColumnsUpdate, onConfigSettingsColumnUpdate, onConfigSettingsCondensedUpdate, onConfigSettingsZebraRowsUpdate, } = useConfigSettings(columnDefs, zebraRows, emit); // Column hovering const { hoveredOverColumn, hoveredOverColumnState } = useColumnHovering( columnSets, selectable, outline, isTotalWidthOfColumnsTakingUpAllAvailableGridWidth ); // Re-ordering column (dragging) const { draggingColumns, draggingEventsForColumns, draggingEventsForRowCell, onMoveColumnByOffset } = useColumnReorderer(sortedColumns, allowColumnDragAndDrop, emit); // Selecting const selectableAcrossPagesComputed = computed( () => props.data?.length > 0 && !!props.selectableAcrossPages && !!props.pagination && props.pagination.totalCount > 0 ); const { onAllRowSelect, onRowSelect, onNestedRowSelect, onDeselectAll, allItemsSelectedState, selectedItems, selectionState, } = useSelector( selection, emit, data, props.getDataItemId, props.isDataItemDisabled, selectableAcrossPagesComputed, pagination ); const visibleRowsState = computed<BentoDataGridVisibleRowsState>(() => ({ filtered: filterBarRef.value?.isClearAllVisible || filterBarRef.value?.isResetAllVisible || !!props.filterSearchTerm, allRowsVisible: pagination.value?.size >= pagination.value?.totalCount, })); // Keyboard navigation const { navigationInput } = useKeyboardNavigator( gridcellRefs, columnHeaderRefs, sortedColumns, selectable, hasRowActions, columnPanel ); // Lazy loading const { isLazyLoad, isInfiniteScroll, verticalScrollPosition, emitLazyLoadEvent } = useLazyLoading( lazyLoadType, verticalScrollPositionRef, body, loading, emit ); // Scrolling const { updateScrollPosition, updateScrollPositionOnFrozenColumnsNavigation, syncVirtualScrollerWithBody, hasLeftOverflow, hasRightOverflow, horizontalScrollbarConditionalStyles, } = useScroller( body, columnsRef, verticalScrollPosition, virtualHorizontalScrollbarRef, paginationContainerRef, totalWidthOfAllColumns, { isLazyLoad, pagination, stickyPagination, isDataGridTemplate, } ); /* == Empty State == */ const onEmptyStateClearResetButtonClick = () => { if (filterBarRef.value.isClearAllVisible) { filterBarRef.value.clearAllFilters(); } else if (filterBarRef.value.isResetAllVisible) { filterBarRef.value.resetAllFilters(); } }; const limitedEmptyStateProps = computed<BentoDatagridEmptyStateProps>(() => ({ ...props.emptyState, variant: props.emptyState?.variant === 'embedded' || props.emptyState?.variant === 'basic' ? props.emptyState.variant : 'basic', })); /* == Styles == */ // Conditional grid classes const conditionalGridClasses = computed(() => ({ [`b-data-grid--${BentoDatagridStyleState.OUTLINE}`]: props.outline, [`b-data-grid--${BentoDatagridStyleState.SCROLL}`]: props.fitContent || isLazyLoad.value, [`b-data-grid--${BentoDatagridStyleState.STICKY_FILTERS}`]: props.filters && props.stickyFilters, [`b-data-grid--${BentoDatagridStyleState.OVERFLOW_SHADOW_LEFT}`]: hasLeftOverflow.value && !frozenColumnsLength.value, [`b-data-grid--${BentoDatagridStyleState.OVERFLOW_SHADOW_RIGHT}`]: hasRightOverflow.value && props.rowActions === undefined, [`b-data-grid--${BentoDatagridStyleState.NO_SPACING_TOP}`]: props.noSpacingTop, [`b-data-grid--${BentoDatagridStyleState.FIT_CONTENT}`]: props.fitContent, [`b-data-grid--${BentoDatagridStyleState.ZEBRA_ROWS}`]: internalZebraRows.value, 'b-data-grid--lazy-load': isLazyLoad.value, })); // Conditional grid body classes const conditionalGridBodyClasses = computed(() => ({ 'b-data-grid__body--sticky-horizontal-scrollbar': props.stickyHorizontalScrollbar, })); const computedColumnPanelAlignment = computed(() => columnSets.value.findIndex(el => el.columnSet.length)); const showFrozenColumnsSeparator = computed(() => { const isSelectionOnlyFrozenColumn = (props.selectable || props.hasNestedContent || props.hasNestedRows) && frozenColumnsLength.value === 1; return !isSelectionOnlyFrozenColumn || hasLeftOverflow.value; }); // Conditional filter classes const conditionalFilterClasses = computed(() => ({ 'b-data-grid__filters--sticky': !!props.stickyFilters || isDataGridTemplate, })); // Conditional pagination classes const conditionalPaginationClasses = computed(() => ({ 'b-data-grid__pagination--padding-left-unset': props.columns?.[0]?.padding?.left === false, 'b-data-grid__pagination--padding-right-unset': props.columns?.[props.columns.length - 1]?.padding?.right === false, 'b-data-grid__pagination--sticky': !!props.stickyPagination || isDataGridTemplate, })); /* == Ref registration methods == */ const registerColumnHeaderRefs = (el: Element | ComponentPublicInstance) => { // We always know this will be used on a DataGridColumns component so we can cast it const datagridColumnsComponent = el as InstanceType<typeof DataGridColumns>; if (el !== null && datagridColumnsComponent?.columnHeaderRefs) { Object.entries(datagridColumnsComponent?.columnHeaderRefs).forEach(([fieldName, colHeaderEl]) => { columnHeaderRefs.value[fieldName] = colHeaderEl as HTMLDivElement; }); } }; /* == Row methods == */ // Row selecting const onRowClick = (row: BentoDatagridDataItem, event: MouseEvent) => { if (props.allowRowClicks) { emit(BentoDatagridEvent.ROW_CLICK, row, event); } }; const onNestedContentToggled = (rowData: BentoDatagridDataItem) => { emit(BentoDatagridEvent.NESTED_CONTENT_TOGGLED, rowData); nextTick(() => rebuildGridcellRefs()); }; // Programmatically set the hover state on a row const onCellHover = (rowIndex: number) => { hoveredRowIndex.value = rowIndex; }; /* == a11y == */ const ariaGridRole = computed(() => (props.hasNestedContent || props.hasNestedRows ? 'treegrid' : 'grid')); /* == Action bar == */ const itemsSelectedCounter = computed(() => selectedItems.value?.length ?? 0); const showActionBar = computed(() => props.bulkActions && itemsSelectedCounter.value > 0); /* == Emits & Events == */ // Resizing const onColumnResized = (resizeEvent: { field: string; width: number }) => emit(BentoDatagridEvent.COLUMN_RESIZE, resizeEvent); // Sorting const onSort = (updatedSortBy: Array<BentoDatagridSortByColumn>) => { emit(BentoDatagridEvent.UPDATE_SORT_BY, updatedSortBy); emit(BentoDatagridEvent.SORT, updatedSortBy); }; // Pagination const onPaginationNavigate = (page: number, size: number) => { emit(BentoDatagridEvent.NAVIGATE, page); emit(BentoDatagridEvent.UPDATE_PAGINATION, { ...props.pagination, size, page, }); }; const onItemsPerPageChange = (itemsPerPage: number) => { emit(BentoDatagridEvent.ITEMS_PAGE, itemsPerPage); }; // Filter bar const onSearchTermCleared = () => { emit(BentoDatagridEvent.FILTER_SEARCH_CLEAR); }; const onSearchTermInput = newSearchTerm => { emit(BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM, newSearchTerm); emit(BentoDatagridEvent.FILTER_SEARCH_INPUT, newSearchTerm); }; /* == Warnings == */ const hasMandatoryColumn = computed(() => columns.value.some(column => column.mandatory)); if (!hasMandatoryColumn.value) { printDevelopmentWarning( 'WARNING: At least one BentoDataGrid column should be set as mandatory to ensure data is always displayed correctly.' ); } /* == Provide/inject == */ const setTitle: BentoTabProvidedTitle = inject(EXTERNAL_TITLE_REGISTER_ENTRY_INJECTION_KEY, () => null); setTitle(props.tabTitle); provide(DATA_GRID_SLASHED_ZERO, toRef(props, 'slashedZero')); /* == Expose methods == */ defineExpose({ onEmptyStateClearResetButtonClick }); </script> <script lang="ts"> /** * A component to display data in columns and row format with a whole array of configuration for visualization and interaction. * * @example * import { BentoDataGrid } from '@adyen/bento-vue2'; * * export default { * components: { BentoDataGrid }, * template: ` * <bento-data-grid * :data="[ * { id: '123', name: 'John', company: 'TestCompany', 'payment': 200 } * { id: '321', name: 'Jane', company: 'Adyen', 'payment': 300 } * ]" * :columns="[ * { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, * { field: 'company', label: 'Reference' }, * { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, * ]" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-data-grid', }; </script> <style lang="scss" scoped src="./data-grid.scss" />
1
+ <template> <div ref="dataGridRef" class="b-data-grid" data-testid="data-grid" :class="conditionalGridClasses" aria-live="polite" :aria-busy="loading" @copy="handleClipboard" > <div v-if="filters" ref="filtersRef" class="b-data-grid__filters" :class="conditionalFilterClasses" :style="{ top: computedStickyFiltersTopPosition }" :aria-controls="dataGridUid" :aria-label="t('filterBar')" role="region" > <bento-filter-bar v-if="filters" ref="filterBarRef" :config="filters" :container-size-layout="filtersContainerSizeLayout" :filter-values="filterValues" :filter-values-object="filterValuesObject" :search="filterSearchConfig" :search-term="filterSearchTerm" @search-input="onSearchTermInput" @search-clear="onSearchTermCleared" @input="filters => $emit(BentoDatagridEvent.UPDATE_FILTERS, filters)" @update:filter-values="filterValues => $emit(BentoDatagridEvent.UPDATE_FILTER_VALUES, filterValues)" @update:filter-values-object=" filterValueObject => $emit(BentoDatagridEvent.UPDATE_FILTER_VALUES_OBJECT, filterValueObject) " /> </div> <div v-if="$slots.header" class="b-data-grid__header"> <slot name="header"></slot> </div> <div :id="dataGridUid" :role="ariaGridRole" :aria-rowcount="data.length" :aria-colcount="columns.length" :aria-label="emptyState ? t('noResults') : null" class="b-data-grid__wrapper" :style="{ top: computedColumnHeadersTopPosition }" > <div class="b-data-grid__column-headers" :style="{ top: computedColumnHeadersTopPosition }"> <div ref="columnsRef" role="rowgroup" class="b-data-grid__columns-row" @scroll="updateScrollPosition" @keydown="navigationInput" > <div v-if="columnPanel" role="row"> <bento-data-grid-config-settings class="b-data-grid__config-settings" role="gridcell" :columns="localColumnsState" :condensed="condensed" :zebra-rows="internalZebraRows" @update:condensed="onConfigSettingsCondensedUpdate" @update:zebra-rows="onConfigSettingsZebraRowsUpdate" @update:columns="onConfigSettingsColumnsUpdate" /> </div> <div :style="gridRowWidthStyle" class="b-data-grid__columns" role="row"> <template v-for="({ columnSet, id, isFrozenColumns }, index) of columnSets"> <data-grid-columns v-if="columnSet?.length || (!isFrozenColumns && rowActionsMaxWidth)" :key="`data-grid-columns-${id}`" :ref="el => registerColumnHeaderRefs(el)" :columns="columnSet" :has-resizable-columns="hasResizableColumns" :sort-by="sortBy" :all-items-selected-state="allItemsSelectedState" :is-frozen-columns="isFrozenColumns" :show-frozen-columns-separator="showFrozenColumnsSeparator" :dragging-columns="draggingColumns" :has-draggable-columns="allowColumnDragAndDrop" :has-column-panel="columnPanel" :has-left-overflow="hasLeftOverflow" :has-first-column-panel-right-aligned="index === computedColumnPanelAlignment" :hovered-over-column-state="hoveredOverColumnState" :is-measuring-column-widths="isMeasuringColumnWidths" :is-only-one-visible-column="isOnlyOneVisibleColumn" :selectable="data?.length > 0 && selectable" :selectable-across-pages="selectableAcrossPagesComputed" :selected-items="selectedItems" :visible-rows-state="visibleRowsState" :selection-state="selectionState" :truncate-column-headers="truncateColumnHeaders" :row-actions-maximum-width="rowActionsMaxWidth" v-on="draggingEventsForColumns" @column-hover="hoveredOverColumn = $event" @column-move="onMoveColumnByOffset" @column-update="onConfigSettingsColumnUpdate" @column-resize="onColumnResized" @column-resizing="onColumnWidthOverride" @select-all-rows="onAllRowSelect" @select-all-across-pages="emit(BentoDatagridEvent.SELECT_ALL_ACROSS_PAGES)" @select-all-current-results="emit(BentoDatagridEvent.SELECT_ALL_CURRENT_RESULTS)" @deselect-all-current-results="emit(BentoDatagridEvent.DESELECT_ALL_CURRENT_RESULTS)" @deselect-all="onDeselectAll" @sort="onSort" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-columns> </template> </div> </div> </div> <div ref="body" data-testid="data-grid-body" class="b-data-grid__body" :class="conditionalGridBodyClasses" @scroll="updateScrollPosition" > <div v-if="loading && !isLazyLoad" class="b-data-grid__loading"> <bento-loading-indicator class="b-data-grid__loading-indicator" large /> <bento-typography el="span" variant="caption">{{ t('gettingYourData') }}</bento-typography> </div> <div v-else role="rowgroup" @keydown="navigationInput"> <template v-if="data?.length === 0 && emptyState"> <div role="row" tabindex="-1"> <div role="gridcell" :aria-colspan="columns.length" tabindex="-1"> <bento-empty-state class="b-data-grid__empty-state" v-bind="limitedEmptyStateProps" /> </div> </div> </template> <data-grid-row v-for="(rowData, rowIndex) in data" v-else :key="rowIndex" :ref="el => registerRowCellRefs(el, rowIndex)" :style="gridRowWidthStyle" :allow-row-clicks="allowRowClicks" :column-sets="columnSets" :condensed="condensed" :dragging-columns="draggingColumns" :dragging-events-for-row-cell="draggingEventsForRowCell" :get-data-item-id="getDataItemId" :has-nested-content="hasNestedContent" :has-nested-rows="hasNestedRows" :has-left-overflow="hasLeftOverflow" :has-right-overflow="hasRightOverflow" :highlighted-rows="highlightedRows" :hovered-row-index="hoveredRowIndex" :hovered-over-column-state="hoveredOverColumnState" :is-data-item-disabled="isDataItemDisabled" :is-row-even="internalZebraRows ? rowIndex % 2 !== 0 : false" :is-row-last-child="rowIndex + 1 === data?.length" :is-measuring-column-widths="isMeasuringColumnWidths" :row-actions-maximum-width="rowActionsMaxWidth" :row-actions="rowActions" :row-data="rowData" :row-index="rowIndex" :row-set-size="data?.length" :show-frozen-columns-separator="showFrozenColumnsSeparator" :should-show-nested-content="shouldShowNestedContent" :should-show-nested-rows="shouldShowNestedRows" :selectable="selectable" :selected-items="selectedItems" @cell-hovered="onCellHover" @cell-focus="emit(BentoDatagridEvent.CELL_FOCUS, $event)" @nested-content-toggled="onNestedContentToggled($event)" @row-click="onRowClick" @on-frozen-columns-key-press="updateScrollPositionOnFrozenColumnsNavigation" @select-row="onRowSelect" @select-nested-row="onNestedRowSelect" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-row> </div> </div> </div> <data-grid-lazy-load v-if="isLazyLoad" :loading="loading" :is-infinite-scroll="isInfiniteScroll" @update:show-more="emitLazyLoadEvent" /> <div v-if="stickyHorizontalScrollbar" ref="virtualHorizontalScrollbarRef" class="b-data-grid__horizontal-scrollbar" aria-hidden="true" data-testid="sticky-horizontal-scrollbar" :style="horizontalScrollbarConditionalStyles" @scroll="syncVirtualScrollerWithBody" > <div :style="{ width: `${totalWidthOfAllColumns}px`, height: '15px' }"></div> </div> <div v-if="pagination && !isLazyLoad" ref="paginationContainerRef" data-testid="pagination-container" class="b-data-grid__pagination" :class="conditionalPaginationClasses" > <bento-pagination v-bind="pagination" :aria-label="t('pagination')" :aria-controls="dataGridUid" @navigate="onPaginationNavigate" @items-page="onItemsPerPageChange" > <slot v-if="hasSlot('pagination-slot')" name="pagination-slot"></slot> </bento-pagination> </div> <div v-if="$slots.footer" class="b-data-grid__footer"><slot name="footer"></slot></div> <bento-action-bar v-if="showActionBar" :actions="bulkActions" :item-counter="itemsSelectedCounter" :extra-bottom-spacing="stickyPagination" @close="onDeselectAll" /> </div> </template> <script lang="ts" setup> import { type ComponentPublicInstance, computed, inject, nextTick, type PropType, provide, ref, toRef, toRefs, useSlots, } from 'vue'; // Components import { BentoActionBar } from '@/components/action-bar'; import { BentoPagination } from '@/components/pagination'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoTypography } from '@/components/typography'; import { BentoDataGridConfigSettings, DataGridColumns, DataGridLazyLoad, DataGridRow } from './components'; // Composables import { useCalculateColumnWidth, useColumnHovering, useColumnReorderer, useConfigSettings, useKeyboardNavigator, useLazyLoading, useScroller, useSelector, } from './composables'; import { useRowCellRefs } from './composables/use-row-cell-refs/use-row-cell-refs'; import { useCalculateStickyElementHeight, useHasSlot, useTouchDevice } from '@/composables'; // Utils import { generateUid } from '@/core/utils/ts/generate-uid'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { handleClipboard } from '@/components/data-grid/utils/handle-clipboard/handle-clipboard'; // Types import { type BentoColumn, type BentoDatagridData, type BentoDatagridDataItem, type BentoDatagridDataItemId, type BentoDatagridEmptyStateProps, BentoDatagridEvent, type BentoDatagridGetDataItemIdFn, type BentoDatagridIsDataItemDisabledFn, type BentoDatagridPaginationProps, type BentoDatagridShouldShowNestedContentFn, type BentoDatagridShouldShowNestedRowsFn, type BentoDatagridSortByColumn, BentoDatagridStyleState, type BentoDataGridVisibleRowsState, } from './data-grid.types'; import { type BentoTabProvidedTitle } from '../tabs/components/tab.types'; import { type BentoDataGridTemplateDataGrid } from '@/templates/data-grid-template/data-grid-template.types'; import { type BentoLazyLoadType } from '@/types/lazy-load'; import type { BentoButtonActionsList } from '@/components/button'; import BentoFilterBar from '@/components/filter-bar/filter-bar.vue'; import { type BentoFilterBarContainerSizeLayout, type BentoFilterBarModel, type BentoFilterBarSearchBarProps, type BentoFilterBarSearchTerm, type BentoFilterValueObject, type BentoFilterValues, } from '@/components/filter-bar'; import { BentoEmptyState } from '@/components/empty-state'; import type { DatagridColumnsHeaderReferences } from '@/components/data-grid/components/data-grid-columns/data-grid-columns.types'; import type { BentoDataGridRowActionsProp } from '@/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.types'; // Keys import { EXTERNAL_TITLE_REGISTER_ENTRY_INJECTION_KEY } from '@/components/tabs/tabs.keys'; import { DATA_GRID_REGISTER_TEMPLATE_INJECTION_KEY } from '@/templates/data-grid-template/data-grid-template.keys'; import { DATA_GRID_SLASHED_ZERO, DATA_GRID_SMALL_CONTAINER, DATA_GRID_TOUCH_DEVICE } from './data-grid.keys'; // Translations import messages from './messages.json'; import { useElementSize } from '@vueuse/core'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * When true, will emit a 'rowClick' event when a row is clicked. Will add cursor: pointer to rows. */ allowRowClicks: { type: Boolean, default: false }, /** * When true, column headers will become draggable, allowing users to rearrange columns. */ allowColumnDragAndDrop: { type: Boolean, default: true }, /** * List of actions that will be rendered in the action bar when user selects items. * * Each object in the list should have a `title`, an `event` and, * optionally, an `icon`. * * The component will display 4 actions and the hide the rest inside a menu. * * @see BentoButton for a list of all the other props supported by each button action. */ bulkActions: { type: Array as PropType<BentoButtonActionsList>, default: null, }, /** * Sets the row cells to the condensed padding state. */ condensed: { type: Boolean, default: false, }, /** * Column definitions using 'key' to relate to data object. */ columns: { type: Array as PropType<Array<BentoColumn>>, required: true, }, /** * Enables / hides the display options panel and the column contextual menu functionality. */ columnPanel: { type: Boolean, default: false }, /** * An array of data objects to display. */ data: { type: Array as PropType<BentoDatagridData>, required: true, }, /** * Empty state props that will be passed down to inner empty-state component */ emptyState: { type: Object as PropType<BentoDatagridEmptyStateProps>, default: undefined, }, /** * filter-bar value (i.e. filters) that will be passed down to inner filter-bar component * {@see BentoFilterBarModel} */ filters: { // TODO: Change this to BentoFilterBarProps['value'] when the interface is available type: Array as PropType<BentoFilterBarModel>, default: undefined, }, /** * Defines the layout (multi-line or one-line) to be used in the filter bar for different * container sizes: * - Large (>=700px): Always multi-line * - Medium (>=430px & <700px) * - Small (<430px) * * @default * { * medium: 'multi-line', * small: 'one-line' * } */ filtersContainerSizeLayout: { type: Object as PropType<BentoFilterBarContainerSizeLayout>, default: undefined, }, /** * An array of the values for each filter set in `value`. * {@see BentoFilterValues} */ filterValues: { type: Array as PropType<BentoFilterValues>, default: () => [], }, /** * An object of the values for each filter in the filter bar where key is a the field name and the value is the BentoFilterBarValue. * * @example * { * 'company': '200', * 'name': 'some text' * }, */ filterValuesObject: { type: Object as PropType<BentoFilterValueObject>, default: () => undefined, }, // TODO: Move this prop as a part of "config" BentoFilterBarModel = Array<BentoFilterModel | BentoFilterSearchModel> in Bento 2.0. // It was not done now because it's a breaking change for the types. /** * Filter's search bar properties with which the placeholder, hint and other * "bento-search-bar" properties can be set. @see {@link BentoFilterBarSearchBarProps} */ filterSearchConfig: { type: Object as PropType<BentoFilterBarSearchBarProps>, default: undefined, }, /** * Filter bar search term useful to filter all the data by text */ filterSearchTerm: { type: String as PropType<BentoFilterBarSearchTerm>, default: undefined, }, /** * Allows the grid to be rendered inside a confined space. * This makes the data-grid expand to fill the height of its parent container, with the grid body becoming scrollable. */ fitContent: { type: Boolean, default: false }, /** * A customisable method to retrieve a unique identifier for a data item in the array of data supplied. * This is used selecting and performance reasons and must exist in each data item. */ getDataItemId: { type: Function as PropType<BentoDatagridGetDataItemIdFn>, default: (item: BentoDatagridDataItem) => item.id, }, /** * Row actions propery that generates which button to show in row actions area */ rowActions: { type: [Function, Array] as PropType<BentoDataGridRowActionsProp>, default: undefined, }, /** * If resize is enabled */ hasResizableColumns: { type: Boolean, default: true, }, /** * Enables the nested content slot functionality. */ hasNestedContent: { type: Boolean, default: false, }, /** * Enables the nested content slot functionality. */ hasNestedRows: { type: Boolean, default: false, }, /** * Highlighted items */ highlightedRows: { type: Array as PropType<Array<BentoDatagridDataItemId>>, default: () => [], }, /** * A customisable method to determine if a row is not selectable. * The default is that every row is selectable. */ isDataItemDisabled: { type: Function as PropType<BentoDatagridIsDataItemDisabledFn>, default: () => false, }, /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button * @values automatic, button, none */ lazyLoadType: { type: String as PropType<BentoLazyLoadType>, default: 'none', validator: (value: BentoLazyLoadType) => ['automatic', 'button', 'none'].includes(value), }, /** * If the loading indicator should be shown. */ loading: { type: Boolean, default: false, }, /** * Enables / Disables the table's surrounding border. */ outline: { type: Boolean, default: false, }, /** * Renders the data grid with no top spacing, to render inside containers that already have spacing */ noSpacingTop: { type: Boolean, default: false, }, /** * Pagination props that will be passed down to inner pagination component */ pagination: { type: Object as PropType<BentoDatagridPaginationProps>, default: undefined, }, /** * Adds an extra option when selecting all items that allows user to select items in other pages too. * Needs pagination.totalCount to be set. */ selectableAcrossPages: { type: Boolean, default: false, }, /** * Renders a font numeric style with slashed zeros instead of normal ones. */ slashedZero: { type: Boolean, default: false }, /** * Sticks the horizontal scrollbar to the bottom of the screen if the data-grid is larger than the screen height. */ stickyHorizontalScrollbar: { type: Boolean, default: false, }, /** * Make pagination stick to the bottom of the page when the data-grid is bigger than the screen. * Because styles have to be applied to pagination container element, this has to be a data-grid prop (rather than a pagination prop). */ stickyPagination: { type: Boolean, default: false, }, /** * Make data-grid filters stick to the top of the page along with the data-grid headers. * Defaults to true. */ stickyFilters: { type: Boolean, default: true, }, /** * Enables / Disables rows to be selected via a checkbox. On select the "select" event will be emitted. */ selectable: { type: Boolean, default: false, }, /** * Selected items */ selection: { type: Array as PropType<Array<BentoDatagridDataItemId>>, default: () => [], }, /** * Enables / Disables the nested-content slot */ shouldShowNestedContent: { type: Function as PropType<BentoDatagridShouldShowNestedContentFn>, default: (_item: BentoDatagridDataItem) => true, }, /** * Returns the nested rows for a given BentoDatagridDataItem. */ shouldShowNestedRows: { type: Function as PropType<BentoDatagridShouldShowNestedRowsFn>, default: (_item: BentoDatagridDataItem) => true, }, /** * Sortby handles proper rendering of sort icon in header. Sorting currently is only supported server side. * It does make sure the currently sorted column has a more visible sort icon, and the sort icon points in the right direction. */ sortBy: { type: Array as PropType<Array<BentoDatagridSortByColumn>>, // NOTE: is an array to allow multi-sorting in the future default: () => [], }, /** * Title of the data grid to be rendered as tab title when a data grid is passed in a tab layout. */ tabTitle: { type: String, default: null, }, /** * Truncates all data grid column headers */ truncateColumnHeaders: { type: Boolean, default: false, }, /** * Toggles the alternating background color of rows. */ zebraRows: { type: Boolean, default: false, }, }); const slots = useSlots(); const hasSlot = useHasSlot(slots); /* == Variables and references == */ // Create refs for elements const body = ref<HTMLDivElement>(null); const columnsRef = ref<HTMLDivElement>(); const columnHeaderRefs = ref<DatagridColumnsHeaderReferences>({}); const dataGridUid = ref<string>(generateUid('data-grid')); const gridcellRefs = ref<{ [key: string]: Array<HTMLDivElement> }>(null); const filtersRef = ref<HTMLDivElement>(null); const filterBarRef = ref<InstanceType<typeof BentoFilterBar>>(null); const paginationContainerRef = ref<HTMLDivElement>(null); const verticalScrollPositionRef = ref(null); const virtualHorizontalScrollbarRef = ref<HTMLDivElement>(null); const { allowColumnDragAndDrop, columns, columnPanel, data, lazyLoadType, loading, hasNestedContent, hasNestedRows, selectable, selection, selectableAcrossPages, stickyPagination, outline, pagination, zebraRows, } = toRefs(props); const hoveredRowIndex = ref(-1); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const frozenColumnsLength = computed(() => columnDefs.value.filter(column => column.frozen).length); /* == Responsive behavior == */ // Identifies if the data-grid is in a container smaller than 550px const MOBILE_BREAKPOINT = 550; const dataGridRef = ref(); const { width } = useElementSize(dataGridRef); const isSmallContainer = computed(() => width.value <= MOBILE_BREAKPOINT); provide(DATA_GRID_SMALL_CONTAINER, isSmallContainer); /* == Touch device behavior == */ const { isTouchDevice } = useTouchDevice(); provide(DATA_GRID_TOUCH_DEVICE, isTouchDevice); /* == Data-grid template == */ const registerDatagridInDataGridTemplate: BentoDataGridTemplateDataGrid = inject( DATA_GRID_REGISTER_TEMPLATE_INJECTION_KEY, () => ({ additionalContentSlotRef: ref(null), isDataGridTemplate: false, stickyAditionalContent: ref(false), stickyTabs: ref(false), tabsRef: ref(null), }) ); const { additionalContentSlotRef, isDataGridTemplate, stickyTabs, tabsRef, stickyAdditionalContent } = registerDatagridInDataGridTemplate(); /* == Composable logic == */ // Column width calculation const { columnDefs, onColumnWidthOverride, gridRowWidthStyle, isMeasuringColumnWidths, isTotalWidthOfColumnsTakingUpAllAvailableGridWidth, columns: localColumnsState, totalWidthOfAllColumns, rowActionsMaxWidth, } = useCalculateColumnWidth(columns, data, body, columnHeaderRefs, gridcellRefs, { hasColumnPanel: columnPanel, hasSelectableColumn: selectable, hasSelectableAcrossPages: selectableAcrossPages, hasNestedContent, hasNestedRows, allowColumnDragAndDrop, isDataGridTemplate: isDataGridTemplate, }); const hasRowActions = computed(() => props.rowActions !== undefined); const { registerRowCellRefs, rebuildGridcellRefs } = useRowCellRefs( gridcellRefs, rowActionsMaxWidth, hasRowActions ); /* == Emitting logic == */ const definedEmitsFunction = defineEmits([ BentoDatagridEvent.COLUMNS, BentoDatagridEvent.COLUMN_RESIZE, BentoDatagridEvent.ITEMS_PAGE, BentoDatagridEvent.FILTER_SEARCH_INPUT, BentoDatagridEvent.FILTER_SEARCH_CLEAR, BentoDatagridEvent.NAVIGATE, BentoDatagridEvent.NESTED_CONTENT_TOGGLED, BentoDatagridEvent.UPDATE_COLUMNS, BentoDatagridEvent.UPDATE_CONDENSED, BentoDatagridEvent.UPDATE_FILTER_VALUES, BentoDatagridEvent.UPDATE_FILTER_VALUES_OBJECT, BentoDatagridEvent.UPDATE_FILTERS, BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM, BentoDatagridEvent.UPDATE_SHOW_MORE, BentoDatagridEvent.UPDATE_PAGINATION, BentoDatagridEvent.UPDATE_SELECTION, BentoDatagridEvent.UPDATE_SORT_BY, BentoDatagridEvent.ROW_CLICK, BentoDatagridEvent.SELECT, BentoDatagridEvent.SELECT_ALL_ACROSS_PAGES, BentoDatagridEvent.SELECT_ALL_CURRENT_RESULTS, BentoDatagridEvent.DESELECT_ALL_CURRENT_RESULTS, BentoDatagridEvent.SORT, BentoDatagridEvent.CELL_FOCUS, ]); /** * Hooks into the emit function and updates the local columns state if we're updating columns. * This is so if users use a computed ref with the columns property the ordering / toggling visibility will still function correctly. */ const emit: typeof definedEmitsFunction = (event, ...args) => { // If we updating columns update the local columns state if (event === BentoDatagridEvent.COLUMNS || event === BentoDatagridEvent.UPDATE_COLUMNS) { [localColumnsState.value] = args; } definedEmitsFunction(event, ...args); }; // Calculate dynamic top position of sticky filters const stickyFiltersHeight = useCalculateStickyElementHeight( tabsRef, stickyTabs, stickyAdditionalContent, additionalContentSlotRef ); const computedStickyFiltersTopPosition = computed(() => props.stickyFilters ? `${stickyFiltersHeight.value}px` : null ); // Calculate dynamic top position of column headers const columnHeadersHeight = useCalculateStickyElementHeight(filtersRef, toRef(props, 'stickyFilters')); const computedColumnHeadersTopPosition = computed( () => `${columnHeadersHeight.value + stickyFiltersHeight.value}px` ); // Config Settings (condensed, visbility and freezing settings) const { sortedColumns, columnSets, isOnlyOneVisibleColumn, internalZebraRows, onConfigSettingsColumnsUpdate, onConfigSettingsColumnUpdate, onConfigSettingsCondensedUpdate, onConfigSettingsZebraRowsUpdate, } = useConfigSettings(columnDefs, zebraRows, emit); // Column hovering const { hoveredOverColumn, hoveredOverColumnState } = useColumnHovering( columnSets, selectable, outline, isTotalWidthOfColumnsTakingUpAllAvailableGridWidth ); // Re-ordering column (dragging) const { draggingColumns, draggingEventsForColumns, draggingEventsForRowCell, onMoveColumnByOffset } = useColumnReorderer(sortedColumns, allowColumnDragAndDrop, emit); // Selecting const selectableAcrossPagesComputed = computed( () => props.data?.length > 0 && !!props.selectableAcrossPages && !!props.pagination && props.pagination.totalCount > 0 ); const { onAllRowSelect, onRowSelect, onNestedRowSelect, onDeselectAll, allItemsSelectedState, selectedItems, selectionState, } = useSelector( selection, emit, data, props.getDataItemId, props.isDataItemDisabled, selectableAcrossPagesComputed, pagination ); const visibleRowsState = computed<BentoDataGridVisibleRowsState>(() => ({ filtered: filterBarRef.value?.isClearAllVisible || filterBarRef.value?.isResetAllVisible || !!props.filterSearchTerm, allRowsVisible: pagination.value?.size >= pagination.value?.totalCount, })); // Keyboard navigation const { navigationInput } = useKeyboardNavigator( gridcellRefs, columnHeaderRefs, sortedColumns, selectable, hasRowActions, columnPanel ); // Lazy loading const { isLazyLoad, isInfiniteScroll, verticalScrollPosition, emitLazyLoadEvent } = useLazyLoading( lazyLoadType, verticalScrollPositionRef, body, loading, emit ); // Scrolling const { updateScrollPosition, updateScrollPositionOnFrozenColumnsNavigation, syncVirtualScrollerWithBody, hasLeftOverflow, hasRightOverflow, horizontalScrollbarConditionalStyles, } = useScroller( body, columnsRef, verticalScrollPosition, virtualHorizontalScrollbarRef, paginationContainerRef, totalWidthOfAllColumns, { isLazyLoad, pagination, stickyPagination, isDataGridTemplate, } ); /* == Empty State == */ const onEmptyStateClearResetButtonClick = () => { if (filterBarRef.value.isClearAllVisible) { filterBarRef.value.clearAllFilters(); } else if (filterBarRef.value.isResetAllVisible) { filterBarRef.value.resetAllFilters(); } }; const limitedEmptyStateProps = computed<BentoDatagridEmptyStateProps>(() => ({ ...props.emptyState, variant: props.emptyState?.variant === 'embedded' || props.emptyState?.variant === 'basic' ? props.emptyState.variant : 'basic', })); /* == Styles == */ // Conditional grid classes const conditionalGridClasses = computed(() => ({ [`b-data-grid--${BentoDatagridStyleState.OUTLINE}`]: props.outline, [`b-data-grid--${BentoDatagridStyleState.SCROLL}`]: props.fitContent || isLazyLoad.value, [`b-data-grid--${BentoDatagridStyleState.STICKY_FILTERS}`]: props.filters && props.stickyFilters, [`b-data-grid--${BentoDatagridStyleState.OVERFLOW_SHADOW_LEFT}`]: hasLeftOverflow.value && !frozenColumnsLength.value, [`b-data-grid--${BentoDatagridStyleState.OVERFLOW_SHADOW_RIGHT}`]: hasRightOverflow.value && props.rowActions === undefined, [`b-data-grid--${BentoDatagridStyleState.NO_SPACING_TOP}`]: props.noSpacingTop, [`b-data-grid--${BentoDatagridStyleState.FIT_CONTENT}`]: props.fitContent, [`b-data-grid--${BentoDatagridStyleState.ZEBRA_ROWS}`]: internalZebraRows.value, 'b-data-grid--lazy-load': isLazyLoad.value, })); // Conditional grid body classes const conditionalGridBodyClasses = computed(() => ({ 'b-data-grid__body--sticky-horizontal-scrollbar': props.stickyHorizontalScrollbar, })); const computedColumnPanelAlignment = computed(() => columnSets.value.findIndex(el => el.columnSet.length)); const showFrozenColumnsSeparator = computed(() => { const isSelectionOnlyFrozenColumn = (props.selectable || props.hasNestedContent || props.hasNestedRows) && frozenColumnsLength.value === 1; return !isSelectionOnlyFrozenColumn || hasLeftOverflow.value; }); // Conditional filter classes const conditionalFilterClasses = computed(() => ({ 'b-data-grid__filters--sticky': !!props.stickyFilters || isDataGridTemplate, })); // Conditional pagination classes const conditionalPaginationClasses = computed(() => ({ 'b-data-grid__pagination--padding-left-unset': props.columns?.[0]?.padding?.left === false, 'b-data-grid__pagination--padding-right-unset': props.columns?.[props.columns.length - 1]?.padding?.right === false, 'b-data-grid__pagination--sticky': !!props.stickyPagination || isDataGridTemplate, })); /* == Ref registration methods == */ const registerColumnHeaderRefs = (el: Element | ComponentPublicInstance) => { // We always know this will be used on a DataGridColumns component so we can cast it const datagridColumnsComponent = el as InstanceType<typeof DataGridColumns>; if (el !== null && datagridColumnsComponent?.columnHeaderRefs) { Object.entries(datagridColumnsComponent?.columnHeaderRefs).forEach(([fieldName, colHeaderEl]) => { columnHeaderRefs.value[fieldName] = colHeaderEl as HTMLDivElement; }); } }; /* == Row methods == */ // Row selecting const onRowClick = (row: BentoDatagridDataItem, event: MouseEvent) => { if (props.allowRowClicks) { emit(BentoDatagridEvent.ROW_CLICK, row, event); } }; const onNestedContentToggled = (rowData: BentoDatagridDataItem) => { emit(BentoDatagridEvent.NESTED_CONTENT_TOGGLED, rowData); nextTick(() => rebuildGridcellRefs()); }; // Programmatically set the hover state on a row const onCellHover = (rowIndex: number) => { hoveredRowIndex.value = rowIndex; }; /* == a11y == */ const ariaGridRole = computed(() => (props.hasNestedContent || props.hasNestedRows ? 'treegrid' : 'grid')); /* == Action bar == */ const itemsSelectedCounter = computed(() => selectedItems.value?.length ?? 0); const showActionBar = computed(() => props.bulkActions && itemsSelectedCounter.value > 0); /* == Emits & Events == */ // Resizing const onColumnResized = (resizeEvent: { field: string; width: number }) => emit(BentoDatagridEvent.COLUMN_RESIZE, resizeEvent); // Sorting const onSort = (updatedSortBy: Array<BentoDatagridSortByColumn>) => { emit(BentoDatagridEvent.UPDATE_SORT_BY, updatedSortBy); emit(BentoDatagridEvent.SORT, updatedSortBy); }; // Pagination const onPaginationNavigate = (page: number, size: number) => { emit(BentoDatagridEvent.NAVIGATE, page); emit(BentoDatagridEvent.UPDATE_PAGINATION, { ...props.pagination, size, page, }); }; const onItemsPerPageChange = (itemsPerPage: number) => { emit(BentoDatagridEvent.ITEMS_PAGE, itemsPerPage); }; // Filter bar const onSearchTermCleared = () => { emit(BentoDatagridEvent.FILTER_SEARCH_CLEAR); }; const onSearchTermInput = newSearchTerm => { emit(BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM, newSearchTerm); emit(BentoDatagridEvent.FILTER_SEARCH_INPUT, newSearchTerm); }; /* == Warnings == */ const hasMandatoryColumn = computed(() => columns.value.some(column => column.mandatory)); if (!hasMandatoryColumn.value) { printDevelopmentWarning( 'WARNING: At least one BentoDataGrid column should be set as mandatory to ensure data is always displayed correctly.' ); } /* == Provide/inject == */ const setTitle: BentoTabProvidedTitle = inject(EXTERNAL_TITLE_REGISTER_ENTRY_INJECTION_KEY, () => null); setTitle(props.tabTitle); provide(DATA_GRID_SLASHED_ZERO, toRef(props, 'slashedZero')); /* == Expose methods == */ defineExpose({ onEmptyStateClearResetButtonClick }); </script> <script lang="ts"> /** * A component to display data in columns and row format with a whole array of configuration for visualization and interaction. * * @example * import { BentoDataGrid } from '@adyen/bento-vue2'; * * export default { * components: { BentoDataGrid }, * template: ` * <bento-data-grid * :data="[ * { id: '123', name: 'John', company: 'TestCompany', 'payment': 200 } * { id: '321', name: 'Jane', company: 'Adyen', 'payment': 300 } * ]" * :columns="[ * { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, * { field: 'company', label: 'Reference' }, * { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, * ]" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-data-grid', }; </script> <style lang="scss" scoped src="./data-grid.scss" />
@@ -1 +1 @@
1
- <template> <div class="b-date"> <bento-typography v-if="!disableTypography" class="b-date__label" el="span" variant="body" strong> {{ formattedDate }} </bento-typography> <template v-else>{{ formattedDate }}</template> </div> </template> <script setup lang="ts"> import { computed } from 'vue'; import { BentoDateFormat, type BentoDateProps } from './date.types'; import { useI18n } from '@/utils/ts/i18n'; import { BentoTypography } from '@/components/typography'; const props = withDefaults(defineProps<BentoDateProps>(), { date: null, format: BentoDateFormat.FULL_DATE_EXACT_TIME_WITHOUT_PERIOD, disableTypography: false, }); const { locale } = useI18n(undefined, true); /** * This is a reverse mapping of the BentoDateFormat enum. * It allows us to get the key of the enum by its value, which is then used to look up the * corresponding formatting options in `BentoDateFormatPresets`. */ const bentoDateFormatKeyValuePairs = Object.entries(BentoDateFormat).reduce< Record<BentoDateFormat, keyof typeof BentoDateFormat> >( (acc, [key, value]) => { acc[value.toString()] = key; return acc; }, {} as Record<BentoDateFormat, keyof typeof BentoDateFormat> ); const formattedDate = computed(() => { // If null, return empty string if (!props.date) { return ''; } const datePropAsDateObject = typeof props.date === 'string' ? new Date(props.date) : props.date; const formatKeyEntry = bentoDateFormatKeyValuePairs?.[props.format]; let dateFormat: Intl.DateTimeFormatOptions | undefined = undefined; // Translate the key of the enum to the Intl.DateTimeFormatOptions configuration object if (formatKeyEntry && BentoDateFormatPresets?.[formatKeyEntry]) { dateFormat = BentoDateFormatPresets?.[formatKeyEntry]; } return new Intl.DateTimeFormat(locale.value, dateFormat).format(datePropAsDateObject); }); </script> <script lang="ts"> /** * The format presets to use when being supplied with a key name in BentoDateFormat. * They translate to configuration options that are used in the Intl.DateTimeFormat function * * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat#using_options */ const BentoDateFormatPresets: { [key in keyof typeof BentoDateFormat]: Intl.DateTimeFormatOptions } = { SHORT_MONTH: { month: 'short', }, DAY_OF_MONTH: { day: 'numeric', }, LONG_WEEKDAY: { weekday: 'long', }, SHORT_TIME_WITH_PERIOD: { hour: 'numeric', minute: 'numeric', hour12: true, }, SHORT_TIME_WITHOUT_PERIOD: { hour: 'numeric', minute: 'numeric', hour12: false, }, EXACT_TIME_WITH_PERIOD: { hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: true, }, EXACT_TIME_WITHOUT_PERIOD: { hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: false, }, LONG_DATE: { year: 'numeric', month: 'long', day: 'numeric', }, SHORT_DATE: { year: 'numeric', month: 'short', day: 'numeric', }, FULL_DATE_TIME_WITH_PERIOD: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: true, }, FULL_DATE_TIME_WITHOUT_PERIOD: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: false, }, FULL_DATE_EXACT_TIME_WITH_PERIOD: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: true, }, FULL_DATE_EXACT_TIME_WITHOUT_PERIOD: { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }, FULL_DATE_TIME_WITH_TIMEZONE_AND_PERIOD: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: true, timeZoneName: 'short', }, FULL_DATE_TIME_WITH_TIMEZONE_AND_WITHOUT_PERIOD: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: false, timeZoneName: 'short', }, FULL_DATE_EXACT_TIME_WITH_TIMEZONE_AND_PERIOD: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: true, timeZoneName: 'short', }, FULL_DATE_EXACT_TIME_WITH_TIMEZONE_AND_WITHOUT_PERIOD: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: false, timeZoneName: 'short', }, }; /** * A Date component is used to display a formatted date value. * * @example * import { BentoDate } from '@adyen/bento-vue2'; * * export default { * components: { BentoDate }, * template: ` * <bento-date :date="dateObject" :format="BentoDateFormat.LONG_DATE"/> * ` * } */ export default { name: 'bento-date', }; </script> <style lang="scss" scoped src="./date.scss" />
1
+ <template> <div class="b-date"> <bento-typography v-if="!disableTypography" class="b-date__label" el="span" variant="body" strong> <time :datetime="dateTime">{{ formattedDate }}</time> </bento-typography> <time v-else :datetime="dateTime">{{ formattedDate }}</time> </div> </template> <script setup lang="ts"> import { computed } from 'vue'; import { BentoDateFormat, type BentoDateProps } from './date.types'; import { useI18n } from '@/utils/ts/i18n'; import { BentoTypography } from '@/components/typography'; const props = withDefaults(defineProps<BentoDateProps>(), { date: null, format: BentoDateFormat.FULL_DATE_EXACT_TIME_WITHOUT_PERIOD, disableTypography: false, }); const { locale } = useI18n(undefined, true); /** * This is a reverse mapping of the BentoDateFormat enum. * It allows us to get the key of the enum by its value, which is then used to look up the * corresponding formatting options in `BentoDateFormatPresets`. */ const bentoDateFormatKeyValuePairs = Object.entries(BentoDateFormat).reduce< Record<BentoDateFormat, keyof typeof BentoDateFormat> >( (acc, [key, value]) => { acc[value.toString()] = key; return acc; }, {} as Record<BentoDateFormat, keyof typeof BentoDateFormat> ); const datePropAsDateObject = computed(() => { if (!props.date) { return null; } return props.date instanceof Date ? props.date : new Date(props.date); }); const dateTime = computed(() => datePropAsDateObject.value && !Number.isNaN(datePropAsDateObject.value.getTime()) ? datePropAsDateObject.value.toISOString() : undefined ); const formattedDate = computed(() => { if (!datePropAsDateObject.value || Number.isNaN(datePropAsDateObject.value.getTime())) { return ''; } const formatKeyEntry = bentoDateFormatKeyValuePairs?.[props.format]; let dateFormat: Intl.DateTimeFormatOptions | undefined = undefined; // Translate the key of the enum to the Intl.DateTimeFormatOptions configuration object if (formatKeyEntry && BentoDateFormatPresets?.[formatKeyEntry]) { dateFormat = BentoDateFormatPresets?.[formatKeyEntry]; } return new Intl.DateTimeFormat(locale.value, dateFormat).format(datePropAsDateObject.value); }); </script> <script lang="ts"> /** * The format presets to use when being supplied with a key name in BentoDateFormat. * They translate to configuration options that are used in the Intl.DateTimeFormat function * * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat#using_options */ const BentoDateFormatPresets: { [key in keyof typeof BentoDateFormat]: Intl.DateTimeFormatOptions } = { SHORT_MONTH: { month: 'short', }, DAY_OF_MONTH: { day: 'numeric', }, LONG_WEEKDAY: { weekday: 'long', }, SHORT_TIME_WITH_PERIOD: { hour: 'numeric', minute: 'numeric', hour12: true, }, SHORT_TIME_WITHOUT_PERIOD: { hour: 'numeric', minute: 'numeric', hour12: false, }, EXACT_TIME_WITH_PERIOD: { hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: true, }, EXACT_TIME_WITHOUT_PERIOD: { hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: false, }, LONG_DATE: { year: 'numeric', month: 'long', day: 'numeric', }, SHORT_DATE: { year: 'numeric', month: 'short', day: 'numeric', }, FULL_DATE_TIME_WITH_PERIOD: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: true, }, FULL_DATE_TIME_WITHOUT_PERIOD: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: false, }, FULL_DATE_EXACT_TIME_WITH_PERIOD: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: true, }, FULL_DATE_EXACT_TIME_WITHOUT_PERIOD: { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }, FULL_DATE_TIME_WITH_TIMEZONE_AND_PERIOD: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: true, timeZoneName: 'short', }, FULL_DATE_TIME_WITH_TIMEZONE_AND_WITHOUT_PERIOD: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: false, timeZoneName: 'short', }, FULL_DATE_EXACT_TIME_WITH_TIMEZONE_AND_PERIOD: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: true, timeZoneName: 'short', }, FULL_DATE_EXACT_TIME_WITH_TIMEZONE_AND_WITHOUT_PERIOD: { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric', hour12: false, timeZoneName: 'short', }, }; /** * A Date component is used to display a formatted date value. * * @example * import { BentoDate } from '@adyen/bento-vue2'; * * export default { * components: { BentoDate }, * template: ` * <bento-date :date="dateObject" :format="BentoDateFormat.LONG_DATE"/> * ` * } */ export default { name: 'bento-date', }; </script> <style lang="scss" scoped src="./date.scss" />
@@ -1 +1 @@
1
- import { type ComputedRef, type Ref } from 'vue'; export interface UseDateDisplayTextProps<T> { datePickerValue: Ref<T>; isDateIncorrect: Ref<boolean>; isMonthVariant?: Ref<boolean>; placeholder?: Ref<string>; } export type UseDateDisplayText<T> = (props: UseDateDisplayTextProps<T>) => { // Values dateDisplayValue: ComputedRef<string>; textInputValue: Ref<string>; // Methods formatTextInputDisplayValue: (dateToFormat?: Date) => void; };
1
+ import { type ComputedRef, type Ref } from 'vue'; export interface UseDateDisplayTextProps<T> { datePickerValue: Ref<T>; dateError: Ref<string | null>; isMonthVariant?: Ref<boolean>; placeholder?: Ref<string>; } export type UseDateDisplayText<T> = (props: UseDateDisplayTextProps<T>) => { // Values dateDisplayValue: ComputedRef<string>; textInputValue: Ref<string>; // Methods formatTextInputDisplayValue: (dateToFormat?: Date) => void; };
@@ -41,6 +41,12 @@ A calendar by month/year instead of day/month. Useful for full month selection,
41
41
 
42
42
  ## Modifiers
43
43
 
44
+ ### Year range
45
+
46
+ Allows defining a specific range of selectable years in the calendar header dropdown. The `yearRange` property accepts
47
+ an object with `minYear` and `maxYear`. If not provided, a default range of years relative to the current year is
48
+ displayed.
49
+
44
50
  ### Default display month
45
51
 
46
52
  This is used to show a default date when the calendar popup is opened and no date is currently selected, otherwise the
@@ -1 +1 @@
1
- import { type Meta, type StoryObj } from '@storybook/vue'; import { action } from '@storybook/addon-actions'; import { ref } from 'vue'; import { BentoDatePickerFirstDayOfWeek } from './date-picker.types'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { isVue2 } from 'vue-demi'; import { startOfMonth } from 'date-fns/startOfMonth'; import DatePickerDefaultExample from './__tests__/date-picker-default-example.vue?raw'; import BentoDatePicker from './date-picker.vue'; const meta: Meta = { title: 'Date picker/Single date', component: BentoDatePicker, argTypes: { // Funcs isDateDisabled: { name: 'isDateDisabled', defaultValue: '', description: 'Disables the dates that meet the criteria defined in the function.', table: { type: { summary: '(currentDate: Date) => boolean' } }, control: { disable: true }, }, min: { control: { type: 'date' } }, max: { control: { type: 'date' } }, defaultDisplayMonth: { defaultValue: '2024-05', control: { type: 'date' } }, value: { control: { type: 'text' }, table: { category: 'Deprecated' }, description: 'Deprecated for usage with properties since version 2.0. Use `v-model` or `model-value` property instead', }, // Enum props firstDayOfWeek: { options: Object.keys(BentoDatePickerFirstDayOfWeek) .map(key => BentoDatePickerFirstDayOfWeek[key]) .filter(value => typeof value !== 'string') as Array<number>, control: { type: 'select', }, }, // Events input: { control: { disable: true }, table: { category: 'Deprecated' }, description: 'Deprecated since version 2.0. Use `v-model` or `update:model-value` instead', }, }, }; export default meta; type Story = StoryObj<typeof BentoDatePicker>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDatePicker }, props: Object.keys(argTypes), template: ` <div style="max-width: 300px"> <bento-date-picker v-bind="args" :model-value="selectedDate" @update:model-value="onDateChanged" /> </div> `, setup(props) { const selectedDate = ref(props.modelValue); const onDateChanged = (newSelectedDate: Date) => { selectedDate.value = newSelectedDate; action('input')(selectedDate); }; return { // Values selectedDate, args: isVue2 ? props : _args, // Events onDateChanged, }; }, }), args: { description: 'Supporting text', label: 'Label', optional: true, required: true, isDateDisabled: (date: Date) => date.valueOf() > Date.now(), modelValue: new Date(), }, parameters: storybookDocsParameter(DatePickerDefaultExample), }; export const MonthVariant: Story = { render: (_args, { argTypes }) => ({ components: { BentoDatePicker }, props: Object.keys(argTypes), template: ` <div style="max-width: 300px"> <bento-date-picker v-bind="args" variant="month" :model-value="selectedDate" @update:model-value="onDateChanged" /> </div> `, setup(props) { const selectedDate = ref(props.modelValue); const onDateChanged = (newSelectedDate: Date) => { selectedDate.value = newSelectedDate; action('input')(selectedDate); }; return { // Values selectedDate, args: isVue2 ? props : _args, // Events onDateChanged, }; }, }), args: { description: 'Supporting text', label: 'Label', optional: true, required: true, isDateDisabled: (date: Date) => date.valueOf() > Date.now(), modelValue: startOfMonth(new Date()), }, parameters: storybookDocsParameter(` <bento-date-picker label="Label" description="Supporting text" isDateDisabled="(currentDate: Date) => boolean" required optional disabled variant="month" v-model="selectedDate" /> `), };
1
+ import { type Meta, type StoryObj } from '@storybook/vue'; import { action } from '@storybook/addon-actions'; import { ref } from 'vue'; import { BentoDatePickerFirstDayOfWeek, type BentoDatePickerProps } from './date-picker.types'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { isVue2 } from 'vue-demi'; import { startOfMonth } from 'date-fns/startOfMonth'; import DatePickerDefaultExample from './__tests__/date-picker-default-example.vue?raw'; import BentoDatePicker from './date-picker.vue'; const meta: Meta = { title: 'Date picker/Single date', component: BentoDatePicker, argTypes: { // Funcs isDateDisabled: { name: 'isDateDisabled', defaultValue: '', description: 'Disables the dates that meet the criteria defined in the function.', table: { type: { summary: '(currentDate: Date) => boolean' } }, control: { disable: true }, }, min: { control: { type: 'date' } }, max: { control: { type: 'date' } }, defaultDisplayMonth: { defaultValue: '2024-05', control: { type: 'date' } }, value: { control: { type: 'text' }, table: { category: 'Deprecated' }, description: 'Deprecated for usage with properties since version 2.0. Use `v-model` or `model-value` property instead', }, // Enum props firstDayOfWeek: { options: Object.keys(BentoDatePickerFirstDayOfWeek) .map(key => BentoDatePickerFirstDayOfWeek[key]) .filter(value => typeof value !== 'string') as Array<number>, control: { type: 'select', }, }, // Events input: { control: { disable: true }, table: { category: 'Deprecated' }, description: 'Deprecated since version 2.0. Use `v-model` or `update:model-value` instead', }, }, }; export default meta; type Story = StoryObj<typeof BentoDatePicker>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDatePicker }, props: Object.keys(argTypes), template: ` <div style="max-width: 300px"> <bento-date-picker v-bind="args" :model-value="selectedDate" @update:model-value="onDateChanged" /> </div> `, setup(props) { const selectedDate = ref(props.modelValue); const onDateChanged = (newSelectedDate: Date) => { selectedDate.value = newSelectedDate; action('input')(selectedDate); }; return { // Values selectedDate, args: isVue2 ? props : _args, // Events onDateChanged, }; }, }), args: { yearRange: { minYear: 1970, maxYear: new Date().getFullYear() }, description: 'Supporting text', label: 'Label', optional: true, required: true, isDateDisabled: (date: Date) => date.valueOf() > Date.now(), modelValue: new Date(), } as BentoDatePickerProps, parameters: storybookDocsParameter(DatePickerDefaultExample), }; export const MonthVariant: Story = { render: (_args, { argTypes }) => ({ components: { BentoDatePicker }, props: Object.keys(argTypes), template: ` <div style="max-width: 300px"> <bento-date-picker v-bind="args" variant="month" :model-value="selectedDate" @update:model-value="onDateChanged" /> </div> `, setup(props) { const selectedDate = ref(props.modelValue); const onDateChanged = (newSelectedDate: Date) => { selectedDate.value = newSelectedDate; action('input')(selectedDate); }; return { // Values selectedDate, args: isVue2 ? props : _args, // Events onDateChanged, }; }, }), args: { yearRange: { minYear: undefined, maxYear: undefined }, description: 'Supporting text', label: 'Label', optional: true, required: true, isDateDisabled: (date: Date) => date.valueOf() > Date.now(), modelValue: startOfMonth(new Date()), }, parameters: storybookDocsParameter(` <bento-date-picker label="Label" description="Supporting text" isDateDisabled="(currentDate: Date) => boolean" required optional disabled variant="month" v-model="selectedDate" /> `), };
@@ -1 +1 @@
1
- import type { FormField, FormFieldEmits } from '../../types/form-field'; import { CalendarFirstDayOfWeek as BentoDatePickerFirstDayOfWeek, type CalendarIsDateDisabled as BentoDatePickerIsDateDisabled, type CalendarSingleDateValue as BentoDatePickerValue, CalendarVariant as BentoDatePickerVariant, } from '@/internal'; export { BentoDatePickerFirstDayOfWeek, type BentoDatePickerIsDateDisabled, type BentoDatePickerValue, BentoDatePickerVariant, }; /** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoDatePickerEvent { INPUT = 'input', } export type BentoDatePickerEmits = FormFieldEmits<BentoDatePickerValue>; export interface BentoDatePickerProps extends FormField { /** * Default start date for the calendar pane. * Expected format: YYYY-MM-DD * @default null */ defaultDisplayMonth?: string | null; /** * Allows you to set the first day of the week. 0 is sunday, 1 is monday, 6 is saturday * @default undefined */ firstDayOfWeek?: BentoDatePickerFirstDayOfWeek; /** * Indicate if a date should be disabled or not */ isDateDisabled?: BentoDatePickerIsDateDisabled; /** * Sets a minimum limit for selectable dates - dates before `min` will not be selectable * @default null */ min?: Date | null; /** * Sets a maximum limit for selectable dates - dates after `max` will not be selectable * @default null */ max?: Date | null; /** * Selected date * * @default null */ modelValue?: BentoDatePickerValue | null; /** * Selected date * * @deprecated Since v2.0.0. Use `v-model` or `model-value` instead. * * @default null */ value?: BentoDatePickerValue | null; /** * The type of calendar to display. Defaults to showing days. */ variant?: `${BentoDatePickerVariant}`; }
1
+ import type { FormField, FormFieldEmits } from '../../types/form-field'; import { CalendarFirstDayOfWeek as BentoDatePickerFirstDayOfWeek, type CalendarIsDateDisabled as BentoDatePickerIsDateDisabled, type CalendarSingleDateValue as BentoDatePickerValue, CalendarVariant as BentoDatePickerVariant, type CalendarYearRange, } from '@/internal'; export { BentoDatePickerFirstDayOfWeek, type BentoDatePickerIsDateDisabled, type BentoDatePickerValue, BentoDatePickerVariant, }; /** * @deprecated Since v2.0.0. Use string literal instead. */ export enum BentoDatePickerEvent { INPUT = 'input', } export type BentoDatePickerEmits = FormFieldEmits<BentoDatePickerValue>; export type BentoDatePickerYearRange = CalendarYearRange; export interface BentoDatePickerProps extends FormField { /** * Default start date for the calendar pane. * Expected format: YYYY-MM-DD * @default null */ defaultDisplayMonth?: string | null; /** * Allows you to set the first day of the week. 0 is sunday, 1 is monday, 6 is saturday * @default undefined */ firstDayOfWeek?: BentoDatePickerFirstDayOfWeek; /** * Indicate if a date should be disabled or not */ isDateDisabled?: BentoDatePickerIsDateDisabled; /** * Sets a minimum limit for selectable dates - dates before `min` will not be selectable * @default null */ min?: Date | null; /** * Sets a maximum limit for selectable dates - dates after `max` will not be selectable * @default null */ max?: Date | null; /** * Selected date * * @default null */ modelValue?: BentoDatePickerValue | null; /** * Selected date * * @deprecated Since v2.0.0. Use `v-model` or `model-value` instead. * * @default null */ value?: BentoDatePickerValue | null; /** * Range of years to be selectable from the year dropdown */ yearRange?: BentoDatePickerYearRange; /** * The type of calendar to display. Defaults to showing days. */ variant?: `${BentoDatePickerVariant}`; }