@adyen/bento-mcp 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (410) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +113 -0
  4. package/dist/assets/components/accordion/accordion.docs.mdx +77 -0
  5. package/dist/assets/components/accordion/accordion.stories.ts +1 -0
  6. package/dist/assets/components/accordion/accordion.vue +1 -0
  7. package/dist/assets/components/accordion/components/accordion-item.stories.ts +1 -0
  8. package/dist/assets/components/accordion/components/accordion-item.vue +1 -0
  9. package/dist/assets/components/action-bar/action-bar.docs.mdx +103 -0
  10. package/dist/assets/components/action-bar/action-bar.stories.ts +1 -0
  11. package/dist/assets/components/action-bar/action-bar.vue +1 -0
  12. package/dist/assets/components/alert/alert.docs.mdx +106 -0
  13. package/dist/assets/components/alert/alert.stories.ts +1 -0
  14. package/dist/assets/components/alert/alert.vue +1 -0
  15. package/dist/assets/components/avatar/avatar.docs.mdx +44 -0
  16. package/dist/assets/components/avatar/avatar.stories.ts +1 -0
  17. package/dist/assets/components/avatar/avatar.vue +1 -0
  18. package/dist/assets/components/avatar/components/avatar-image/avatar-image.vue +1 -0
  19. package/dist/assets/components/button/button.docs.mdx +137 -0
  20. package/dist/assets/components/button/button.stories.ts +1 -0
  21. package/dist/assets/components/button/button.vue +1 -0
  22. package/dist/assets/components/button/components/ai-button/ai-button.docs.mdx +40 -0
  23. package/dist/assets/components/button/components/ai-button/ai-button.stories.ts +1 -0
  24. package/dist/assets/components/button/components/ai-button/ai-button.vue +1 -0
  25. package/dist/assets/components/button/components/button-actions/button-actions.docs.mdx +169 -0
  26. package/dist/assets/components/button/components/button-actions/button-actions.stories.ts +1 -0
  27. package/dist/assets/components/button/components/button-actions/button-actions.vue +1 -0
  28. package/dist/assets/components/button/components/loading-button/loading-button.docs.mdx +106 -0
  29. package/dist/assets/components/button/components/loading-button/loading-button.stories.ts +1 -0
  30. package/dist/assets/components/button/components/loading-button/loading-button.vue +1 -0
  31. package/dist/assets/components/button/components/toggle-button/toggle-button.docs.mdx +81 -0
  32. package/dist/assets/components/button/components/toggle-button/toggle-button.stories.ts +1 -0
  33. package/dist/assets/components/button/components/toggle-button/toggle-button.vue +1 -0
  34. package/dist/assets/components/card/card.docs.mdx +166 -0
  35. package/dist/assets/components/card/card.stories.ts +1 -0
  36. package/dist/assets/components/card/card.vue +1 -0
  37. package/dist/assets/components/checkbox/checkbox.docs.mdx +116 -0
  38. package/dist/assets/components/checkbox/checkbox.stories.ts +1 -0
  39. package/dist/assets/components/checkbox/checkbox.vue +1 -0
  40. package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.docs.mdx +134 -0
  41. package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.stories.ts +1 -0
  42. package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.vue +1 -0
  43. package/dist/assets/components/chip/chip.docs.mdx +51 -0
  44. package/dist/assets/components/chip/chip.stories.ts +1 -0
  45. package/dist/assets/components/chip/chip.vue +1 -0
  46. package/dist/assets/components/click-outside/click-outside.docs.mdx +30 -0
  47. package/dist/assets/components/click-outside/click-outside.stories.ts +1 -0
  48. package/dist/assets/components/code-snippet/code-snippet.docs.mdx +73 -0
  49. package/dist/assets/components/code-snippet/code-snippet.stories.ts +1 -0
  50. package/dist/assets/components/code-snippet/code-snippet.vue +1 -0
  51. package/dist/assets/components/country/country.docs.mdx +61 -0
  52. package/dist/assets/components/country/country.stories.ts +1 -0
  53. package/dist/assets/components/country/country.vue +1 -0
  54. package/dist/assets/components/currency/currency.docs.mdx +38 -0
  55. package/dist/assets/components/currency/currency.stories.ts +1 -0
  56. package/dist/assets/components/currency/currency.vue +1 -0
  57. package/dist/assets/components/dashed-underline/dashed-underline.docs.mdx +57 -0
  58. package/dist/assets/components/dashed-underline/dashed-underline.stories.ts +1 -0
  59. package/dist/assets/components/dashed-underline/dashed-underline.vue +1 -0
  60. package/dist/assets/components/data-grid/components/data-grid-cell/data-grid-cell.vue +1 -0
  61. package/dist/assets/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.vue +1 -0
  62. package/dist/assets/components/data-grid/components/data-grid-columns/data-grid-columns.vue +1 -0
  63. package/dist/assets/components/data-grid/components/data-grid-config-settings/components/data-grid-config-settings-column-row/data-grid-config-settings-column-row.vue +1 -0
  64. package/dist/assets/components/data-grid/components/data-grid-config-settings/data-grid-config-settings.vue +1 -0
  65. package/dist/assets/components/data-grid/components/data-grid-lazy-load/data-grid-lazy-load.vue +1 -0
  66. package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.vue +1 -0
  67. package/dist/assets/components/data-grid/components/data-grid-static-icon/data-grid-static-icon.docs.mdx +61 -0
  68. package/dist/assets/components/data-grid/components/data-grid-static-icon/data-grid-static-icon.stories.ts +1 -0
  69. package/dist/assets/components/data-grid/components/data-grid-static-icon/data-grid-static-icon.vue +1 -0
  70. package/dist/assets/components/data-grid/components/data-grid-summary-cell/data-grid-summary-cell.docs.mdx +56 -0
  71. package/dist/assets/components/data-grid/components/data-grid-summary-cell/data-grid-summary-cell.stories.ts +1 -0
  72. package/dist/assets/components/data-grid/components/data-grid-summary-cell/data-grid-summary-cell.vue +1 -0
  73. package/dist/assets/components/data-grid/components/data-grid-trend-cell/data-grid-trend-cell.docs.mdx +48 -0
  74. package/dist/assets/components/data-grid/components/data-grid-trend-cell/data-grid-trend-cell.stories.ts +1 -0
  75. package/dist/assets/components/data-grid/components/data-grid-trend-cell/data-grid-trend-cell.vue +1 -0
  76. package/dist/assets/components/data-grid/data-grid.docs.mdx +951 -0
  77. package/dist/assets/components/data-grid/data-grid.stories.ts +1 -0
  78. package/dist/assets/components/data-grid/data-grid.vue +1 -0
  79. package/dist/assets/components/date/date.docs.mdx +49 -0
  80. package/dist/assets/components/date/date.stories.ts +1 -0
  81. package/dist/assets/components/date/date.vue +1 -0
  82. package/dist/assets/components/date-picker/date-picker.docs.mdx +69 -0
  83. package/dist/assets/components/date-picker/date-picker.stories.ts +1 -0
  84. package/dist/assets/components/date-picker/date-picker.vue +1 -0
  85. package/dist/assets/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.vue +1 -0
  86. package/dist/assets/components/date-range-picker/date-range-picker.docs.mdx +168 -0
  87. package/dist/assets/components/date-range-picker/date-range-picker.stories.ts +1 -0
  88. package/dist/assets/components/date-range-picker/date-range-picker.vue +1 -0
  89. package/dist/assets/components/divider/divider.docs.mdx +27 -0
  90. package/dist/assets/components/divider/divider.stories.ts +1 -0
  91. package/dist/assets/components/divider/divider.vue +1 -0
  92. package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue +1 -0
  93. package/dist/assets/components/dropdown/components/dropdown-default-textbox/dropdown-default-textbox.vue +1 -0
  94. package/dist/assets/components/dropdown/components/dropdown-options-container/dropdown-options-container.vue +1 -0
  95. package/dist/assets/components/dropdown/components/dropdown-small-textbox/dropdown-small-textbox.vue +1 -0
  96. package/dist/assets/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.stories.ts +1 -0
  97. package/dist/assets/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.vue +1 -0
  98. package/dist/assets/components/dropdown/components/variants/dropdown-country/dropdown-country.stories.ts +1 -0
  99. package/dist/assets/components/dropdown/components/variants/dropdown-country/dropdown-country.vue +1 -0
  100. package/dist/assets/components/dropdown/components/variants/dropdown-currency/dropdown-currency.stories.ts +1 -0
  101. package/dist/assets/components/dropdown/components/variants/dropdown-currency/dropdown-currency.vue +1 -0
  102. package/dist/assets/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.stories.ts +1 -0
  103. package/dist/assets/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.vue +1 -0
  104. package/dist/assets/components/dropdown/components/variants/dropdown-tag/dropdown-tag.stories.ts +1 -0
  105. package/dist/assets/components/dropdown/components/variants/dropdown-tag/dropdown-tag.vue +1 -0
  106. package/dist/assets/components/dropdown/dropdown.docs.mdx +420 -0
  107. package/dist/assets/components/dropdown/dropdown.stories.ts +1 -0
  108. package/dist/assets/components/dropdown/dropdown.vue +1 -0
  109. package/dist/assets/components/empty-state/empty-state.docs.mdx +115 -0
  110. package/dist/assets/components/empty-state/empty-state.stories.ts +1 -0
  111. package/dist/assets/components/empty-state/empty-state.vue +1 -0
  112. package/dist/assets/components/file-uploader/components/file-uploader-file-card/components/file-uploader-file-card-animation/file-uploader-file-card-animation.vue +1 -0
  113. package/dist/assets/components/file-uploader/components/file-uploader-file-card/file-uploader-file-card.stories.ts +1 -0
  114. package/dist/assets/components/file-uploader/components/file-uploader-file-card/file-uploader-file-card.vue +1 -0
  115. package/dist/assets/components/file-uploader/components/file-uploader-restrictions/file-uploader-restrictions.stories.ts +1 -0
  116. package/dist/assets/components/file-uploader/components/file-uploader-restrictions/file-uploader-restrictions.vue +1 -0
  117. package/dist/assets/components/file-uploader/file-uploader.docs.mdx +117 -0
  118. package/dist/assets/components/file-uploader/file-uploader.stories.ts +1 -0
  119. package/dist/assets/components/file-uploader/file-uploader.vue +1 -0
  120. package/dist/assets/components/filter-bar/components/all-filters-modal/all-filters-modal.stories.ts +1 -0
  121. package/dist/assets/components/filter-bar/components/all-filters-modal/all-filters-modal.vue +1 -0
  122. package/dist/assets/components/filter-bar/components/all-filters-modal/components/all-filters-modal-button/all-filters-modal-button.vue +1 -0
  123. package/dist/assets/components/filter-bar/components/base-filter/base-filter.docs.mdx +90 -0
  124. package/dist/assets/components/filter-bar/components/base-filter/base-filter.stories.ts +1 -0
  125. package/dist/assets/components/filter-bar/components/base-filter/base-filter.vue +1 -0
  126. package/dist/assets/components/filter-bar/components/base-filter-button/base-filter-button.vue +1 -0
  127. package/dist/assets/components/filter-bar/components/boolean-filter/boolean-filter.vue +1 -0
  128. package/dist/assets/components/filter-bar/components/checkbox-group-filter/checkbox-group-filter.stories.ts +1 -0
  129. package/dist/assets/components/filter-bar/components/checkbox-group-filter/checkbox-group-filter.vue +1 -0
  130. package/dist/assets/components/filter-bar/components/date-filter/date-filter.stories.ts +1 -0
  131. package/dist/assets/components/filter-bar/components/date-filter/date-filter.vue +1 -0
  132. package/dist/assets/components/filter-bar/components/date-range-filter/date-range-filter.stories.ts +1 -0
  133. package/dist/assets/components/filter-bar/components/date-range-filter/date-range-filter.vue +1 -0
  134. package/dist/assets/components/filter-bar/components/filter-bar-button/filter-bar-button.vue +1 -0
  135. package/dist/assets/components/filter-bar/components/filter-bar-button-group/filter-bar-button-group.vue +1 -0
  136. package/dist/assets/components/filter-bar/components/input-filter/input-filter.docs.mdx +18 -0
  137. package/dist/assets/components/filter-bar/components/input-filter/input-filter.stories.ts +1 -0
  138. package/dist/assets/components/filter-bar/components/input-filter/input-filter.vue +1 -0
  139. package/dist/assets/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.docs.mdx +18 -0
  140. package/dist/assets/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.stories.ts +1 -0
  141. package/dist/assets/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.vue +1 -0
  142. package/dist/assets/components/filter-bar/components/radio-group-filter/radio-group-filter.stories.ts +1 -0
  143. package/dist/assets/components/filter-bar/components/radio-group-filter/radio-group-filter.vue +1 -0
  144. package/dist/assets/components/filter-bar/components/range-filter/range-filter.vue +1 -0
  145. package/dist/assets/components/filter-bar/components/select-filter/components/select-filter-button/select-filter-button.vue +1 -0
  146. package/dist/assets/components/filter-bar/components/select-filter/components/select-filter-dropdown/select-filter-dropdown.vue +1 -0
  147. package/dist/assets/components/filter-bar/components/select-filter/select-filter.stories.ts +1 -0
  148. package/dist/assets/components/filter-bar/components/select-filter/select-filter.vue +1 -0
  149. package/dist/assets/components/filter-bar/components/select-filter/variants/select-avatar-filter/select-avatar-filter.stories.ts +1 -0
  150. package/dist/assets/components/filter-bar/components/select-filter/variants/select-avatar-filter/select-avatar-filter.vue +1 -0
  151. package/dist/assets/components/filter-bar/components/select-filter/variants/select-country-filter/select-country-filter.stories.ts +1 -0
  152. package/dist/assets/components/filter-bar/components/select-filter/variants/select-country-filter/select-country-filter.vue +1 -0
  153. package/dist/assets/components/filter-bar/components/select-filter/variants/select-currency-filter/select-currency-filter.stories.ts +1 -0
  154. package/dist/assets/components/filter-bar/components/select-filter/variants/select-currency-filter/select-currency-filter.vue +1 -0
  155. package/dist/assets/components/filter-bar/components/select-filter/variants/select-payment-method-filter/select-payment-method-filter.stories.ts +1 -0
  156. package/dist/assets/components/filter-bar/components/select-filter/variants/select-payment-method-filter/select-payment-method-filter.vue +1 -0
  157. package/dist/assets/components/filter-bar/components/select-filter/variants/select-tag-filter/select-tag-filter.stories.ts +1 -0
  158. package/dist/assets/components/filter-bar/components/select-filter/variants/select-tag-filter/select-tag-filter.vue +1 -0
  159. package/dist/assets/components/filter-bar/components/toggle-filter/toggle-filter.stories.ts +1 -0
  160. package/dist/assets/components/filter-bar/components/toggle-filter/toggle-filter.vue +1 -0
  161. package/dist/assets/components/filter-bar/filter-bar.docs.mdx +1005 -0
  162. package/dist/assets/components/filter-bar/filter-bar.stories.ts +1 -0
  163. package/dist/assets/components/filter-bar/filter-bar.vue +1 -0
  164. package/dist/assets/components/focus-trap/focus-trap.docs.mdx +10 -0
  165. package/dist/assets/components/focus-trap/focus-trap.stories.ts +1 -0
  166. package/dist/assets/components/focus-trap/focus-trap.vue +1 -0
  167. package/dist/assets/components/form-layout/components/form-layout-field-tip/form-layout-field-tip.vue +1 -0
  168. package/dist/assets/components/form-layout/components/form-layout-group/form-layout-group.vue +1 -0
  169. package/dist/assets/components/form-layout/components/form-layout-title/form-layout-title.vue +1 -0
  170. package/dist/assets/components/form-layout/form-layout.docs.mdx +452 -0
  171. package/dist/assets/components/form-layout/form-layout.stories.ts +1 -0
  172. package/dist/assets/components/form-layout/form-layout.vue +1 -0
  173. package/dist/assets/components/header/header.docs.mdx +85 -0
  174. package/dist/assets/components/header/header.stories.ts +1 -0
  175. package/dist/assets/components/header/header.vue +1 -0
  176. package/dist/assets/components/header-with-views/components/header-with-views-all-views/header-with-views-all-views.vue +1 -0
  177. package/dist/assets/components/header-with-views/components/header-with-views-item-edit-form/header-with-views-edit-form.vue +1 -0
  178. package/dist/assets/components/header-with-views/header-with-views.docs.mdx +103 -0
  179. package/dist/assets/components/header-with-views/header-with-views.stories.ts +1 -0
  180. package/dist/assets/components/header-with-views/header-with-views.vue +1 -0
  181. package/dist/assets/components/image/image.docs.mdx +34 -0
  182. package/dist/assets/components/image/image.stories.ts +1 -0
  183. package/dist/assets/components/image/image.vue +1 -0
  184. package/dist/assets/components/info-icon/info-icon.docs.mdx +57 -0
  185. package/dist/assets/components/info-icon/info-icon.stories.ts +1 -0
  186. package/dist/assets/components/info-icon/info-icon.vue +1 -0
  187. package/dist/assets/components/input-field/input-field.docs.mdx +141 -0
  188. package/dist/assets/components/input-field/input-field.stories.ts +1 -0
  189. package/dist/assets/components/input-field/input-field.vue +1 -0
  190. package/dist/assets/components/input-field-password/input-field-password.docs.mdx +292 -0
  191. package/dist/assets/components/input-field-password/input-field-password.stories.ts +1 -0
  192. package/dist/assets/components/input-field-password/input-field-password.vue +1 -0
  193. package/dist/assets/components/input-field-phone-number/input-field-phone-number.docs.mdx +72 -0
  194. package/dist/assets/components/input-field-phone-number/input-field-phone-number.stories.ts +1 -0
  195. package/dist/assets/components/input-field-phone-number/input-field-phone-number.vue +1 -0
  196. package/dist/assets/components/inspector/inspector.docs.mdx +118 -0
  197. package/dist/assets/components/inspector/inspector.stories.ts +1 -0
  198. package/dist/assets/components/inspector/inspector.vue +1 -0
  199. package/dist/assets/components/internal/base-button/base-button.vue +1 -0
  200. package/dist/assets/components/internal/button-actions-with-menu/button-actions-with-menu.stories.ts +1 -0
  201. package/dist/assets/components/internal/button-actions-with-menu/button-actions-with-menu.vue +1 -0
  202. package/dist/assets/components/internal/calendar/calendar.stories.ts +1 -0
  203. package/dist/assets/components/internal/calendar/calendar.vue +1 -0
  204. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.stories.ts +1 -0
  205. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.vue +1 -0
  206. package/dist/assets/components/internal/calendar/components/calendar-month/components/calendar-month-day/__test__/calendar-month-day-stub.vue +1 -0
  207. package/dist/assets/components/internal/calendar/components/calendar-month/components/calendar-month-day/calendar-month-day.stories.ts +1 -0
  208. package/dist/assets/components/internal/calendar/components/calendar-month/components/calendar-month-day/calendar-month-day.vue +1 -0
  209. package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.vue +1 -0
  210. package/dist/assets/components/internal/calendar/components/calendar-year/components/calendar-year-month/__test__/calendar-year-month-stub.vue +1 -0
  211. package/dist/assets/components/internal/calendar/components/calendar-year/components/calendar-year-month/calendar-year-month.vue +1 -0
  212. package/dist/assets/components/internal/checkbox-input/checkbox-input.stories.ts +1 -0
  213. package/dist/assets/components/internal/checkbox-input/checkbox-input.vue +1 -0
  214. package/dist/assets/components/internal/controls-group/controls-group.vue +1 -0
  215. package/dist/assets/components/internal/copy/copy.docs.mdx +43 -0
  216. package/dist/assets/components/internal/copy/copy.stories.ts +1 -0
  217. package/dist/assets/components/internal/copy/copy.vue +1 -0
  218. package/dist/assets/components/internal/decorative-checkbox/decorative-checkbox.vue +1 -0
  219. package/dist/assets/components/internal/dialog-page/dialog-page.vue +1 -0
  220. package/dist/assets/components/internal/error-message/error-message.vue +1 -0
  221. package/dist/assets/components/internal/field-label/field-label.vue +1 -0
  222. package/dist/assets/components/internal/fixed-scroller/fixed-scroller.docs.mdx +21 -0
  223. package/dist/assets/components/internal/fixed-scroller/fixed-scroller.stories.ts +1 -0
  224. package/dist/assets/components/internal/fixed-scroller/fixed-scroller.vue +1 -0
  225. package/dist/assets/components/internal/footer-actions/footer-actions.vue +1 -0
  226. package/dist/assets/components/internal/grid-layout/grid-layout.vue +1 -0
  227. package/dist/assets/components/internal/header-meta/header-meta.vue +1 -0
  228. package/dist/assets/components/internal/info-icon-with-popover/info-icon-with-popover.vue +1 -0
  229. package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-category/listbox-multi-select-category.vue +1 -0
  230. package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/listbox-multi-select-options.vue +1 -0
  231. package/dist/assets/components/internal/listbox/components/listbox-multi-select/listbox-multi-select.vue +1 -0
  232. package/dist/assets/components/internal/listbox/components/listbox-option/listbox-option.vue +1 -0
  233. package/dist/assets/components/internal/listbox/components/listbox-option-tag/listbox-option-tag.vue +1 -0
  234. package/dist/assets/components/internal/listbox/components/listbox-single-select/components/listbox-single-select-category/listbox-single-select-category.vue +1 -0
  235. package/dist/assets/components/internal/listbox/components/listbox-single-select/listbox-single-select.vue +1 -0
  236. package/dist/assets/components/internal/listbox/components/listbox-single-select-option/listbox-single-select-option.vue +1 -0
  237. package/dist/assets/components/internal/listbox/listbox.stories.ts +1 -0
  238. package/dist/assets/components/internal/listbox/listbox.vue +1 -0
  239. package/dist/assets/components/internal/radio-input/radio-input.stories.ts +1 -0
  240. package/dist/assets/components/internal/radio-input/radio-input.vue +1 -0
  241. package/dist/assets/components/internal/teleport/teleport.vue +1 -0
  242. package/dist/assets/components/internal/tooltip/tooltip.test.vue +1 -0
  243. package/dist/assets/components/internal/tooltip/tooltip.vue +1 -0
  244. package/dist/assets/components/layout/components/layout-row/layout-row.stories.ts +1 -0
  245. package/dist/assets/components/layout/components/layout-row/layout-row.vue +1 -0
  246. package/dist/assets/components/layout/layout.docs.mdx +108 -0
  247. package/dist/assets/components/layout/layout.stories.ts +1 -0
  248. package/dist/assets/components/layout/layout.vue +1 -0
  249. package/dist/assets/components/link/link.docs.mdx +119 -0
  250. package/dist/assets/components/link/link.stories.ts +1 -0
  251. package/dist/assets/components/link/link.vue +1 -0
  252. package/dist/assets/components/loading-indicator/loading-indicator.docs.mdx +65 -0
  253. package/dist/assets/components/loading-indicator/loading-indicator.stories.ts +1 -0
  254. package/dist/assets/components/loading-indicator/loading-indicator.vue +1 -0
  255. package/dist/assets/components/menu/components/menu-item/menu-item.vue +1 -0
  256. package/dist/assets/components/menu/components/menu-item-list/menu-item-list.vue +1 -0
  257. package/dist/assets/components/menu/components/menu-list-container/menu-list-container.vue +1 -0
  258. package/dist/assets/components/menu/components/sub-menu-item-list/sub-menu-item-list.vue +1 -0
  259. package/dist/assets/components/menu/menu.docs.mdx +121 -0
  260. package/dist/assets/components/menu/menu.stories.ts +1 -0
  261. package/dist/assets/components/menu/menu.vue +1 -0
  262. package/dist/assets/components/modal/components/base-modal/base-modal.vue +1 -0
  263. package/dist/assets/components/modal/components/modal-default/modal-default.vue +1 -0
  264. package/dist/assets/components/modal/components/modal-dialog/modal-dialog.vue +1 -0
  265. package/dist/assets/components/modal/components/modal-page/modal-page.vue +1 -0
  266. package/dist/assets/components/modal/modal.docs.mdx +215 -0
  267. package/dist/assets/components/modal/modal.stories.ts +1 -0
  268. package/dist/assets/components/modal/modal.vue +1 -0
  269. package/dist/assets/components/modal-fullscreen/components/modal-fullscreen-page.vue +1 -0
  270. package/dist/assets/components/modal-fullscreen/modal-fullscreen.docs.mdx +217 -0
  271. package/dist/assets/components/modal-fullscreen/modal-fullscreen.stories.ts +1 -0
  272. package/dist/assets/components/modal-fullscreen/modal-fullscreen.vue +1 -0
  273. package/dist/assets/components/pagination/components/pagination-context/pagination-context.vue +1 -0
  274. package/dist/assets/components/pagination/components/pagination-controls/pagination-controls.vue +1 -0
  275. package/dist/assets/components/pagination/components/pagination-results-per-page/pagination-results-per-page.vue +1 -0
  276. package/dist/assets/components/pagination/pagination.docs.mdx +116 -0
  277. package/dist/assets/components/pagination/pagination.stories.ts +1 -0
  278. package/dist/assets/components/pagination/pagination.vue +1 -0
  279. package/dist/assets/components/payment-method/payment-method.docs.mdx +34 -0
  280. package/dist/assets/components/payment-method/payment-method.stories.ts +1 -0
  281. package/dist/assets/components/payment-method/payment-method.vue +1 -0
  282. package/dist/assets/components/popover/components/popover-dismiss-button/popover-dismiss-button.vue +1 -0
  283. package/dist/assets/components/popover/components/popover-image/popover-image.vue +1 -0
  284. package/dist/assets/components/popover/components/popover-title/popover-title.vue +1 -0
  285. package/dist/assets/components/popover/popover.docs.mdx +89 -0
  286. package/dist/assets/components/popover/popover.stories.ts +1 -0
  287. package/dist/assets/components/popover/popover.vue +1 -0
  288. package/dist/assets/components/popper-container/popper-container.docs.mdx +11 -0
  289. package/dist/assets/components/popper-container/popper-container.stories.ts +1 -0
  290. package/dist/assets/components/popper-container/popper-container.vue +1 -0
  291. package/dist/assets/components/promo-banner/promo-banner.docs.mdx +94 -0
  292. package/dist/assets/components/promo-banner/promo-banner.stories.ts +1 -0
  293. package/dist/assets/components/promo-banner/promo-banner.vue +1 -0
  294. package/dist/assets/components/radio-group/components/radio-button/radio-button.docs.mdx +76 -0
  295. package/dist/assets/components/radio-group/components/radio-button/radio-button.stories.ts +1 -0
  296. package/dist/assets/components/radio-group/components/radio-button/radio-button.vue +1 -0
  297. package/dist/assets/components/radio-group/radio-group.docs.mdx +154 -0
  298. package/dist/assets/components/radio-group/radio-group.stories.ts +1 -0
  299. package/dist/assets/components/radio-group/radio-group.vue +1 -0
  300. package/dist/assets/components/rich-text-editor/components/rich-text-editor-content-area/rich-text-editor-content-area.vue +1 -0
  301. package/dist/assets/components/rich-text-editor/components/rich-text-editor-renderer/components/rich-text-editor-code-block/rich-text-editor-code-block.vue +1 -0
  302. package/dist/assets/components/rich-text-editor/components/rich-text-editor-renderer/components/rich-text-editor-inline-code/rich-text-editor-inline-code.vue +1 -0
  303. package/dist/assets/components/rich-text-editor/components/rich-text-editor-renderer/components/rich-text-editor-link/rich-text-editor-link.vue +1 -0
  304. package/dist/assets/components/rich-text-editor/components/rich-text-editor-renderer/components/rich-text-editor-list-item/rich-text-editor-list-item.vue +1 -0
  305. package/dist/assets/components/rich-text-editor/components/rich-text-editor-renderer/components/rich-text-editor-table/rich-text-editor-table.vue +1 -0
  306. package/dist/assets/components/rich-text-editor/rich-text-editor.docs.mdx +52 -0
  307. package/dist/assets/components/rich-text-editor/rich-text-editor.stories.ts +1 -0
  308. package/dist/assets/components/rich-text-editor/rich-text-editor.vue +1 -0
  309. package/dist/assets/components/search-bar/search-bar.docs.mdx +117 -0
  310. package/dist/assets/components/search-bar/search-bar.stories.ts +1 -0
  311. package/dist/assets/components/search-bar/search-bar.vue +1 -0
  312. package/dist/assets/components/secondary-nav/components/secondary-nav-category/secondary-nav-category.vue +1 -0
  313. package/dist/assets/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue +1 -0
  314. package/dist/assets/components/secondary-nav/secondary-nav.docs.mdx +40 -0
  315. package/dist/assets/components/secondary-nav/secondary-nav.stories.ts +1 -0
  316. package/dist/assets/components/secondary-nav/secondary-nav.vue +1 -0
  317. package/dist/assets/components/segmented-control/components/segmented-control-button/segmented-control-button.vue +1 -0
  318. package/dist/assets/components/segmented-control/segmented-control.docs.mdx +90 -0
  319. package/dist/assets/components/segmented-control/segmented-control.stories.ts +1 -0
  320. package/dist/assets/components/segmented-control/segmented-control.vue +1 -0
  321. package/dist/assets/components/selection-card/components/selection-card-group/selection-card-group.docs.mdx +119 -0
  322. package/dist/assets/components/selection-card/components/selection-card-group/selection-card-group.stories.ts +1 -0
  323. package/dist/assets/components/selection-card/components/selection-card-group/selection-card-group.vue +1 -0
  324. package/dist/assets/components/selection-card/selection-card.docs.mdx +108 -0
  325. package/dist/assets/components/selection-card/selection-card.stories.ts +1 -0
  326. package/dist/assets/components/selection-card/selection-card.vue +1 -0
  327. package/dist/assets/components/sidepanel/components/sidepanel-page.stories.ts +1 -0
  328. package/dist/assets/components/sidepanel/components/sidepanel-page.vue +1 -0
  329. package/dist/assets/components/sidepanel/sidepanel.docs.mdx +129 -0
  330. package/dist/assets/components/sidepanel/sidepanel.stories.ts +1 -0
  331. package/dist/assets/components/sidepanel/sidepanel.vue +1 -0
  332. package/dist/assets/components/status/status.docs.mdx +62 -0
  333. package/dist/assets/components/status/status.stories.ts +1 -0
  334. package/dist/assets/components/status/status.vue +1 -0
  335. package/dist/assets/components/stepper/components/step/step.stories.ts +1 -0
  336. package/dist/assets/components/stepper/components/step/step.vue +1 -0
  337. package/dist/assets/components/stepper/stepper.docs.mdx +127 -0
  338. package/dist/assets/components/stepper/stepper.stories.ts +1 -0
  339. package/dist/assets/components/stepper/stepper.vue +1 -0
  340. package/dist/assets/components/structured-list/components/structered-list-item/structured-list-item.stories.ts +1 -0
  341. package/dist/assets/components/structured-list/components/structered-list-item/structured-list-item.vue +1 -0
  342. package/dist/assets/components/structured-list/structured-list.docs.mdx +68 -0
  343. package/dist/assets/components/structured-list/structured-list.stories.ts +1 -0
  344. package/dist/assets/components/structured-list/structured-list.vue +1 -0
  345. package/dist/assets/components/summary-grid/components/summary-grid-item/summary-grid-item.vue +1 -0
  346. package/dist/assets/components/summary-grid/components/summary-grid-item-amount/summary-grid-item-amount.stories.ts +1 -0
  347. package/dist/assets/components/summary-grid/components/summary-grid-item-amount/summary-grid-item-amount.vue +1 -0
  348. package/dist/assets/components/summary-grid/components/summary-grid-item-custom/summary-grid-item-custom.stories.ts +1 -0
  349. package/dist/assets/components/summary-grid/components/summary-grid-item-custom/summary-grid-item-custom.vue +1 -0
  350. package/dist/assets/components/summary-grid/components/summary-grid-item-numeric/summary-grid-item-numeric.stories.ts +1 -0
  351. package/dist/assets/components/summary-grid/components/summary-grid-item-numeric/summary-grid-item-numeric.vue +1 -0
  352. package/dist/assets/components/summary-grid/components/summary-grid-item-percentage/summary-grid-item-percentage.stories.ts +1 -0
  353. package/dist/assets/components/summary-grid/components/summary-grid-item-percentage/summary-grid-item-percentage.vue +1 -0
  354. package/dist/assets/components/summary-grid/components/summary-grid-item-text/summary-grid-item-text.stories.ts +1 -0
  355. package/dist/assets/components/summary-grid/components/summary-grid-item-text/summary-grid-item-text.vue +1 -0
  356. package/dist/assets/components/summary-grid/summary-grid.docs.mdx +152 -0
  357. package/dist/assets/components/summary-grid/summary-grid.stories.ts +1 -0
  358. package/dist/assets/components/summary-grid/summary-grid.vue +1 -0
  359. package/dist/assets/components/tabs/components/tab.vue +1 -0
  360. package/dist/assets/components/tabs/tabs.docs.mdx +98 -0
  361. package/dist/assets/components/tabs/tabs.stories.ts +1 -0
  362. package/dist/assets/components/tabs/tabs.vue +1 -0
  363. package/dist/assets/components/tag/tag.docs.mdx +137 -0
  364. package/dist/assets/components/tag/tag.stories.ts +1 -0
  365. package/dist/assets/components/tag/tag.vue +1 -0
  366. package/dist/assets/components/textarea/textarea.docs.mdx +53 -0
  367. package/dist/assets/components/textarea/textarea.stories.ts +1 -0
  368. package/dist/assets/components/textarea/textarea.vue +1 -0
  369. package/dist/assets/components/timeline/components/timeline-item/timeline-item.stories.ts +1 -0
  370. package/dist/assets/components/timeline/components/timeline-item/timeline-item.vue +1 -0
  371. package/dist/assets/components/timeline/timeline.docs.mdx +131 -0
  372. package/dist/assets/components/timeline/timeline.stories.ts +1 -0
  373. package/dist/assets/components/timeline/timeline.vue +1 -0
  374. package/dist/assets/components/toast/components/toast-item/toast-item.vue +1 -0
  375. package/dist/assets/components/toast/components/toast-shadow-stack/toast-shadow-stack.vue +1 -0
  376. package/dist/assets/components/toast/toast.docs.mdx +151 -0
  377. package/dist/assets/components/toast/toast.stories.ts +1 -0
  378. package/dist/assets/components/toast/toast.vue +1 -0
  379. package/dist/assets/components/toggle/toggle.docs.mdx +81 -0
  380. package/dist/assets/components/toggle/toggle.stories.ts +1 -0
  381. package/dist/assets/components/toggle/toggle.vue +1 -0
  382. package/dist/assets/components/tutorial/components/tutorial-popover/components/tutorial-indicator/tutorial-indicator.vue +1 -0
  383. package/dist/assets/components/tutorial/components/tutorial-popover/tutorial-popover.vue +1 -0
  384. package/dist/assets/components/tutorial/tutorial.docs.mdx +85 -0
  385. package/dist/assets/components/tutorial/tutorial.stories.ts +1 -0
  386. package/dist/assets/components/tutorial/tutorial.vue +1 -0
  387. package/dist/assets/components/typography/typography.docs.mdx +124 -0
  388. package/dist/assets/components/typography/typography.stories.ts +1 -0
  389. package/dist/assets/components/typography/typography.vue +1 -0
  390. package/dist/assets/composables/use-bento-currency/use-bento-currency.docs.mdx +28 -0
  391. package/dist/assets/composables/use-bento-currency/use-bento-currency.stories.ts +1 -0
  392. package/dist/assets/composables/use-click-outside/use-click-outside.docs.mdx +10 -0
  393. package/dist/assets/composables/use-click-outside/use-click-outside.stories.ts +1 -0
  394. package/dist/assets/directives/highlight/highlight.docs.mdx +64 -0
  395. package/dist/assets/directives/highlight/highlight.stories.ts +1 -0
  396. package/dist/assets/directives/keyboard-navigation/keyboard-navigation.docs.mdx +41 -0
  397. package/dist/assets/directives/keyboard-navigation/keyboard-navigation.stories.ts +1 -0
  398. package/dist/assets/directives/tooltip/tooltip.docs.mdx +86 -0
  399. package/dist/assets/directives/tooltip/tooltip.stories.ts +1 -0
  400. package/dist/assets/index.ts +1 -0
  401. package/dist/assets/templates/dashboard-template/components/dashboard-section/dashboard-section.stories.ts +1 -0
  402. package/dist/assets/templates/dashboard-template/components/dashboard-section/dashboard-section.vue +1 -0
  403. package/dist/assets/templates/dashboard-template/dashboard-template.docs.mdx +148 -0
  404. package/dist/assets/templates/dashboard-template/dashboard-template.stories.ts +1 -0
  405. package/dist/assets/templates/dashboard-template/dashboard-template.vue +1 -0
  406. package/dist/assets/templates/data-grid-template/data-grid-template.docs.mdx +117 -0
  407. package/dist/assets/templates/data-grid-template/data-grid-template.stories.ts +1 -0
  408. package/dist/assets/templates/data-grid-template/data-grid-template.vue +1 -0
  409. package/dist/main.js +30031 -0
  410. package/package.json +45 -3
@@ -0,0 +1 @@
1
+ import { type BentoColumn, BentoColumnOverflow, type BentoDatagridData, type BentoDatagridDataItem, type BentoDatagridDataItemId, BentoDatagridEvent, type BentoDatagridPaginationProps, type BentoDatagridSelection, type BentoDatagridSortByColumn, BentoDatagridSortDirection, } from './data-grid.types'; import { BentoCard } from '@/components/card'; import { action } from '@storybook/addon-actions'; import { type BentoFilterBarModel, type BentoFilterBarSearchBarProps, BentoFilterItemType, type BentoFilterValues, } from '../filter-bar'; import { BentoCurrency } from '@/components/currency'; import { BentoStatus, BentoStatusVariant } from '@/components/status'; import { BentoLink } from '@/components/link'; import { BentoTypography } from '@/components/typography'; import { computed, ref, watch } from 'vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { BentoDate, BentoDateFormat } from '@/components/date'; import { BentoCountry, BentoCountryCode } from '@/components/country'; import type { Meta, StoryObj } from '@storybook/vue'; import BentoDataGrid from './data-grid.vue'; import { isVue2 } from 'vue-demi'; import { BentoButton } from '../button'; import { BentoDashedUnderline } from '../dashed-underline'; import { BentoDataGridSummaryCell, BentoDataGridTrendCell } from './components'; import type { BentoColumnCellAction, BentoColumnCellActionEvent, BentoColumnCellActionsProp, BentoDataGridRowActionsProp, } from './components/data-grid-cell-actions/data-grid-cell-actions.types'; // Examples import DataGridEverythingBagel from './__tests__/data-grid-everything-bagel.vue?raw'; import DataGridFitContentInCardExample from './__tests__/data-grid-fit-content-in-card-example.vue?raw'; import DataGridWithSlotsExample from './__tests__/data-grid-with-slots-example.vue?raw'; import DataGridFilteringExample from './__tests__/data-grid-filtering-example.vue?raw'; import DataGridSelectionExample from './__tests__/data-grid-selection-example.vue?raw'; import DataGridTrendCellExample from './__tests__/data-grid-trend-cell-example.vue?raw'; const meta: Meta = { title: 'Data Grid', component: BentoDataGrid, argTypes: { // Values lazyLoadType: { control: { type: 'select', }, }, // Filter bar props filters: { table: { type: { summary: 'BentoFilterBarModel' }, }, }, filterSearchTerm: { name: 'filterSearchTerm', defaultValue: undefined, description: "Filter bar search term useful to filter all the data by text. If the value is non-existant (`null` or `undefined`) the search bar won't be enabled.", control: { type: 'text', }, }, // Slots default: { table: { disable: true }, }, header: { name: 'header', defaultValue: '', description: 'header text', table: { type: { summary: 'VNode[]' }, }, control: { type: 'text', }, }, footer: { name: 'footer', defaultValue: '', description: 'footer text', table: { type: { summary: 'VNode[]' }, }, control: { type: 'text', }, }, 'head-{columnKey}': { name: 'head-{columnKey}', defaultValue: '', description: 'the column slot where `{columnKey}` is the name of the column key. Allows the Column header to be customised.', table: { type: { summary: 'VNode[]' }, category: 'slots', }, control: { disable: true }, }, 'item-{columnKey}': { name: 'item-{columnKey}', defaultValue: '', description: 'the data item slot where `{columnKey}` is the name of the data item column key. Allow row cells to be customised.', table: { type: { summary: 'VNode[]' }, category: 'slots', }, control: { disable: true }, }, ['pagination-slot']: { control: { type: 'text' }, description: 'Passes content to the default pagination slot', table: { type: { summary: 'VNode[]' }, }, }, // Events [BentoDatagridEvent.COLUMN_RESIZE]: { name: BentoDatagridEvent.COLUMN_RESIZE, defaultValue: '', description: 'Triggered when a column is resized. Provides a field name and the new resized width in pixels.', table: { category: 'events', type: { summary: '({ field, newResizedWidth }) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.SELECT]: { name: BentoDatagridEvent.SELECT, defaultValue: '', description: 'Triggered when a row is selected. Provides an array of data item ids. `BentoDatagridDataItemId[]`', table: { category: 'events', type: { summary: '(selectedDataItemIds) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.NAVIGATE]: { name: BentoDatagridEvent.NAVIGATE, defaultValue: '', description: `Triggered when the navigation controls in the pagination are clicked (i.e. when the arrow buttons to the rightmost of the screen or the dropdown on the right hand are updated). It indicates to which page it should go next.`, table: { category: 'events', type: { summary: '(pageNumber: number) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.ITEMS_PAGE]: { name: BentoDatagridEvent.ITEMS_PAGE, defaultValue: '', description: `Triggered when the number of items per page is changed from the left hand dropdown of the pagination. It indicates the new number of items that will be displayed.`, table: { category: 'events', type: { summary: '(itemsPerPage: number) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.SORT]: { name: BentoDatagridEvent.SORT, defaultValue: '', description: `Triggered when a column is sorted. Provides an array of \`BentoDatagridSortByColumn\` which includes the \`field\` and the sort \`direction\` (\`BentoDatagridSortDirection\`) of the column:`, table: { category: 'events', type: { summary: '(sortByDirections) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_COLUMNS]: { name: BentoDatagridEvent.UPDATE_COLUMNS, defaultValue: '', description: `Triggered when a the columns have been updated. Provides an updated array of columns \`BentoColumn[]\` which should be passed back into the \`columns\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:columns.sync="columns"\``, table: { category: 'events', type: { summary: '(columns) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_CONDENSED]: { name: 'update:condensed', defaultValue: '', description: `Triggered when the config panel's condensed view toggle is interacted with. Provides the updated condensed \`boolean\`. (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:condensed.sync="condensed"\``, table: { category: 'events', type: { summary: '(condensed) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_FILTER_VALUES]: { name: BentoDatagridEvent.UPDATE_FILTER_VALUES, defaultValue: '', description: `Triggered when a the filters in the filterbar have updated. Provides an array of filters \`BentoFilterBarModel[]\` which should be passed back into the \`filter-values\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:filter-values.sync="filtersValues"\``, table: { category: 'events', type: { summary: '(filterValues) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_PAGINATION]: { name: BentoDatagridEvent.UPDATE_PAGINATION, defaultValue: '', description: `Triggered when a the pagination state has been updated. Provides an updated \`pagination\` properties object which should be passed back into the \`pagination\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:pagination.sync="pagination"\``, table: { category: 'events', type: { summary: '(paginationProps) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_SELECTION]: { name: BentoDatagridEvent.UPDATE_SELECTION, defaultValue: '', description: `Triggered when a the select data rows state has been updated. Provides an array of row IDs \`BentoDatagridDataItemId[]\` which should be passed back into the \`selection\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:selection.sync="selection"\``, table: { category: 'events', type: { summary: '(selection) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_SORT_BY]: { name: 'update:sort-by', defaultValue: '', description: `Triggered when a the sorting state has been updated. Provides an array of sorting directions \`BentoDatagridSortByColumn[]\` which should be passed back into the \`sort-by\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:sort-by.sync="sortBy"\``, table: { category: 'events', type: { summary: '(sortingDirections) => void' }, }, control: { disable: true }, }, // Filter bar search events [BentoDatagridEvent.FILTER_SEARCH_INPUT]: { name: BentoDatagridEvent.FILTER_SEARCH_INPUT, defaultValue: '', description: "Triggered whenever the filter's search is updated.", table: { category: 'events', type: { summary: '(newSearchTerm) => void' } }, control: { disable: true }, }, [BentoDatagridEvent.FILTER_SEARCH_CLEAR]: { name: BentoDatagridEvent.FILTER_SEARCH_CLEAR, defaultValue: '', description: "Triggered whenever the filter's search is cleared.", table: { category: 'events', type: { summary: '() => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM]: { name: BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM, defaultValue: '', description: "Triggered whenever the filter's search is updated to sync the model.", table: { category: 'events', type: { summary: '(newSearchTerm) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.ROW_CLICK]: { name: BentoDatagridEvent.ROW_CLICK, defaultValue: '', description: 'Triggered whenever a row has been clicked.', table: { category: 'events', type: { summary: '(row: BentoDatagridDataItem, event: MouseEvent) => void' }, }, }, [BentoDatagridEvent.NESTED_CONTENT_TOGGLED]: { name: BentoDatagridEvent.NESTED_CONTENT_TOGGLED, defaultValue: '', description: 'Triggered whenever a data item toggled is nested content', table: { category: 'events', type: { summary: '(dataItem: BentoDatagridDataItem) => void' }, }, control: { disable: true }, }, }, }; export default meta; type Story = StoryObj<typeof BentoDataGrid>; const DEFAULT_COLUMNS: Array<BentoColumn> = [ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Company', autoWidth: true }, { field: 'payment', label: 'Payment', minWidth: 350 }, ]; const DEFAULT_DATA: BentoDatagridData = [ { id: '03c0a799-1ced-4017-aea3-0cebb0106335', name: 'Shepherd Kane', payment: 160405, company: 'Merchant Helium', registered: '2023-06-30T13:56:09.498Z', tags: ['deserunt', 'et', 'esse'], reference: 'UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn', paymentStatus: 1, countryCode: BentoCountryCode.NL, }, { id: 'b6d4f1fd-b1f6-40a6-a443-f785c89604ef', name: 'Santos Palmer', payment: 163500, company: 'Merchant B', registered: '2023-04-28T13:11:09.498Z', tags: ['et', 'Lorem', 'sint'], reference: 'IxUZ9FsEPCZ9dFFPpI9C', paymentStatus: 2, countryCode: BentoCountryCode.BE, }, { id: '8f9e7877-1ca2-4929-9217-f1ec78709291', name: 'Joyce Johns', payment: 78500, company: 'Merchant C', registered: '2023-06-30T11:01:09.498Z', tags: ['laborum', 'enim', 'ullamco'], reference: 'y0qAQ0WDszRyqLY2WO9f', paymentStatus: 1, countryCode: BentoCountryCode.UA, }, { id: '0789a012-85a6-4657-87bb-319141938a61', name: 'Lynnette Melendez', payment: -45000, company: 'STRALOY', registered: '2019-08-01T13:56:01.498Z', tags: ['sit', 'consectetur', 'amet'], reference: '2vv5pzbRSwx8YsFoGGOC', paymentStatus: 1, countryCode: BentoCountryCode.BR, }, { id: '0b3981b6-c73b-47ad-81d1-b06f9b95d467', name: 'Wagner Mcintosh', payment: 1112, company: 'Merchant A', registered: '2022-12-30T02:12:09.498Z', tags: ['ad', 'ullamco', 'et'], reference: '54KN4jkpBuaRY5xMfe84', paymentStatus: 1, countryCode: BentoCountryCode.US, }, { id: '51d2b31d-d577-4fca-8329-a583babbbdda', name: 'Evans Zamora', payment: 1153.4, company: 'GEEKWAGON', registered: '2023-06-30T13:51:09.498Z', tags: ['consequat', 'magna', 'sit'], reference: 'Sj84kad0MGxpuozTjEk', paymentStatus: 0, countryCode: BentoCountryCode.FR, }, { id: '9bd59a6e-bd03-4477-813f-b844953b7645', name: 'Anderson Middleton', payment: 181722.23, company: 'BALOOBA', registered: '2021-03-02T01:34:09.498Z', tags: ['aliqua', 'adipisicing', 'magna'], reference: 'MtJ5Bnew0MGxpuozTjEk', paymentStatus: 1, countryCode: BentoCountryCode.IT, }, ]; const ADDITIONAL_DATA: BentoDatagridData = [ { id: '03c0a799-1ced-4017-aea3-0cebb0106335', name: 'Additional data fetched here', payment: 160405, company: 'Merchant Helium', registered: '2023-06-30T13:56:09.498Z', tags: ['deserunt', 'et', 'esse'], reference: 'UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn', paymentStatus: 1, countryCode: BentoCountryCode.NL, }, { id: 'b6d4f1fd-b1f6-40a6-a443-f785c89604ef', name: 'Santos Palmer', payment: 163500, company: 'Merchant B', registered: '2023-04-28T13:11:09.498Z', tags: ['et', 'Lorem', 'sint'], reference: 'IxUZ9FsEPCZ9dFFPpI9C', paymentStatus: 2, countryCode: BentoCountryCode.BE, }, { id: '8f9e7877-1ca2-4929-9217-f1ec78709291', name: 'Joyce Johns', payment: 78500, company: 'Merchant C', registered: '2023-06-30T11:01:09.498Z', tags: ['laborum', 'enim', 'ullamco'], reference: 'y0qAQ0WDszRyqLY2WO9f', paymentStatus: 1, countryCode: BentoCountryCode.UA, }, { id: '0789a012-85a6-4657-87bb-319141938a61', name: 'Lynnette Melendez', payment: -45000, company: 'STRALOY', registered: '2019-08-01T13:56:01.498Z', tags: ['sit', 'consectetur', 'amet'], reference: '2vv5pzbRSwx8YsFoGGOC', paymentStatus: 1, countryCode: BentoCountryCode.BR, }, { id: '0b3981b6-c73b-47ad-81d1-b06f9b95d467', name: 'Wagner Mcintosh', payment: 1112, company: 'Merchant A', registered: '2022-12-30T02:12:09.498Z', tags: ['ad', 'ullamco', 'et'], reference: '54KN4jkpBuaRY5xMfe84', paymentStatus: 1, countryCode: BentoCountryCode.US, }, { id: '51d2b31d-d577-4fca-8329-a583babbbdda', name: 'Evans Zamora', payment: 1153.4, company: 'GEEKWAGON', registered: '2023-06-30T13:51:09.498Z', tags: ['consequat', 'magna', 'sit'], reference: 'Sj84kad0MGxpuozTjEk', paymentStatus: 0, countryCode: BentoCountryCode.FR, }, { id: '9bd59a6e-bd03-4477-813f-b844953b7645', name: 'Anderson Middleton', payment: 181722.23, company: 'BALOOBA', registered: '2021-03-02T01:34:09.498Z', tags: ['aliqua', 'adipisicing', 'magna'], reference: 'MtJ5Bnew0MGxpuozTjEk', paymentStatus: 1, countryCode: BentoCountryCode.IT, }, ]; const DEFAULT_ARGS = { data: DEFAULT_DATA, columns: DEFAULT_COLUMNS, allowColumnDragAndDrop: false, pagination: {}, emptyState: { title: 'No results were found', image: 'no-results-found', variant: 'embedded', action: { title: 'Clear filters', event: action('clear'), }, description: 'Try a different term or reset search filters', }, }; // Default const DefaultCode = ` <template> <bento-data-grid :allow-row-clicks="false" :allow-column-drag-and-drop="false" :bulk-actions="undefined" :condensed="false" :columns="columns" :column-panel="false" :data="data" :empty-state="undefined" :filters="undefined" :filters-search-term="undefined" :fit-content="false" :get-data-item-id="undefined" :has-resizeable-columns="false" :has-nested-content="false" :highlighted-rows="undefined" :is-data-item-disabled="undefined" lazy-load-type="none" :loading="false" :outline="false" :no-spacing-top="false" :pagination="undefinded" :selectable-accross-pages="false" :sticky-pagination="false" :sticky-filters="false" :selectable="false" :selection="undefined" :sort-by="undefined" :tab-title="undefined" :truncate-column-headers="false" /> </template> <script setup lang="ts"> import { BentoColumn, BentoDataGrid, BentoDatagridData } from '@adyen/bento-vue2'; import { ref } from 'vue'; const data = ref<BentoDatagridData>(${JSON.stringify(DEFAULT_DATA)}); const columns = ref<BentoColumn[]>([ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]); </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" /> `, setup(props) { return { args: isVue2 ? props : _args }; }, }), args: DEFAULT_ARGS, parameters: storybookDocsParameter(DefaultCode), }; // Fluid Columns const FluidTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" /> </template> <script setup lang="ts"> import { BentoColumn, BentoDataGrid, BentoDatagridData } from '@adyen/bento-vue2'; import { ref } from 'vue'; const data = ref<BentoDatagridData>(${JSON.stringify(DEFAULT_DATA)}); const columns = ref<BentoColumn[]>([ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ]); </script> `; const rowActionsFluid: BentoDataGridRowActionsProp = (item: BentoDatagridDataItem) => { if (item.id === DEFAULT_DATA[3].id) { return [ { variant: 'edit', event: action('edit'), }, ]; } else if (item.id === DEFAULT_DATA[2].id) { return [ { variant: 'edit', event: action('edit') }, { variant: 'open', isShort: true, menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ]; } else if (item.id === DEFAULT_DATA[1].id) { return []; } return [ { variant: 'edit', event: action('edit'), }, { title: 'Delete', critical: true, event: action('Delete'), }, ]; }; export const Fluid: Story = { ...Default, args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ], rowActions: rowActionsFluid, }, parameters: storybookDocsParameter(FluidTemplateCode), }; // Slots export const Slots: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" > <template #header> <bento-typography variant="title" el="h2"> Users </bento-typography> </template> <template #head-name="{column}"> <bento-typography variant="caption"> {{ column.label }} </bento-typography> </template> <template #item-name="{item}"> <bento-typography variant="body"> {{ item.name }} </bento-typography> <template v-if="item.company === 'Merchant Helium'"> <div style="display: inline-flex; gap: 4px;"> <bento-typography v-for="itemTag in item.tags" :key="itemTag" variant="caption" :style="{ color: 'var(--b-color-label-secondary)' }" > { {{ itemTag }} } </bento-typography> </div> </template> </template> <template #item-company="{item}"> <bento-link isNotRouting to="https://www.adyen.com" :title="item.company" > {{ item.company }} </bento-link> <bento-typography v-if="item.company === 'Merchant B'" variant="caption" :style="{ color: 'var(--b-color-label-secondary)' }"> {{ item.registered }} </bento-typography> </template> <template #item-payment="{item}"> $ {{ item.payment }} </template> <template #footer> <bento-typography variant="caption"> Footer </bento-typography> </template> </bento-data-grid> `, setup(props) { return { args: isVue2 ? props : _args }; }, }), args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ], }, parameters: storybookDocsParameter(DataGridWithSlotsExample), }; // Sorting const useSort = () => { const sortBy = ref<Array<BentoDatagridSortByColumn>>([]); const data = ref<BentoDatagridData>(DEFAULT_DATA); const sort = (sortedColumns: Array<BentoDatagridSortByColumn>) => { data.value = sortedColumns[0].direction === BentoDatagridSortDirection.ASCENDING ? data.value.sort((a, b) => (a[sortedColumns[0].field] as string).localeCompare(b[sortedColumns[0].field] as string) ) : data.value .sort((a, b) => (a[sortedColumns[0].field] as string).localeCompare(b[sortedColumns[0].field] as string) ) .reverse(); action('sort')(sortedColumns); }; return { data, sortBy, sort }; }; const SortingTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" @sort="sort" :sort-by.sync="sortBy" /> </template> <script setup lang="ts"> import { BentoDataGrid, type BentoDatagridSortByColumn, type BentoDatagridData, type BentoColumn, } from '@adyen/bento-vue2' // Sortable fields must have "sortable: true" const columns: BentoColumn = [ { field: 'id', label: 'ID', minWidth: 300, sortable: true, mandatory: true }, { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, ] const data = ref<BentoDatagridData>([ { id: 1, name: 'First' }, { id: 2, name: 'Second' }, ]); const sortBy = ref<Array<BentoDatagridSortByColumn>>([]); const sort = (sortedColumns: Array<BentoDatagridSortByColumn>) => { data.value = sortedColumns[0].direction === BentoDatagridSortDirection.ASCENDING ? data.value.sort((a, b) => (a[sortedColumns[0].field] as number) .toString() .localeCompare(b[sortedColumns[0].field] as string) ) : data.value .sort((a, b) => (a[sortedColumns[0].field] as number) .toString() .localeCompare(b[sortedColumns[0].field] as string) ) .reverse(); } </script> `; export const Sorting: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid :data="data" :columns="columns" :sort-by.sync="sortBy" @sort="sort" /> `, setup(props) { const { data, sortBy, sort } = useSort(); return { args: isVue2 ? props : _args, data, sortBy, sort, }; }, }), args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', minWidth: 300, sortable: true, mandatory: true }, { field: 'company', label: 'Company', minWidth: 150, sortable: true }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ], sortBy: [], }, parameters: storybookDocsParameter(SortingTemplateCode), }; export const Selection: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <div> <bento-data-grid v-bind="args" :data="pagedData" :pagination="pagination" :filters="filtersConfig" :filter-values="filterValues" :is-data-item-disabled="isDataItemDisabled" :selection="selection" :filter-search-term="searchTerm" @update:filter-search-term="searchTerm = $event" @update:pagination="pagination = $event" @update:filter-values="filterValues = $event" @select-all-across-pages="onSelectAllAcrossPages" @select-all-current-results="onSelectAllInCurrentResults" @deselect-all-current-results="onDeselectAllInCurrentResults" @filter-search-clear="onSearchCleared" @update:selection="onSelect" @row-click="rowClickAction" /> </div> `, setup(props) { const args = isVue2 ? props : _args; const selection = ref([]); const filtersConfig = computed<BentoFilterBarModel>(() => args?.filters ?? FILTERS); const filterValues = ref<BentoFilterValues>([]); const searchTerm = ref(''); const data = ref( Array.from({ length: 20 }, (_, i) => ({ id: `id-${i + 1}`, name: `Name ${i + 1}`, payment: (i + 1) * 100, company: `Merchant ${i + 1}`, })) ); const isDataItemDisabled = (row: BentoDatagridDataItem) => { return row.id === data.value[3].id; }; const pagination = ref<BentoDatagridPaginationProps>({ page: 1, size: 10, totalCount: 20, hasNext: true, }); const filteredData = computed<BentoDatagridData>(() => { let internalData = data.value; if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); const searchableFields = args.columns.map(column => column.field); internalData = internalData.filter((item: BentoDatagridDataItem) => { return searchableFields.some(field => { const value = String(item[field]); return value.toLowerCase().includes(lowerCaseSearchTerm); }); }); } return internalData.filter(item => filterValues.value.every(filter => filter.value ? String(item[filter.field]).toLowerCase().includes(String(filter.value).toLowerCase()) : true ) ); }); const pagedData = computed<BentoDatagridData>(() => { return filteredData.value.slice( (pagination.value.page - 1) * pagination.value.size, pagination.value.page * pagination.value.size ); }); const enabledIds = computed(() => data.value.filter(item => !isDataItemDisabled(item)).map(x => x.id)); const enabledFilteredIds = computed(() => filteredData.value.filter(item => !isDataItemDisabled(item)).map(x => x.id) ); const onSelect = value => { selection.value = value; action('update:selection')(value); }; const onSelectAllAcrossPages = () => { selection.value = enabledIds.value; action('select-all-across-pages')(); }; const onSelectAllInCurrentResults = () => { selection.value = [...new Set([...selection.value, ...enabledFilteredIds.value])]; action('select-all-current-results')(); }; const onDeselectAllInCurrentResults = () => { selection.value = selection.value.filter(item => !enabledFilteredIds.value.includes(item)); action('deselect-all-current-results')(); }; const onSearchCleared = () => { searchTerm.value = ''; action('search-clear')(); }; watch( filteredData, () => { pagination.value.totalCount = filteredData.value.length; }, { immediate: true } ); return { args, onSelect, onSelectAllAcrossPages, onSelectAllInCurrentResults, onDeselectAllInCurrentResults, pagination, selection, pagedData, filtersConfig, filterValues, searchTerm, rowClickAction: value => action('row-click')(value), isDataItemDisabled, onSearchCleared, }; }, }), args: { ...DEFAULT_ARGS, selectable: true, allowRowClicks: true, selectableAcrossPages: true, bulkActions: [ { title: 'Edit', event: () => true, }, { title: 'Delete', critical: true, event: () => true, }, ], }, parameters: storybookDocsParameter(DataGridSelectionExample), }; // Filtering const FILTERS: BentoFilterBarModel = [ { field: 'name', label: 'Name', type: BentoFilterItemType.INPUT, }, { field: 'company', label: 'Company', type: BentoFilterItemType.INPUT, }, ]; export const Filtering: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const filtersConfig = computed<BentoFilterBarModel>(() => args?.filters ?? FILTERS); const filterValues = ref<BentoFilterValues>([]); const searchTerm = ref(''); const searchConfig = ref<BentoFilterBarSearchBarProps>({ inputFieldAriaLabel: 'Search for items', placeholder: 'Placeholder', hint: 'Hint', }); const dataSet = computed<BentoDatagridData>(() => { let filteredData = args.data; // Apply global search term filter if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); const searchableFields = args.columns.map(column => column.field); filteredData = filteredData.filter((item: BentoDatagridDataItem) => { return searchableFields.some(field => { const value = String(item[field]); return value.toLowerCase().includes(lowerCaseSearchTerm); }); }); } filteredData = filteredData.filter(item => filterValues.value?.every(filter => { return filter.value ? String(item[filter.field]).toLowerCase().includes(String(filter.value).toLowerCase()) : true; }) ); return filteredData; }); watch(filterValues, () => { action('filter-values:update')(filterValues.value); }); const onSearchCleared = action('search-clear'); const onSearchInput = action('search-input'); const onSearchTermUpdated = action('update:search-term'); return { // Values dataSet, filtersConfig, filterValues, searchTerm, searchConfig, // Methods onSearchCleared, onSearchInput, onSearchTermUpdated, // Vue 3 storybook fix args, }; }, template: ` <bento-data-grid v-bind="args" :data="dataSet" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" :filter-search-config="searchConfig" @filter-search-clear="onSearchCleared" @filter-search-input="onSearchInput" @update:filter-search-term="onSearchTermUpdated" /> `, }), args: DEFAULT_ARGS, parameters: storybookDocsParameter(DataGridFilteringExample), }; // No Results const NoResultsTemplateCode = ` <bento-data-grid :data="[]" :columns="[ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]" :empty-state="{ title: 'No results were found', image: 'no-results-found', variant: 'embedded', action: { title: 'Clear filters', event: clearFiltersFunction, }, description: 'Try a different term or reset search filters', }" /> `; export const NoResults: Story = { ...Filtering, args: { ...DEFAULT_ARGS, data: [], filters: FILTERS, filterValues: [{ field: 'company', value: 'CIA' }], }, parameters: storybookDocsParameter(NoResultsTemplateCode), }; // Pagination const PaginationTemplateCode = ` <template> <bento-data-grid :data="dataGridData" :columns="columnsConfig" :pagination.sync="pagination" @update:pagination="onUpdatePagination" /> </template> <script lang="ts" setup> const data="[{ id: '1', name: 'Jane' }, { id: '1', name: 'John' }]" const pagination = ref({ description: 'datagrid navigation', hasNext: true, size: 10 }); // With API call watch( () => pagination.value, () => { makeAPICall(pagination.value); } ); // Without API call const onUpdatePagination = (newPagination: BentoDatagridPaginationProps) => { pagination.value = { ...newPagination }; const newSetStartIndex = pagination.value.page * pagination.value.size; const amountOfItems = newSetStartIndex + pagination.value.size - 1; data.value = arrayWithFullSetOfValues.slice(newSetStartIndex, amountOfItems); }; </script> `; export const Pagination: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="dataGridData" :pagination.sync="pagination" @update:pagination="updatePagination" @items-page="updateItemsPerPage" /> `, setup(props) { const args = isVue2 ? props : _args; const largeData = [ ...DEFAULT_ARGS.data, ...DEFAULT_ARGS.data.map(dataItem => ({ ...dataItem, id: `${dataItem.id}+1` })), ]; const pagination = ref(args.pagination); const dataGridData = computed(() => { const end = pagination.value.size <= largeData.length ? pagination.value.size : largeData.length; return largeData.slice(pagination.value.page - 1, end); }); const updateItemsPerPage = (itemsPerPage: number) => { action('items-page')(itemsPerPage); }; const updatePagination = (newPagination: BentoDatagridPaginationProps) => { action('navigate')(newPagination.page); action('update:pagination')(newPagination); }; return { // Values dataGridData, pagination, // Methods updateItemsPerPage, updatePagination, // Vue 3 storybook fixes args: isVue2 ? props : _args, }; }, }), args: { ...DEFAULT_ARGS, pagination: { hasNext: true, description: 'datagrid navigation', size: 10, page: 1, totalCount: DEFAULT_ARGS.data.length * 2, }, }, parameters: storybookDocsParameter(PaginationTemplateCode), }; // Payment Status (demo use case for datagrid V2) export const EverythingBagelExample: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoStatus, BentoCurrency, BentoDate, BentoCountry, BentoButton }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="filteredAndSortedData" :columns.sync="columns" :condensed="isCondensed" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" :sort-by="sortBy" @update:sort-by="sortBy = $event" @update:condensed="isCondensed = $event" :selection.sync="selection" :is-data-item-disabled="isDataItemDisabledForSelection" :row-actions="rowActions" selectable @row-click="onRowClick" > <template #item-paymentStatus="{item}"> <div style="display: flex"> <bento-status :variant="getPaymentStatus(item.paymentStatus)" /> <span>{{ getPaymentText(item.paymentStatus) }}</span> </div> </template> <template #item-reference="{item}"> <bento-link isNotRouting to="https://www.adyen.com" :title="item.reference" > {{ item.reference }} </bento-link> </template> <template #item-countryCode="{item}"> <bento-country :code="item.countryCode" /> </template> <template #item-payment="{item}"> <bento-currency currency="EUR" :value="item.payment" /> </template> <template #item-date="{item}"> <bento-date :date="new Date(item.registered)" :format='BentoDateFormat.FULL_DATE_TIME_WITH_PERIOD'/> </template> </bento-data-grid> `, setup(props) { const args = isVue2 ? props : _args; const columns = ref<Array<BentoColumn>>(args.columns); const filtersConfig = ref<BentoFilterBarModel>(args.filters); const filterValues = ref<BentoFilterValues>(args?.filterValues); const sortBy = ref<Array<BentoDatagridSortByColumn>>([]); const searchTerm = ref(''); const isCondensed = ref(args.condensed); const getPaymentStatus = (paymentStatus: number) => { switch (paymentStatus) { case 2: return BentoStatusVariant.GREEN; case 1: return BentoStatusVariant.YELLOW; case 0: default: return BentoStatusVariant.RED; } }; const getPaymentText = (paymentStatus: number) => { switch (paymentStatus) { case 2: return 'Paid'; case 1: return 'In progress'; case 0: default: return 'Not paid'; } }; const onRowClick = (item: BentoDatagridDataItem) => { action('row-click')(item); }; // Selection const isDataItemDisabledForSelection = (row: BentoDatagridDataItem) => row.id === DEFAULT_DATA[3].id; const selection = ref<BentoDatagridSelection>([DEFAULT_DATA[1].id]); // Filtering const filteredData = computed(() => // If any filters or search terms have been set filterValues.value?.length === 0 && searchTerm.value === '' ? args.data : args.data.filter(item => { const foundFilteredItem = filterValues.value?.length > 0 ? filterValues.value.every(filter => filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true ) : true; const foundSearchFilteredItem = searchTerm.value ? (item?.company as string) .toLowerCase() .includes((searchTerm.value as string).toLowerCase()) : true; return foundFilteredItem && foundSearchFilteredItem; }) ); const filteredAndSortedData = computed(() => { if (sortBy.value?.length === 0) { return filteredData.value; } return sortBy.value[0].direction === BentoDatagridSortDirection.ASCENDING ? filteredData.value.sort((a, b) => (a[sortBy.value[0].field] as string).localeCompare(b[sortBy.value[0].field] as string) ) : filteredData.value .sort((a, b) => (a[sortBy.value[0].field] as string).localeCompare(b[sortBy.value[0].field] as string) ) .reverse(); }); const rowActions: BentoDataGridRowActionsProp = (item: BentoDatagridDataItem) => { if (item.id === DEFAULT_DATA[3].id) { return [ { variant: 'edit', event: action('edit'), }, ]; } else if (item.id === DEFAULT_DATA[2].id) { return [ { variant: 'edit', event: action('edit') }, { variant: 'open', isShort: true, menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ]; } else if (item.id === DEFAULT_DATA[1].id) { return []; } return [ { variant: 'edit', event: action('edit'), }, { title: 'Delete', critical: true, event: action('Delete'), }, ]; }; return { // Methods getPaymentStatus, getPaymentText, // Functions onRowClick, isDataItemDisabledForSelection, // Values columns, filteredAndSortedData, sortBy, filtersConfig, filterValues, searchTerm, isCondensed, selection, rowActions, // Enums BentoDateFormat, // Vue 3 storybook fix args, }; }, }), args: { ...DEFAULT_ARGS, zebraRows: true, filters: [ { field: 'company', label: 'Company', value: null, type: BentoFilterItemType.INPUT, }, { field: 'countryCode', label: 'Country/Region', value: undefined, options: { listboxItems: [ { value: BentoCountryCode.NL, }, { value: BentoCountryCode.BE, }, ], variant: 'name', }, type: BentoFilterItemType.SELECT_COUNTRY, }, ], columns: [ { field: 'company', label: 'Company', sortable: true, mandatory: true, autoWidth: true, frozen: true, minWidth: 200, }, { field: 'paymentStatus', label: 'Status', width: 180, infoIconTooltip: 'For current day' }, { field: 'reference', label: 'Reference', width: 320, overflow: BentoColumnOverflow.ELLIPSIS }, { field: 'countryCode', label: 'Country/Region', width: 320 }, { field: 'payment', label: 'Payment', width: 180, numeric: true }, { field: 'date', label: 'Execution Date', width: 200 }, ], allowRowClicks: true, allowColumnDragAndDrop: true, columnPanel: true, highlightedRows: [DEFAULT_DATA[0].id, DEFAULT_DATA[4].id], }, parameters: storybookDocsParameter(DataGridEverythingBagel), }; export const DisplayOptions = (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoDate }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :columns.sync="columns" :condensed.sync="isCondensed"> <template #item-date="{item}"> <bento-date :date="new Date(item.registered)" :format='BentoDateFormat.FULL_DATE_TIME_WITH_PERIOD'/> </template> </bento-data-grid> `, setup(props) { const args = isVue2 ? props : _args; const isCondensed = ref(args.condensed); const columns = ref([ { field: 'name', label: 'Name', minWidth: 200, mandatory: true }, { field: 'reference', label: 'Reference', width: 320, overflow: BentoColumnOverflow.ELLIPSIS, frozen: true, }, { field: 'company', label: 'Company', minWidth: 150, width: 250, frozen: true }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, { field: 'countryCode', label: 'Country/Region', width: 280 }, { field: 'date', label: 'Execution Date', width: 300 }, ]); return { // Values columns, isCondensed, // Enums BentoDateFormat, // Vue 3 storybook fix args, }; }, }); DisplayOptions.args = { ...DEFAULT_ARGS, allowColumnDragAndDrop: true, columnPanel: true, }; DisplayOptions.parameters = storybookDocsParameter(` <bento-data-grid column-panel :data="dataArray" :columns.sync="columns" :condensed.sync="isCondensed" />`); export const GridInCard: Story = { render: (_args, { argTypes }) => ({ components: { BentoCard, BentoDataGrid, BentoDate }, props: Object.keys(argTypes), template: ` <div style="max-width: 500px;"> <bento-card> Grid in a card <template #content> <div style="height: 300px;"> <bento-data-grid v-bind="args" :columns.sync="columns" :condensed.sync="isCondensed" :pagination="pagination" fit-content outline > <template #item-date="{item}"> <bento-date :date="new Date(item.registered)" :format='BentoDateFormat.FULL_DATE_TIME_WITH_PERIOD'/> </template> </bento-data-grid> </div> </template> </bento-card> </div> `, setup(props) { const args = isVue2 ? props : _args; const isCondensed = ref(args.condensed); const pagination = ref({ hasNext: true, size: 20, page: 1, description: 'datagrid navigation', }); const columns = ref([ { field: 'name', label: 'Name', minWidth: 200, mandatory: true }, { field: 'reference', label: 'Reference', width: 320, overflow: BentoColumnOverflow.ELLIPSIS, }, { field: 'company', label: 'Company', minWidth: 150, width: 250 }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, { field: 'countryCode', label: 'Country/Region', width: 280 }, { field: 'date', label: 'Execution Date', width: 300 }, ]); return { // Values columns, isCondensed, pagination, // Enums BentoDateFormat, // Vue 3 storybook fix args, }; }, }), args: { ...DEFAULT_ARGS, columnPanel: false, }, parameters: storybookDocsParameter(DataGridFitContentInCardExample), }; // Default const lazyLoadingCode = ` // With infinite scroll <bento-data-grid :data="dataArray" :columns="[ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]" lazy-load-type="automatic" :loading="yourLoadingLogic" @update:load-more="yourInfiniteScrollHandler" /> // With 'load more' button <bento-data-grid :data="dataArray" :columns="[ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]" lazy-load-type="button" :loading="yourLoadingLogic" @update:load-more="yourInfiniteScrollHandler" />`; export const lazyLoading: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <div style="height: 300px;"> <bento-data-grid v-bind="args" :lazy-load-type="lazyLoadType" :loading="loading" @update:show-more="infiniteScrollHandler"/> </div> `, setup(props) { const args = isVue2 ? props : _args; const loading = ref(false); const data = ref(DEFAULT_DATA); const infiniteScrollHandler = () => { loading.value = true; setTimeout(() => { data.value.push(...ADDITIONAL_DATA); loading.value = false; }, 2000); }; return { args, loading, infiniteScrollHandler, }; }, }), args: { ...DEFAULT_ARGS, lazyLoadType: 'automatic', }, parameters: storybookDocsParameter(lazyLoadingCode), }; // Added just to be able to easily play around with the sticky headers/pagination/filters // Not really relevant as a Storybook example, so not adding storybook code example export const LongDataWithStickyFiltersAndPagination: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const filtersConfig = ref<BentoFilterBarModel>([ { field: 'col1', label: 'Column 1', type: BentoFilterItemType.INPUT, }, { field: 'col2', label: 'Column 2', type: BentoFilterItemType.INPUT, }, ]); const filterValues = ref(); const searchTerm = ref(''); const isCondensed = ref(args.condensed); const columns = ref( Array.from({ length: 20 }, (_, index) => ({ field: `col${index + 1}`, label: `Column ${index + 1}`, width: 200, mandatory: index === 0, })) ); const data = ref( Array.from({ length: 50 }, (_, rowIndex) => columns.value.reduce((acc, col, colIndex) => { acc[col.field] = `Row ${rowIndex + 1} - Col ${colIndex + 1}`; return acc; }, {}) ) ); const dataSet = computed(() => // If any filters or search terms have been set filterValues.value?.length === 0 && searchTerm.value === '' ? data.value : data.value.filter(item => { const foundFilteredItem = filterValues.value?.length > 0 ? filterValues.value.every(filter => filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true ) : true; const foundSearchFilteredItem = searchTerm.value ? Object.keys(item).some(key => key ? (item[key] as string) .toLowerCase() .includes((searchTerm.value as string).toLowerCase()) : true ) : true; return foundFilteredItem && foundSearchFilteredItem; }) ); const onSearchCleared = action('search-clear'); const onSearchInput = action('search-input'); const onSearchTermUpdated = action('update:search-term'); return { // Values dataSet, filtersConfig, filterValues, searchTerm, columns, isCondensed, // Methods onSearchCleared, onSearchInput, onSearchTermUpdated, // Vue 3 storybook fix args, }; }, template: ` <bento-data-grid v-bind="args" :data="dataSet" :columns.sync="columns" :condensed.sync="isCondensed" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" @filter-search-clear="onSearchCleared" @filter-search-input="onSearchInput" @update:filter-search-term="onSearchTermUpdated" /> `, }), args: { ...DEFAULT_ARGS, columnPanel: true, stickyPagination: true, stickyFilters: true }, }; // Cell actions const CellActionsTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" /> </template> <script setup lang="ts"> import { ref } from 'vue'; import { BentoDataGrid, type BentoColumn, type BentoDatagridData, type BentoDatagridDataItem, type BentoColumnCellActionEvent, type BentoColumnCellAction, } from '@adyen/bento-vue2'; // --- Action Handlers --- const editItem: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { // to be implemented }; const cancelAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { // to be implemented }; const transferAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { // to be implemented }; const deleteAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { // to be implemented }; // --- Dynamic Cell Actions Function --- // This function generates actions based on the row's data. const getNameCellActions = (rowDataItem: BentoDatagridDataItem): Array<BentoColumnCellAction> => { const actions: Array<BentoColumnCellAction> = []; // Conditionally add the 'edit' action only for the first row. if (rowDataItem.id === 1) { actions.push({ variant: 'edit', event: editItem }); } // Add a 'more' menu with dynamic options. actions.push({ variant: 'more', isShort: true, menuData: [ { text: 'Cancel payment', event: cancelAction, critical: true, }, { text: 'Transfer payment', event: transferAction, }, { text: 'Delete payment', event: deleteAction, // Example: disable delete for the second row. disabled: rowDataItem.id === 2, }, ], }); return actions; }; // --- Column Definitions --- const columns: Array<BentoColumn> = [ { field: 'name', label: 'Name (Dynamic Actions)', minWidth: 300, mandatory: true, // Use a function for dynamic cell actions cellActions: getNameCellActions, }, { field: 'company', label: 'Reference', autoWidth: true }, { field: 'payment', label: 'Payment (Static Actions)', minWidth: 350, // Use a static array for cell actions cellActions: [ { variant: 'copy', getCopyData: (rowDataItem: BentoDatagridDataItem, columnKey: string) => \`\${rowDataItem[columnKey]}\`, }, { variant: 'open', event: action('open-payment') }, ], }, ]; // --- Data --- const data = ref<BentoDatagridData>([ { id: 1, name: 'First', payment: 20.9 }, { id: 2, name: 'Second', payment: 10.0 }, ]); </script> `; export const CellActions: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :columns="columns"> <template #item-custom="{item}"> Value </template> </bento-data-grid> `, setup(props) { // --- Action Handlers --- const editItem: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('edit')(rowDataItem, columnKey); }; const cancelAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('cancel')(rowDataItem, columnKey); }; const transferAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('transfer')(rowDataItem, columnKey); }; const deleteAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('delete')(rowDataItem, columnKey); }; // --- Dynamic Cell Actions Function --- // This function generates actions based on the row's data. const getNameCellActions: BentoColumnCellActionsProp = rowDataItem => { const actions: Array<BentoColumnCellAction> = []; // Conditionally add the 'edit' action only for the first row. if (rowDataItem.id === DEFAULT_DATA[3].id) { actions.push({ variant: 'edit', event: editItem }); } // Add a 'more' menu with dynamic options. actions.push({ variant: 'more', isShort: true, menuData: [ { text: 'Cancel payment', event: cancelAction, critical: true, }, { text: 'Transfer payment', event: transferAction, }, { text: 'Delete payment', event: deleteAction, // Example: disable delete for the second row. disabled: rowDataItem.id === DEFAULT_DATA[2].id, }, ], }); return actions; }; // --- Column Definitions --- const columns: Array<BentoColumn> = [ { field: 'name', label: 'Name (Dynamic Actions)', minWidth: 300, mandatory: true, // Use a function for dynamic cell actions cellActions: getNameCellActions, }, { field: 'company', label: 'Reference', autoWidth: true }, { field: 'payment', label: 'Payment (Static Actions)', minWidth: 350, // Use a static array for cell actions cellActions: [ { variant: 'copy', getCopyData: (rowDataItem: BentoDatagridDataItem, columnKey: string) => `${rowDataItem[columnKey]}`, }, { variant: 'open', event: action('open-payment') }, ], }, ]; return { args: isVue2 ? props : _args, columns }; }, }), args: { ...DEFAULT_ARGS, highlightedRows: [DEFAULT_DATA[3].id], columns: [ { field: 'name', label: 'Name (Dynamic Actions)', minWidth: 300, mandatory: true, cellActions: [ () => (rowDataItem: BentoDatagridDataItem) => ({ variant: 'edit', disabled: rowDataItem.id === DEFAULT_DATA[3].id, event: action('edit'), }), { variant: 'open', menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ], }, { field: 'company', label: 'Reference', autoWidth: true }, { field: 'payment', label: 'Payment (Static Actions)', minWidth: 350, cellActions: [ { variant: 'copy', getCopyData: (rowDataItem: BentoDatagridDataItem, columnKey: string) => `$${rowDataItem[columnKey]}`, }, { variant: 'more', menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], isShort: true, }, ], }, { field: 'custom', label: 'Custom template', minWidth: 200, cellActions: [{ variant: 'edit', event: action('edit') }], }, ], }, parameters: storybookDocsParameter(CellActionsTemplateCode), }; // NestContentSlot const NestedContentSlotTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" has-nested-content :should-show-nested-content="filterRowsThatShowNestedContent" > <template #nested-slot="{item}"> <bento-typography>Nested content for {{ item.company }}</bento-typography> </template> </bento-data-grid> </template> <script setup lang="ts"> import { BentoDataGrid, type BentoColumn, type BentoDatagridData, type BentoDatagridShouldShowNestedContent } from '@adyen/bento-vue2' const columns: BentoColumn = [ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'payment', label: 'Payment', minWidth: 300, mandatory: true }, ] const data = ref<BentoDatagridData>([ { id: 1, name: 'First', payment: 20.9 }, { id: 2, name: 'Second', payment: 10.0 }, ]); const filterRowsThatShowNestedContent: BentoDatagridShouldShowNestedContent = (item) => item.id === 2; </script> `; export const NestedContentSlot: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" > <template #nested-slot="{item}"> <div style="height: 80px; text-align: center; display: flex; align-items: center;"> <bento-typography>Nested content slot for {{ item.company }}</bento-typography> </div> </template> </bento-data-grid> `, setup(props) { return { args: isVue2 ? props : _args }; }, }), args: { ...DEFAULT_ARGS, hasNestedContent: true, zebraRows: true, shouldShowNestedContent: (item: BentoDatagridDataItem) => item.id !== DEFAULT_ARGS.data[1].id, }, parameters: storybookDocsParameter(NestedContentSlotTemplateCode), }; export const StickyHorizontalScrollbar: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoButton }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const filtersConfig = ref<BentoFilterBarModel>([ { field: 'col1', label: 'Column 1', type: BentoFilterItemType.INPUT, }, { field: 'col2', label: 'Column 2', type: BentoFilterItemType.INPUT, }, ]); const filterValues = ref(); const searchTerm = ref(''); const isCondensed = ref(args.condensed); const columns = ref( Array.from({ length: 20 }, (_, index) => ({ field: `col${index + 1}`, label: `Column ${index + 1}`, width: 200, mandatory: index === 0, })) ); const data = ref( Array.from({ length: 50 }, (_, rowIndex) => columns.value.reduce((acc, col, colIndex) => { acc[col.field] = `Row ${rowIndex + 1} - Col ${colIndex + 1}`; return acc; }, {}) ) ); const dataSet = computed(() => // If any filters or search terms have been set filterValues.value?.length === 0 && searchTerm.value === '' ? data.value : data.value.filter(item => { const foundFilteredItem = filterValues.value?.length > 0 ? filterValues.value.every(filter => filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true ) : true; const foundSearchFilteredItem = searchTerm.value ? Object.keys(item).some(key => key ? (item[key] as string) .toLowerCase() .includes((searchTerm.value as string).toLowerCase()) : true ) : true; return foundFilteredItem && foundSearchFilteredItem; }) ); const onSearchCleared = action('search-clear'); const onSearchInput = action('search-input'); const onSearchTermUpdated = action('update:search-term'); return { // Values dataSet, filtersConfig, filterValues, searchTerm, columns, isCondensed, // Methods onSearchCleared, onSearchInput, onSearchTermUpdated, // Vue 3 storybook fix args, }; }, template: ` <bento-data-grid v-bind="args" :data="dataSet" :columns.sync="columns" :condensed.sync="isCondensed" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" @filter-search-clear="onSearchCleared" @filter-search-input="onSearchInput" @update:filter-search-term="onSearchTermUpdated" > <template #pagination-slot> <bento-button variant="tertiary"> Pagination slot button </bento-button> </template> </bento-data-grid> `, }), args: { ...DEFAULT_ARGS, pagination: { page: 1, hidePageSize: true }, columnPanel: true, stickyPagination: true, stickyFilters: true, stickyHorizontalScrollbar: true, }, }; const NestedRowsTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" has-nested-rows :should-show-nested-row="filterRowsThatShowNestedContent" @nested-content-toggled="onParentRowToggle" /> </template> <script setup lang="ts"> import { BentoDataGrid, type BentoColumn, type BentoDatagridData, type BentoDatagridShouldShowNestedRowsFn } from '@adyen/bento-vue2' const columns: BentoColumn = [ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'payment', label: 'Payment', minWidth: 300, mandatory: true }, ] const data = ref<BentoDatagridData>([ { id: 1, name: 'First', payment: 20.9 }, { id: 2, name: 'Second', payment: 10.0 }, ]); const filterRowsThatShowNestedRows: BentoDatagridShouldShowNestedRowsFn = (item) => item.id === 2; const onParentRowToggle = async (item: BentoDatagridDataItem) => { const childrenForToggledParentItem = await makeAPICallToGetChildren(item.id); dataState.value = dataState.value.map(itemToChange => { if (itemToChange.id === item.id) { return { ...itemToChange, children: childrenForToggledParentItem } } return itemToChange; }); }; </script> `; export const NestedRows: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="dataState" @nested-content-toggled="onParentRowToggle" /> `, setup(props) { const dataState = ref<Array<BentoDatagridDataItem>>(DEFAULT_ARGS.data); const selection = ref<BentoDatagridDataItemId>(); const onParentRowToggle = (item: BentoDatagridDataItem) => { setTimeout(() => { const children = [ { id: `03c0a799-1ced-4017-aea3-111111${item.id}`, name: `Child of ${item.name} #1`, payment: 160405, company: 'Merchant Helium', registered: '2023-06-30T13:56:09.498Z', tags: ['deserunt', 'et', 'esse'], reference: 'UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn', paymentStatus: 1, countryCode: BentoCountryCode.NL, parentId: DEFAULT_ARGS.data[1].id, }, { id: `03c0a799-1ced-4017-aea3-1111113${item.id}`, name: `Child of ${item.name} #2`, payment: 163500, company: 'Merchant B', registered: '2023-04-28T13:11:09.498Z', tags: ['et', 'Lorem', 'sint'], reference: 'IxUZ9FsEPCZ9dFFPpI9C', paymentStatus: 2, countryCode: BentoCountryCode.BE, parentId: DEFAULT_ARGS.data[1].id, }, { id: `03c0a799-1ced-4017-aea3-1111114${item.id}`, name: `Child of ${item.name} #3`, payment: 78500, company: 'Merchant C', registered: '2023-06-30T11:01:09.498Z', tags: ['laborum', 'enim', 'ullamco'], reference: 'y0qAQ0WDszRyqLY2WO9f', paymentStatus: 1, countryCode: BentoCountryCode.UA, parentId: DEFAULT_ARGS.data[1].id, }, ]; dataState.value = dataState.value.map(itemToChange => { if (itemToChange.id === item.id) { return { ...itemToChange, children, }; } return itemToChange; }); }, 1000); }; return { args: isVue2 ? props : _args, dataState, selection, onParentRowToggle }; }, }), args: { ...DEFAULT_ARGS, hasNestedRows: true, zebraRows: true, shouldShowNestedRows: dataItem => { return dataItem.id !== DEFAULT_ARGS.data[1].id; }, }, parameters: storybookDocsParameter(NestedRowsTemplateCode), }; export const RowActions: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :row-actions="rowActions" /> `, setup(props) { const rowActions: BentoDataGridRowActionsProp = (item: BentoDatagridDataItem) => { if (item.id === DEFAULT_DATA[0].id) { return [ { variant: 'edit', event: action('edit') }, { title: 'Delete', critical: true, event: action('Delete'), }, ]; } else if (item.id === DEFAULT_DATA[1].id) { return []; } else if (item.id === DEFAULT_DATA[2].id) { return [ { variant: 'edit', event: action('edit') }, { variant: 'open', isShort: true, menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ]; } return [ { variant: 'edit', event: action('edit'), }, ]; }; return { args: isVue2 ? props : _args, rowActions, }; }, }), args: { ...DEFAULT_ARGS, }, parameters: storybookDocsParameter(` <template> <bento-data-grid :data="data" :columns="columns" :row-actions="getRowActions" /> </template> <script setup lang="ts"> import { ref } from 'vue'; import { BentoDataGrid, type BentoColumn, type BentoDatagridData, type BentoDatagridDataItem, type BentoDataGridRowActionsProp } from '@adyen/bento-vue2'; const data = ref<BentoDatagridData>([ { id: '1', name: 'Shepherd Kane', company: 'Merchant Helium' }, { id: '2', name: 'Santos Palmer', company: 'Merchant B' }, { id: '3', name: 'Joyce Johns', company: 'Merchant C' }, ]); const columns = ref<BentoColumn[]>([ { field: 'name', label: 'Name' }, { field: 'company', label: 'Company' }, ]); const getRowActions: BentoDataGridRowActionsProp = (item: BentoDatagridDataItem) => { // Example: Show different actions based on the row data if (item.id === '1') { return [ { variant: 'edit', event: (item) => console.log('Editing', item) }, { title: 'Delete', critical: true, event: (item) => console.log('Deleting', item) } ]; } if (item.id === '3') { return [ { variant: 'edit', event: (item) => console.log('Editing', item) }, { variant: 'more', menuData: [ { text: 'Action 1', event: (item) => console.log('Action 1 on', item) }, { text: 'Action 2', event: (item) => console.log('Action 2 on', item), disabled: true }, ] } ]; } // Return an empty array for rows that should have no actions (e.g., row with id '2') return []; }; </script> `), }; const SummaryCellTemplateCode = ` <template> <bento-data-grid :columns="columns" :data="dataSet" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term="searchTerm" :filter-search-config="searchConfig" @update:filter-values="filterValues = $event" @update:filter-search-term="searchTerm = $event" @filter-search-clear="onSearchCleared" > <template #item-company="{item}"> <bento-data-grid-summary-cell :label="\`\${item.company.length} merchants\`" :items="item.company" :search-term="companyColumnSearchTerm" /> </template> </bento-data-grid> </template> <script setup lang="ts"> import { BentoColumn, BentoDataGrid, BentoDataGridSummaryCell, BentoDatagridData, BentoFilterBarModel, BentoFilterBarSearchBarProps, BentoFilterItemType, BentoFilterValues, BentoCountryCode, } from '@adyen/bento-vue2'; import { computed, ref } from 'vue'; const columns: Array<BentoColumn> = [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ]; /** * Data set with array of companies */ const multiData: BentoDatagridData = [ { id: '03c0a799-1ced-4017-aea3-0cebb0106335', name: 'Shepherd Kane', payment: 160405, company: ['Merchant Helium', 'Merchant A'], }, { id: '8f9e7877-1ca2-4929-9217-f1ec78709291', name: 'Joyce Johns', payment: 78500, company: ['Merchant C', 'Merchant Hedra'], }, { id: '51d2b31d-d577-4fca-8329-a583babbbdda', name: 'Evans Zamora', payment: 1153.4, company: [ 'Merchant A', 'Merchant B', 'Merchant C', 'Merchant D', 'Merchant E', 'Merchant F', 'Merchant G', 'Merchant H', 'Merchant I', 'Merchant J', ], }, ]; const filtersConfig = computed<BentoFilterBarModel>(() => [ { field: 'name', label: 'Name', type: BentoFilterItemType.INPUT, }, { field: 'company', label: 'Company', type: BentoFilterItemType.INPUT, }, ]); /** * The values of applied filters. */ const filterValues = ref<BentoFilterValues>(); /** * The current global search term that is being searched with */ const searchTerm = ref(''); /** * Global search configuration */ const searchConfig = ref<BentoFilterBarSearchBarProps>({ inputFieldAriaLabel: 'Descriptive details about the search bar', debounceTime: 1000, // in ms placeholder: 'Find merchants', }); /** * Concatenated search term using the global search and the company filter value */ const companyColumnSearchTerm = computed( () => searchTerm.value || filterValues.value?.find(filter => filter.field === 'company')?.value?.toString() || '' ); /** * Simplified computed data set that filters data depending on applied filters and global search values */ const dataSet = computed<BentoDatagridData>(() => { let filteredData = multiData; // Apply global search term filter if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); filteredData = filteredData.filter( item => (item.name as string).toLowerCase().includes(lowerCaseSearchTerm) || (item.company as string[]).some(company => company.toLowerCase().includes(lowerCaseSearchTerm) ) ); } // Apply filterValues filter if (filterValues.value?.length) { filteredData = filteredData.filter(item => filterValues.value?.every(filter => { if (filter.field === 'company') { return filter.value ? (item[filter.field] as string[]).some(company => (company as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) ) : true; } return filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true; }) ); } return filteredData; }); const onSearchCleared = () => { searchTerm.value = ''; }; </script> `; export const SummaryCell: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, BentoDashedUnderline, BentoDataGridSummaryCell, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="dataSet" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term="searchTerm" :filter-search-config="searchConfig" @update:filter-values="filterValues = $event" @update:filter-search-term="searchTerm = $event" @filter-search-clear="onSearchCleared" > <template #item-company="{item}"> <bento-data-grid-summary-cell :label="\`\${item.company.length} merchants\`" :items="item.company" :search-term="companyColumnSearchTerm" /> </template> </bento-data-grid> `, setup(props) { /** * Data set with array of companies */ const multiData: BentoDatagridData = [ { id: '03c0a799-1ced-4017-aea3-0cebb0106335', name: 'Shepherd Kane', payment: 160405, company: ['Merchant Helium', 'Merchant A'], }, { id: 'b6d4f1fd-b1f6-40a6-a443-f785c89604ef', name: 'Santos Palmer', payment: 163500, company: ['Merchant B'], }, { id: '8f9e7877-1ca2-4929-9217-f1ec78709291', name: 'Joyce Johns', payment: 78500, company: ['Merchant C', 'Merchant Hedra'], }, { id: '0789a012-85a6-4657-87bb-319141938a61', name: 'Lynnette Melendez', payment: -45000, company: ['Merchant A', 'Merchant B', 'Merchant C'], }, { id: '0b3981b6-c73b-47ad-81d1-b06f9b95d467', name: 'Wagner Mcintosh', payment: 1112, company: ['Merchant A'], }, { id: '51d2b31d-d577-4fca-8329-a583babbbdda', name: 'Evans Zamora', payment: 1153.4, company: [ 'Merchant A', 'Merchant B', 'Merchant C', 'Merchant D', 'Merchant E', 'Merchant F', 'Merchant G', 'Merchant H', 'Merchant I', 'Merchant J', 'Merchant K', 'Merchant L', 'Merchant M', 'Merchant N', 'Merchant O', 'Merchant P', 'Merchant Q', 'Merchant R', 'Merchant S', 'Merchant T', ], }, { id: '9bd59a6e-bd03-4477-813f-b844953b7645', name: 'Anderson Middleton', payment: 181722.23, company: ['Merchant A'], }, ]; const filtersConfig = computed<BentoFilterBarModel>(() => [ { field: 'name', label: 'Name', type: BentoFilterItemType.INPUT, }, { field: 'company', label: 'Company', type: BentoFilterItemType.INPUT, }, ]); /** * The values of applied filters. */ const filterValues = ref<BentoFilterValues>(); /** * The current global search term that is being searched with */ const searchTerm = ref(''); /** * Global search configuration */ const searchConfig = ref<BentoFilterBarSearchBarProps>({ inputFieldAriaLabel: 'Descriptive details about the search bar', debounceTime: 1000, // in ms placeholder: 'Find merchants', }); /** * Concatenated search term using the global search and the company filter value */ const companyColumnSearchTerm = computed( () => searchTerm.value || filterValues.value?.find(filter => filter.field === 'company')?.value?.toString() || '' ); /** * Simplified computed data set that filters data depending on applied filters and global search values */ const dataSet = computed<BentoDatagridData>(() => { let filteredData = multiData; // Apply global search term filter if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); filteredData = filteredData.filter( item => (item.name as string).toLowerCase().includes(lowerCaseSearchTerm) || (item.company as Array<string>).some(company => company.toLowerCase().includes(lowerCaseSearchTerm) ) ); } // Apply filterValues filter if (filterValues.value?.length) { filteredData = filteredData.filter(item => filterValues.value?.every(filter => { if (filter.field === 'company') { return filter.value ? (item[filter.field] as Array<string>).some(company => (company as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) ) : true; } return filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true; }) ); } return filteredData; }); const onSearchCleared = () => { searchTerm.value = ''; }; return { args: isVue2 ? props : _args, filtersConfig, filterValues, searchTerm, searchConfig, onSearchCleared, dataSet, companyColumnSearchTerm, }; }, }), args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ], zebraRows: true, }, parameters: storybookDocsParameter(SummaryCellTemplateCode), }; export const TrendCell: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoDataGridTrendCell, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args"> <template #item-performance="{ item }"> <bento-data-grid-trend-cell :value="item.performance.value" :direction="item.performance.direction" :variant="item.performance.variant" /> </template> </bento-data-grid> `, setup(props) { const columns: Array<BentoColumn> = [ { field: 'name', label: 'Name', flex: 2 }, { field: 'performance', label: 'Performance', flex: 1, numeric: true }, ]; const data: BentoDatagridData = [ { id: '1', name: 'Shepherd Kane', performance: { value: 25.5, variant: 'green', direction: 'up', }, }, { id: '2', name: 'Santos Palmer', performance: { value: 10.2, variant: 'red', direction: 'down', }, }, { id: '3', name: 'Joyce Johns', performance: { value: 0, variant: 'black', direction: 'none', }, }, { id: '4', name: 'Lynnette Melendez', performance: { value: '3.1428571429%', variant: 'green', direction: 'down', }, }, { id: '5', name: 'Wagner Mcintosh', performance: { value: '15.2%', variant: 'red', direction: 'up', }, }, { id: '6', name: 'Evans Zamora', performance: { value: '1.23', direction: 'up', }, }, { id: '7', name: 'Anderson Middleton', performance: { value: '90', direction: 'down', }, }, ]; return { args: { ...(isVue2 ? props : _args), columns, data }, }; }, }), args: { ...DEFAULT_ARGS, zebraRows: true, }, parameters: storybookDocsParameter(DataGridTrendCellExample), };
@@ -0,0 +1 @@
1
+ <template> <div ref="dataGridRef" class="b-data-grid" data-testid="data-grid" :class="conditionalGridClasses" aria-live="polite" :aria-busy="loading" @copy="handleClipboard" > <div v-if="filters" ref="filtersRef" class="b-data-grid__filters" :class="conditionalFilterClasses" :style="{ top: computedStickyFiltersTopPosition }" :aria-controls="dataGridUid" :aria-label="t('filterBar')" role="region" > <bento-filter-bar v-if="filters" ref="filterBarRef" :config="filters" :container-size-layout="filtersContainerSizeLayout" :filter-values="filterValues" :filter-values-object="filterValuesObject" :search="filterSearchConfig" :search-term="filterSearchTerm" @search-input="onSearchTermInput" @search-clear="onSearchTermCleared" @input="filters => $emit(BentoDatagridEvent.UPDATE_FILTERS, filters)" @update:filter-values="filterValues => $emit(BentoDatagridEvent.UPDATE_FILTER_VALUES, filterValues)" @update:filter-values-object=" filterValueObject => $emit(BentoDatagridEvent.UPDATE_FILTER_VALUES_OBJECT, filterValueObject) " /> </div> <div v-if="$slots.header" class="b-data-grid__header"> <slot name="header"></slot> </div> <div :id="dataGridUid" :role="ariaGridRole" :aria-rowcount="data.length" :aria-colcount="columns.length" :aria-label="emptyState ? t('noResults') : null" class="b-data-grid__wrapper" :style="{ top: computedColumnHeadersTopPosition }" > <div class="b-data-grid__column-headers" :style="{ top: computedColumnHeadersTopPosition }"> <div ref="columnsRef" role="rowgroup" class="b-data-grid__columns-row" @scroll="updateScrollPosition" @keydown="navigationInput" > <div v-if="columnPanel" role="row"> <bento-data-grid-config-settings class="b-data-grid__config-settings" role="gridcell" :columns="localColumnsState" :condensed="condensed" :zebra-rows="internalZebraRows" @update:condensed="onConfigSettingsCondensedUpdate" @update:zebra-rows="onConfigSettingsZebraRowsUpdate" @update:columns="onConfigSettingsColumnsUpdate" /> </div> <div :style="gridRowWidthStyle" class="b-data-grid__columns" role="row"> <template v-for="({ columnSet, id, isFrozenColumns }, index) of columnSets"> <data-grid-columns v-if="columnSet?.length" :key="`data-grid-columns-${id}`" :ref="el => registerColumnHeaderRefs(el)" :columns="columnSet" :has-resizable-columns="hasResizableColumns" :sort-by="sortBy" :all-items-selected-state="allItemsSelectedState" :is-frozen-columns="isFrozenColumns" :show-frozen-columns-separator="showFrozenColumnsSeparator" :dragging-columns="draggingColumns" :has-draggable-columns="allowColumnDragAndDrop" :has-column-panel="columnPanel" :has-left-overflow="hasLeftOverflow" :has-first-column-panel-right-aligned="index === computedColumnPanelAlignment" :hovered-over-column-state="hoveredOverColumnState" :is-measuring-column-widths="isMeasuringColumnWidths" :is-only-one-visible-column="isOnlyOneVisibleColumn" :selectable="data?.length > 0 && selectable" :selectable-across-pages="selectableAcrossPagesComputed" :selected-items="selectedItems" :visible-rows-state="visibleRowsState" :selection-state="selectionState" :truncate-column-headers="truncateColumnHeaders" :row-actions-maximum-width="rowActionsMaxWidth" v-on="draggingEventsForColumns" @column-hover="hoveredOverColumn = $event" @column-move="onMoveColumnByOffset" @column-update="onConfigSettingsColumnUpdate" @column-resize="onColumnResized" @column-resizing="onColumnWidthOverride" @select-all-rows="onAllRowSelect" @select-all-across-pages="emit(BentoDatagridEvent.SELECT_ALL_ACROSS_PAGES)" @select-all-current-results="emit(BentoDatagridEvent.SELECT_ALL_CURRENT_RESULTS)" @deselect-all-current-results="emit(BentoDatagridEvent.DESELECT_ALL_CURRENT_RESULTS)" @deselect-all="onDeselectAll" @sort="onSort" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-columns> </template> </div> </div> </div> <div ref="body" data-testid="data-grid-body" class="b-data-grid__body" :class="conditionalGridBodyClasses" @scroll="updateScrollPosition" > <div v-if="loading && !isLazyLoad" class="b-data-grid__loading"> <bento-loading-indicator class="b-data-grid__loading-indicator" large /> <bento-typography el="span" variant="caption">{{ t('gettingYourData') }}</bento-typography> </div> <div v-else role="rowgroup" @keydown="navigationInput"> <template v-if="data?.length === 0 && emptyState"> <div role="row" tabindex="-1"> <div role="gridcell" :aria-colspan="columns.length" tabindex="-1"> <bento-empty-state class="b-data-grid__empty-state" v-bind="limitedEmptyStateProps" /> </div> </div> </template> <data-grid-row v-for="(rowData, rowIndex) in data" v-else :key="rowIndex" :ref="el => registerRowCellRefs(el, rowIndex)" :style="gridRowWidthStyle" :allow-row-clicks="allowRowClicks" :column-sets="columnSets" :condensed="condensed" :dragging-columns="draggingColumns" :dragging-events-for-row-cell="draggingEventsForRowCell" :get-data-item-id="getDataItemId" :has-nested-content="hasNestedContent" :has-nested-rows="hasNestedRows" :has-left-overflow="hasLeftOverflow" :has-right-overflow="hasRightOverflow" :highlighted-rows="highlightedRows" :hovered-row-index="hoveredRowIndex" :hovered-over-column-state="hoveredOverColumnState" :is-data-item-disabled="isDataItemDisabled" :is-row-even="internalZebraRows ? rowIndex % 2 !== 0 : false" :is-row-last-child="rowIndex + 1 === data?.length" :is-measuring-column-widths="isMeasuringColumnWidths" :row-actions-maximum-width="rowActionsMaxWidth" :row-actions="rowActions" :row-data="rowData" :row-index="rowIndex" :row-set-size="data?.length" :show-frozen-columns-separator="showFrozenColumnsSeparator" :should-show-nested-content="shouldShowNestedContent" :should-show-nested-rows="shouldShowNestedRows" :selectable="selectable" :selected-items="selectedItems" @cell-hovered="onCellHover" @cell-focus="emit(BentoDatagridEvent.CELL_FOCUS, $event)" @nested-content-toggled="emit(BentoDatagridEvent.NESTED_CONTENT_TOGGLED, $event)" @row-click="onRowClick" @on-frozen-columns-key-press="updateScrollPositionOnFrozenColumnsNavigation" @select-row="onRowSelect" @select-nested-row="onNestedRowSelect" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-row> </div> </div> </div> <data-grid-lazy-load v-if="isLazyLoad" :loading="loading" :is-infinite-scroll="isInfiniteScroll" @update:show-more="emitLazyLoadEvent" /> <div v-if="stickyHorizontalScrollbar" ref="virtualHorizontalScrollbarRef" class="b-data-grid__horizontal-scrollbar" aria-hidden="true" data-testid="sticky-horizontal-scrollbar" :style="horizontalScrollbarConditionalStyles" @scroll="syncVirtualScrollerWithBody" > <div :style="{ width: `${totalWidthOfAllColumns}px`, height: '15px' }"></div> </div> <div v-if="pagination && !isLazyLoad" ref="paginationContainerRef" data-testid="pagination-container" class="b-data-grid__pagination" :class="conditionalPaginationClasses" > <bento-pagination v-bind="pagination" :aria-label="$t('pagination')" :aria-controls="dataGridUid" @navigate="onPaginationNavigate" @items-page="onItemsPerPageChange" > <slot v-if="hasSlot('pagination-slot')" name="pagination-slot"></slot> </bento-pagination> </div> <div v-if="$slots.footer" class="b-data-grid__footer"><slot name="footer"></slot></div> <bento-action-bar v-if="showActionBar" :actions="bulkActions" :item-counter="itemsSelectedCounter" :extra-bottom-spacing="stickyPagination" @close="onDeselectAll" /> </div> </template> <script lang="ts" setup> import { type ComponentPublicInstance, computed, inject, onMounted, type PropType, provide, ref, toRef, toRefs, useSlots, } from 'vue'; // Components import { BentoActionBar } from '@/components/action-bar'; import { BentoPagination } from '@/components/pagination'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoTypography } from '@/components/typography'; import { BentoDataGridConfigSettings, DataGridColumns, DataGridLazyLoad, DataGridRow } from './components'; // Composables import { DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME, useCalculateColumnWidth, useColumnHovering, useColumnReorderer, useConfigSettings, useKeyboardNavigator, useLazyLoading, useScroller, useSelector, } from './composables'; import { useCalculateStickyElementHeight, useHasSlot } from '@/composables'; // Utils import { generateUid } from '@/core/utils/ts/generate-uid'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { handleClipboard } from '@/components/data-grid/utils/handle-clipboard/handle-clipboard'; // Types import { type BentoColumn, type BentoDatagridData, type BentoDatagridDataItem, type BentoDatagridDataItemId, type BentoDatagridEmptyStateProps, BentoDatagridEvent, type BentoDatagridGetDataItemIdFn, type BentoDatagridIsDataItemDisabledFn, type BentoDatagridPaginationProps, type BentoDatagridShouldShowNestedContentFn, type BentoDatagridShouldShowNestedRowsFn, type BentoDatagridSortByColumn, BentoDatagridStyleState, type BentoDataGridVisibleRowsState, } from './data-grid.types'; import { type BentoTabProvidedTitle } from '../tabs/components/tab.types'; import { type BentoDataGridTemplateDataGrid } from '@/templates/data-grid-template/data-grid-template.types'; import { type BentoLazyLoadType } from '@/types/lazy-load'; import type { BentoButtonActionsList } from '@/components/button'; import { BentoFilterBar, type BentoFilterBarContainerSizeLayout, type BentoFilterBarModel, type BentoFilterBarSearchBarProps, type BentoFilterValueObject, } 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: BentoFilterBar.props.filterValues.type, 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: BentoFilterBar.props.searchTerm.type, 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>>(); 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 behaviour == */ // 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 behaviour == */ // Identifies if the data-grid is rendered on a touch device const isTouchDevice = ref(false); onMounted(() => { if (typeof window !== 'undefined') { isTouchDevice.value = 'ontouchstart' in window || navigator?.maxTouchPoints > 0; } }); provide(DATA_GRID_TOUCH_DEVICE, isTouchDevice); /* == Data-grid template == */ const registerDatagridInDataGridTemplate: BentoDataGridTemplateDataGrid = inject( DATA_GRID_REGISTER_TEMPLATE_INJECTION_KEY, () => ({ additionalContentSlotRef: ref(null), isDataGridTemplate: false, stickyAditionalContent: ref(false), stickyTabs: ref(false), tabsRef: ref(null), }) ); const { additionalContentSlotRef, isDataGridTemplate, stickyTabs, tabsRef, stickyAdditionalContent } = registerDatagridInDataGridTemplate(); /* == Composable logic == */ // Column width calculation const { columnDefs, onColumnWidthOverride, gridRowWidthStyle, isMeasuringColumnWidths, isTotalWidthOfColumnsTakingUpAllAvailableGridWidth, columns: localColumnsState, totalWidthOfAllColumns, rowActionsMaxWidth, } = useCalculateColumnWidth(columns, data, body, columnHeaderRefs, gridcellRefs, { hasColumnPanel: columnPanel, hasSelectableColumn: selectable, hasSelectableAcrossPages: selectableAcrossPages, hasNestedContent, hasNestedRows, allowColumnDragAndDrop, isDataGridTemplate: isDataGridTemplate, }); /* == Emitting logic == */ const definedEmitsFunction = defineEmits([ BentoDatagridEvent.COLUMNS, BentoDatagridEvent.COLUMN_RESIZE, BentoDatagridEvent.ITEMS_PAGE, BentoDatagridEvent.FILTER_SEARCH_INPUT, BentoDatagridEvent.FILTER_SEARCH_CLEAR, BentoDatagridEvent.NAVIGATE, BentoDatagridEvent.NESTED_CONTENT_TOGGLED, BentoDatagridEvent.UPDATE_COLUMNS, BentoDatagridEvent.UPDATE_CONDENSED, BentoDatagridEvent.UPDATE_FILTER_VALUES, BentoDatagridEvent.UPDATE_FILTER_VALUES_OBJECT, BentoDatagridEvent.UPDATE_FILTERS, BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM, BentoDatagridEvent.UPDATE_SHOW_MORE, BentoDatagridEvent.UPDATE_PAGINATION, BentoDatagridEvent.UPDATE_SELECTION, BentoDatagridEvent.UPDATE_SORT_BY, BentoDatagridEvent.ROW_CLICK, BentoDatagridEvent.SELECT, BentoDatagridEvent.SELECT_ALL_ACROSS_PAGES, BentoDatagridEvent.SELECT_ALL_CURRENT_RESULTS, BentoDatagridEvent.DESELECT_ALL_CURRENT_RESULTS, BentoDatagridEvent.SORT, BentoDatagridEvent.CELL_FOCUS, ]); /** * Hooks into the emit function and updates the local columns state if we're updating columns. * This is so if users use a computed ref with the columns property the ordering / toggling visibility will still function correctly. */ const emit: typeof definedEmitsFunction = (event, ...args) => { // If we updating columns update the local columns state if (event === BentoDatagridEvent.COLUMNS || event === BentoDatagridEvent.UPDATE_COLUMNS) { [localColumnsState.value] = args; } definedEmitsFunction(event, ...args); }; // Calculate dynamic top position of sticky filters const stickyFiltersHeight = useCalculateStickyElementHeight( tabsRef, stickyTabs, stickyAdditionalContent, additionalContentSlotRef ); const computedStickyFiltersTopPosition = computed(() => props.stickyFilters ? `${stickyFiltersHeight.value}px` : null ); // Calculate dynamic top position of column headers const columnHeadersHeight = useCalculateStickyElementHeight(filtersRef, toRef(props, 'stickyFilters')); const computedColumnHeadersTopPosition = computed( () => `${columnHeadersHeight.value + stickyFiltersHeight.value}px` ); // Config Settings (condensed, visbility and freezing settings) const { sortedColumns, columnSets, isOnlyOneVisibleColumn, internalZebraRows, onConfigSettingsColumnsUpdate, onConfigSettingsColumnUpdate, onConfigSettingsCondensedUpdate, onConfigSettingsZebraRowsUpdate, } = useConfigSettings(columnDefs, zebraRows, emit); // Column hovering const { hoveredOverColumn, hoveredOverColumnState } = useColumnHovering( columnSets, selectable, outline, isTotalWidthOfColumnsTakingUpAllAvailableGridWidth ); // Re-ordering column (dragging) const { draggingColumns, draggingEventsForColumns, draggingEventsForRowCell, onMoveColumnByOffset } = useColumnReorderer(sortedColumns, allowColumnDragAndDrop, emit); // Selecting const selectableAcrossPagesComputed = computed( () => props.data?.length > 0 && !!props.selectableAcrossPages && !!props.pagination && props.pagination.totalCount > 0 ); const { onAllRowSelect, onRowSelect, onNestedRowSelect, onDeselectAll, allItemsSelectedState, selectedItems, selectionState, } = useSelector( selection, emit, data, props.getDataItemId, props.isDataItemDisabled, selectableAcrossPagesComputed, pagination ); const visibleRowsState = computed<BentoDataGridVisibleRowsState>(() => ({ filtered: filterBarRef.value?.isClearAllVisible || filterBarRef.value?.isResetAllVisible || !!props.filterSearchTerm, allRowsVisible: pagination.value?.size >= pagination.value?.totalCount, })); // Keyboard navigation const { navigationInput } = useKeyboardNavigator( gridcellRefs, columnHeaderRefs, sortedColumns, selectable, computed(() => props.rowActions !== undefined), columnPanel ); // Lazy loading const { isLazyLoad, isInfiniteScroll, verticalScrollPosition, emitLazyLoadEvent } = useLazyLoading( lazyLoadType, verticalScrollPositionRef, body, loading, emit ); // Scrolling const { updateScrollPosition, updateScrollPositionOnFrozenColumnsNavigation, syncVirtualScrollerWithBody, hasLeftOverflow, hasRightOverflow, horizontalScrollbarConditionalStyles, } = useScroller( body, columnsRef, verticalScrollPosition, virtualHorizontalScrollbarRef, paginationContainerRef, totalWidthOfAllColumns, { isLazyLoad, pagination, stickyPagination, isDataGridTemplate, } ); /* == Empty State == */ const onEmptyStateClearResetButtonClick = () => { if (filterBarRef.value.isClearAllVisible) { filterBarRef.value.clearAllFilters(); } else if (filterBarRef.value.isResetAllVisible) { filterBarRef.value.resetAllFilters(); } }; const limitedEmptyStateProps = computed<BentoDatagridEmptyStateProps>(() => ({ ...props.emptyState, variant: props.emptyState?.variant === 'embedded' || props.emptyState?.variant === 'basic' ? props.emptyState.variant : 'basic', })); /* == Styles == */ // Conditional grid classes const conditionalGridClasses = computed(() => ({ [`b-data-grid--${BentoDatagridStyleState.OUTLINE}`]: props.outline, [`b-data-grid--${BentoDatagridStyleState.SCROLL}`]: props.fitContent || isLazyLoad.value, [`b-data-grid--${BentoDatagridStyleState.STICKY_FILTERS}`]: props.filters && props.stickyFilters, [`b-data-grid--${BentoDatagridStyleState.OVERFLOW_SHADOW_LEFT}`]: hasLeftOverflow.value && !frozenColumnsLength.value, [`b-data-grid--${BentoDatagridStyleState.OVERFLOW_SHADOW_RIGHT}`]: hasRightOverflow.value && props.rowActions === undefined, [`b-data-grid--${BentoDatagridStyleState.NO_SPACING_TOP}`]: props.noSpacingTop, [`b-data-grid--${BentoDatagridStyleState.FIT_CONTENT}`]: props.fitContent, [`b-data-grid--${BentoDatagridStyleState.ZEBRA_ROWS}`]: internalZebraRows.value, 'b-data-grid--lazy-load': isLazyLoad.value, })); // Conditional grid body classes const conditionalGridBodyClasses = computed(() => ({ 'b-data-grid__body--sticky-horizontal-scrollbar': props.stickyHorizontalScrollbar, })); const computedColumnPanelAlignment = computed(() => columnSets.value.findIndex(el => el.columnSet.length)); const showFrozenColumnsSeparator = computed(() => { const isSelectionOnlyFrozenColumn = (props.selectable || props.hasNestedContent || props.hasNestedRows) && frozenColumnsLength.value === 1; return !isSelectionOnlyFrozenColumn || hasLeftOverflow.value; }); // Conditional filter classes const conditionalFilterClasses = computed(() => ({ 'b-data-grid__filters--sticky': !!props.stickyFilters || isDataGridTemplate, })); // Conditional pagination classes const conditionalPaginationClasses = computed(() => ({ 'b-data-grid__pagination--padding-left-unset': props.columns?.[0]?.padding?.left === false, 'b-data-grid__pagination--padding-right-unset': props.columns?.[props.columns.length - 1]?.padding?.right === false, 'b-data-grid__pagination--sticky': !!props.stickyPagination || isDataGridTemplate, })); /* == Ref registration methods == */ const registerColumnHeaderRefs = (el: Element | ComponentPublicInstance) => { // We always know this will be used on a DataGridColumns component so we can cast it const datagridColumnsComponent = el as InstanceType<typeof DataGridColumns>; if (el !== null && datagridColumnsComponent?.columnHeaderRefs) { Object.entries(datagridColumnsComponent?.columnHeaderRefs).forEach(([fieldName, colHeaderEl]) => { columnHeaderRefs.value[fieldName] = colHeaderEl as HTMLDivElement; }); } }; const registerRowCellRefs = (el: Element | ComponentPublicInstance, rowIndex: number) => { // We always know this will be used on a DataGridCell component so we can cast it const gridRowComponent = el as InstanceType<typeof DataGridRow>; if (el !== null) { if (gridRowComponent?.gridcellRefs) { const areRowActions = props.rowActions !== undefined && gridRowComponent?.rowActionsRef; Object.entries(gridRowComponent?.gridcellRefs).map(([field, cell]) => { if (gridcellRefs.value?.[field] === undefined) { gridcellRefs.value = { ...gridcellRefs.value, [field]: [] }; } gridcellRefs.value[field][rowIndex] = cell as HTMLDivElement; }); // This calculates the maxium width of the row actions column so we can add the same padding to the columns keep everything aligned if (areRowActions) { if (gridcellRefs.value?.[DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME] === undefined) { gridcellRefs.value = { ...gridcellRefs.value, [DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME]: [] }; } gridcellRefs.value[DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME][rowIndex] = gridRowComponent .rowActionsRef?.[0] as HTMLDivElement; rowActionsMaxWidth.value = Math.max( rowActionsMaxWidth.value, gridRowComponent.rowActionsRef?.[0]?.offsetWidth ?? 0 ); } } } }; /* == Row methods == */ // Row selecting const onRowClick = (row: BentoDatagridDataItem, event: MouseEvent) => { if (props.allowRowClicks) { emit(BentoDatagridEvent.ROW_CLICK, row, event); } }; // Programmatically set the hover state on a row const onCellHover = (rowIndex: number) => { hoveredRowIndex.value = rowIndex; }; /* == a11y == */ const ariaGridRole = computed(() => (props.hasNestedContent || props.hasNestedRows ? 'treegrid' : 'grid')); /* == Action bar == */ const itemsSelectedCounter = computed(() => selectedItems.value?.length ?? 0); const showActionBar = computed(() => props.bulkActions && itemsSelectedCounter.value > 0); /* == Emits & Events == */ // Resizing const onColumnResized = (resizeEvent: { field: string; width: number }) => emit(BentoDatagridEvent.COLUMN_RESIZE, resizeEvent); // Sorting const onSort = (updatedSortBy: Array<BentoDatagridSortByColumn>) => { emit(BentoDatagridEvent.UPDATE_SORT_BY, updatedSortBy); emit(BentoDatagridEvent.SORT, updatedSortBy); }; // Pagination const onPaginationNavigate = (page: number, size: number) => { emit(BentoDatagridEvent.NAVIGATE, page); emit(BentoDatagridEvent.UPDATE_PAGINATION, { ...props.pagination, size, page, }); }; const onItemsPerPageChange = (itemsPerPage: number) => { emit(BentoDatagridEvent.ITEMS_PAGE, itemsPerPage); }; // Filter bar const onSearchTermCleared = () => { emit(BentoDatagridEvent.FILTER_SEARCH_CLEAR); }; const onSearchTermInput = newSearchTerm => { emit(BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM, newSearchTerm); emit(BentoDatagridEvent.FILTER_SEARCH_INPUT, newSearchTerm); }; /* == Warnings == */ const hasMandatoryColumn = computed(() => columns.value.some(column => column.mandatory)); if (!hasMandatoryColumn.value) { printDevelopmentWarning( 'WARNING: At least one BentoDataGrid column should be set as mandatory to ensure data is always displayed correctly.' ); } /* == Provide/inject == */ const setTitle: BentoTabProvidedTitle = inject(EXTERNAL_TITLE_REGISTER_ENTRY_INJECTION_KEY, () => null); setTitle(props.tabTitle); provide(DATA_GRID_SLASHED_ZERO, toRef(props, 'slashedZero')); /* == Expose methods == */ defineExpose({ onEmptyStateClearResetButtonClick }); </script> <script lang="ts"> /** * A component to display data in columns and row format with a whole array of configuration for visualization and interaction. * * @example * import { BentoDataGrid } from '@adyen/bento-vue2'; * * export default { * components: { BentoDataGrid }, * template: ` * <bento-data-grid * :data="[ * { id: '123', name: 'John', company: 'TestCompany', 'payment': 200 } * { id: '321', name: 'Jane', company: 'Adyen', 'payment': 300 } * ]" * :columns="[ * { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, * { field: 'company', label: 'Reference' }, * { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, * ]" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-data-grid', }; </script> <style lang="scss" scoped src="./data-grid.scss" />
@@ -0,0 +1,49 @@
1
+ import { Canvas, Meta } from '@storybook/blocks';
2
+ import * as DateStories from './date.stories';
3
+
4
+ <Meta of={DateStories} />
5
+
6
+ # Date
7
+
8
+ A `bento-date` component is used to display a formatted date value
9
+
10
+ <Canvas of={DateStories.Default} />
11
+
12
+ ## Use Cases
13
+
14
+ Use `bento-date` component for:
15
+
16
+ - displaying date or time information in data-grid
17
+
18
+ ## Variations
19
+
20
+ Based on provided `format` property, the displayed result may be one of the following options passed as a string:
21
+
22
+ | Format Value | Result examples |
23
+ | ------------------------------------------------------- | ------------------------------ |
24
+ | `LONG_WEEKDAY` | Friday |
25
+ | `SHORT_TIME_WITH_PERIOD` | 2:44 PM |
26
+ | `SHORT_TIME_WITHOUT_PERIOD` | 14:45 |
27
+ | `EXACT_TIME_WITH_PERIOD` | 2:05:11 PM |
28
+ | `EXACT_TIME_WITHOUT_PERIOD` | 14:05:11 |
29
+ | `LONG_DATE` | June 30, 2023 |
30
+ | `SHORT_DATE` | Jun 30, 2023 |
31
+ | `FULL_DATE_EXACT_TIME_WITH_PERIOD` | Jun 30, 2023, 2:54:10 PM |
32
+ | `FULL_DATE_EXACT_TIME_WITHOUT_PERIOD` | Jun 30, 2023, 14:54:42 |
33
+ | `FULL_DATE_EXACT_TIME_WITH_TIMEZONE_AND_PERIOD` | Jun 30, 2023, 2:56:07 PM GMT+2 |
34
+ | `FULL_DATE_EXACT_TIME_WITH_TIMEZONE_AND_WITHOUT_PERIOD` | Jun 30, 2023, 14:56:28 GMT+2 |
35
+ | `FULL_DATE_TIME_WITH_PERIOD` | Jun 30, 2023, 2:48 PM |
36
+ | `FULL_DATE_TIME_WITH_TIMEZONE_AND_PERIOD` | Jun 30, 2023, 2:44 PM GMT+2 |
37
+ | `FULL_DATE_TIME_WITHOUT_PERIOD` | Jun 30, 2023, 14:48 |
38
+ | `FULL_DATE_TIME_WITH_TIMEZONE_AND_WITHOUT_PERIOD` | Jun 30, 2023, 14:43 GMT+2 |
39
+
40
+ ## Accessibility
41
+
42
+ ### Keyboard Interaction
43
+
44
+ The `bento-date` component is not reachable through keyboard navigation since it's a non-interactive element that
45
+ displays a readonly date information
46
+
47
+ ## Resources
48
+
49
+ - [Figma link](https://www.figma.com/file/uLabwF3243jdMDsNSP7U9I/Bento---Components?type=design&node-id=2882-78027&mode=design&t=RCr4nN4DHKfugcNX-0)
@@ -0,0 +1 @@
1
+ import { BentoDateFormat } from '@/components/date'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { type Meta, type StoryObj } from '@storybook/vue'; import { isVue2 } from 'vue-demi'; import BentoDate from './date.vue'; const DateFormatStorybookLabels = Object.keys(BentoDateFormat).reduce((result, key) => { // eslint-disable-next-line no-param-reassign result[key] = BentoDateFormat[key as keyof typeof BentoDateFormat]; return result; }, {} as { [key: string]: string }); export default { title: 'Date', component: BentoDate, argTypes: { format: { options: DateFormatStorybookLabels, mapping: BentoDateFormat, control: { type: 'select', }, }, }, } as Meta; type Story = StoryObj<typeof BentoDate>; const DEFAULT_DATE_PROPS = { date: new Date(), }; const defaultCode = ` <bento-date :date="dateObject" :format="BentoDateFormat.LONG_DATE"/> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDate }, props: Object.keys(argTypes), template: ` <bento-date v-bind="args"/> `, setup(props) { return { args: isVue2 ? props : _args, }; }, }), args: { ...DEFAULT_DATE_PROPS, }, parameters: storybookDocsParameter(defaultCode), };
@@ -0,0 +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" />
@@ -0,0 +1,69 @@
1
+ import { Canvas, Story, Meta } from '@storybook/blocks';
2
+ import * as DatePickerStories from './date-picker.stories';
3
+
4
+ <Meta of={DatePickerStories} />
5
+
6
+ # Date picker
7
+
8
+ A date picker is a type of dropdown that allows users to select either a singular date or a date range in a visual way.
9
+
10
+ <Canvas withSource="closed">
11
+ <Story of={DatePickerStories.Default} />
12
+ </Canvas>
13
+
14
+ ## Use Cases
15
+
16
+ Use a DatePicker if:
17
+
18
+ - You want the user to input a custom date
19
+
20
+ ### Do not use
21
+
22
+ Don't use the `bento-date-picker` if:
23
+
24
+ - You want to display a date. Use `bento-date` instead.
25
+
26
+ ## Variations
27
+
28
+ ### Singular calendar
29
+
30
+ Single date calendar. Only shows one calendar and allows for a single date to be selected or typed in when the date
31
+ picker is opened.
32
+
33
+ ### Range calendar
34
+
35
+ Range of dates calendar. Can show multiple calendars at the same time and allows to select a range of dates either
36
+ through the form or by clicking over the desired range.
37
+
38
+ ### Month variant
39
+
40
+ A calendar by month/year instead of day/month. Useful for full month selection, where days are trivial.
41
+
42
+ ## Modifiers
43
+
44
+ ### Default display month
45
+
46
+ This is used to show a default date when the calendar popup is opened and no date is currently selected, otherwise the
47
+ default is current month. The `defaultDisplayMonth` prop accepts an ISO 8601 format i.e `2021-12-05` or `YYYY-MM-DD`.
48
+ The following format i.e `2021-12` or `YYYY-MM` is also accepted.
49
+
50
+ ### Error Message
51
+
52
+ Renders the date picker in an error state and displays the error message beneath the date input field.
53
+
54
+ ### Tooltip
55
+
56
+ A tooltip with extra information for the field can be rendered by passing the desired information to the `tooltipText`
57
+ prop.
58
+
59
+ ### Roles, states and properties
60
+
61
+ | ARIA attribute | is automatic? | fallback used | manual override |
62
+ | -------------- | ------------------------------------- | ------------------------------------------ | --------------- |
63
+ | label | Yes, label property | If no label then a generate label is added | - |
64
+ | role | Yes, `combobox` and `dialog` are used | - | - |
65
+ | describedby | Yes, `description` and `error`s | - | - |
66
+
67
+ ## Resources
68
+
69
+ - [Figma link](https://www.figma.com/file/uLabwF3243jdMDsNSP7U9I/Bento---Components?node-id=5046%3A73755)
@@ -0,0 +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" /> `), };
@@ -0,0 +1 @@
1
+ <!-- TODO: Remove aria props as these are part of JSX elements /vue/types/jsx.d.ts --> <!-- eslint-disable vue/attribute-hyphenation --> <template> <div v-bento-click-outside-directive="closeDatePicker" class="b-date-picker" @keydown.space.capture="onSpaceBarOrEnter" @keydown.enter.capture="onSpaceBarOrEnter" > <!-- Label --> <field-label v-if="label" :id="labelId" :label="label" :tooltip-text="tooltipText" :optional="optional" :required="required" /> <!-- Input --> <div> <dropdown-input-default ref="inputContainerRef" always-combobox-is-input :ariaControls="datePickerContainerId" :ariaExpanded="isDatePickerOpen" :aria-labelledby="label ? labelId : null" :aria-describedby="ariaDescribedBy" :disabled="disabled" dynamic-filtering :is-invalid="!!errorMessage || !!dateErrorText" :open="isDatePickerOpen" :value="textInputValue" :display-value="dateDisplayValue" :debounce-time="0" :readonly="isReadOnly" @input="onDateInputChange" @open="openDatePicker" @keydown="onDateKeyDown" @clear="clearDatePickerValue" @close="closeDatePicker" /> </div> <!-- Error Messages --> <error-message v-if="dateErrorText" :id="dateErrorId" :error-message="dateErrorText" class="b-date-picker__error-message" /> <error-message v-if="errorMessage" :id="errorId" :error-message="errorMessage" class="b-date-picker__error-message" /> <!-- Description --> <bento-typography v-if="description" class="b-date-picker__description" :class="descriptionConditionalClasses" el="span" > {{ description }} </bento-typography> <!-- Calendar container --> <bento-popover v-if="inputContainerRef" :id="datePickerContainerId" class="b-date-picker__container" role="dialog" :open="isDatePickerOpen" :target-element="inputContainerRef" :aria-label="popoverAriaLabel" :aria-modal="true" position="bottom-start" fit-content trap-all :trap-all-options="computedTrapAllOptions" @keydown.esc.native="closeDatePicker" > <calendar ref="calendarContainerRef" :default-display-month="defaultDisplayMonth" :first-day-of-week="firstDayOfWeek" :is-date-disabled="isDateDisabled" :number-of-months="1" :value="internalModelValue" :min="min" :max="max" :variant="variant" @input="onDateSelected" /> </bento-popover> </div> </template> <script setup lang="ts"> import { computed, nextTick, ref, toRef, watch } from 'vue'; import { startOfMonth } from 'date-fns/startOfMonth'; // Components import { BentoPopover } from '@/components/popover'; import { BentoTypography } from '../typography'; import { Calendar, type CalendarSingleDateValue, ErrorMessage, FieldLabel } from '@/internal'; import { DropdownInputDefault } from '@/components/dropdown/components'; // Utils import { debounce } from '@/utils/ts/debounce'; import { formatDateInputValue, validateDateInput } from '@/utils/ts/date-input'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { deprecate } from '@/utils/ts/deprecate'; // Composables import { useDatePickerSingleCalendarText } from './composables/use-date-picker-single-calendar-text'; import { useFormLayoutFieldLoading } from '@/composables'; // Directive import { BentoClickOutsideDirective as vBentoClickOutsideDirective } from '@/directives'; // Types import { type BentoDatePickerEmits, type BentoDatePickerProps } from './date-picker.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const DEBOUNCE_TIME = 1000; const props = withDefaults(defineProps<BentoDatePickerProps>(), { defaultDisplayMonth: null, description: undefined, disabled: false, errorMessage: null, firstDayOfWeek: 1, // Monday isDateDisabled: undefined, label: undefined, max: null, min: null, modelValue: null, optional: false, placeholder: null, readonly: false, required: false, tooltipText: null, value: null, variant: undefined, // Default will be set by @/internal/calendar }); const emit = defineEmits<BentoDatePickerEmits>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const datePickerContainerId = generateUid('bento-date-picker-container'); const labelId = generateUid('bento-date-picker-label'); const descriptionId = generateUid('bento-date-picker-description'); const errorId = generateUid('bento-date-picker-error'); const dateErrorId = generateUid('bento-date-picker-date-error'); const popoverAriaLabel = computed(() => props.label || t('datePicker', { name: datePickerContainerId })); // Refs const calendarContainerRef = ref(null); const inputContainerRef = ref(null); // Values const isDateIncorrect = ref(false); const isDatePickerOpen = ref(false); const initialFocusOnDatePickerOpen = ref(undefined); const internalModelValue = ref<CalendarSingleDateValue | null>(props.modelValue); // to be removed when `props.value` is removed const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const { emitValue } = useFormFieldEmits<CalendarSingleDateValue>(emit); const computedTrapAllOptions = computed(() => ({ initialFocus: initialFocusOnDatePickerOpen.value, additionalContainers: [inputContainerRef.value.$el], })); watch( () => props.disabled, disabled => { // Close date picker popover if it's opened when it's disabled if (disabled && isDatePickerOpen.value) { isDatePickerOpen.value = false; } } ); watch( [() => props.value, () => props.modelValue], ([newValue, newModelValue]) => { if (newValue) { deprecate( 'BentoDatePicker "value" property', `The use of "value" prop in "BentoDatePicker" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } internalModelValue.value = newModelValue ?? newValue; }, { immediate: true } ); const isMonthVariant = computed(() => props.variant === 'month'); const { dateDisplayValue, textInputValue, formatTextInputDisplayValue } = useDatePickerSingleCalendarText({ datePickerValue: internalModelValue, isDateIncorrect, isMonthVariant, placeholder: toRef(props, 'placeholder'), }); const dateErrorText = computed(() => { if ( props.isDateDisabled && textInputValue.value && props.isDateDisabled(new Date(textInputValue.value)) && !isDatePickerOpen.value ) { return t('selectedDateIsNotAvailable'); } return isDateIncorrect.value ? t('provideDateInAFormat') : null; }); const descriptionConditionalClasses = computed(() => ({ 'b-date-picker__description--error': props.errorMessage || dateErrorText.value, })); const ariaDescribedBy = computed( () => `${props.description ? descriptionId : ''} ${props.errorMessage ? errorId : ''} ${ dateErrorText.value ? dateErrorId : '' }`.trim() || null ); const isDateEnabled = (date: Date) => { // Check if date is between min/max bounds, if there are any if ((props.min && date <= props.min) || (props.max && date >= props.max)) { return false; } // Check if function is set, otherwise all dates are enabled if (props.isDateDisabled) { return !props.isDateDisabled(date); } return true; }; const closeDatePicker = () => { isDatePickerOpen.value = false; }; const openDatePicker = () => { isDatePickerOpen.value = true; formatTextInputDisplayValue(); }; const onDateSelected = (selectedDate: Date) => { // Reset the error state when a valid date is selected isDateIncorrect.value = false; closeDatePicker(); emitValue(selectedDate); }; const clearDatePickerValue = async () => { if (isDateIncorrect.value) { isDateIncorrect.value = false; textInputValue.value = null; } emitValue(null); }; const onDateKeyDown = (event: KeyboardEvent) => { validateDateInput(event); }; const validateDate = (stringDate: string) => { // Validate only if the date is complete if (stringDate.length < 10) { return; } // Checks for YYYY-MM-DD format const dateRegex = /^\d{4}-(0?[1-9]|1[012])-(0?[1-9]|[12][0-9]|3[01])$/; const isValidDate = dateRegex.test(stringDate) && isDateEnabled(new Date(stringDate)); if (isValidDate) { isDateIncorrect.value = false; emitValue(new Date(stringDate)); } else if (stringDate) { // Only set to true if the value exists (not null/undefined) isDateIncorrect.value = true; emitValue(null); } }; const validateDateDebounced = debounce(validateDate, DEBOUNCE_TIME); const onDateInputChange = (stringDate: string) => { textInputValue.value = formatDateInputValue(stringDate); // If we are closing the date picker, we should validate (and emit date inputs) immediately so the user doesn't have to wait for the debounce time. if (isDatePickerOpen.value) { validateDateDebounced(textInputValue.value); } else { validateDate(textInputValue.value); } }; /** * Space bar and Enter keyDown event handler. * * * Opens the date picker dialog on the first keydown. * * Move focus to selected date, i.e., the date displayed in the date input text field. If no date has been selected, places focus on the current date. * @param {KeyboardEvent} event Event being triggered */ const onSpaceBarOrEnter = async (event: KeyboardEvent) => { if (isDatePickerOpen.value) { return; } event.preventDefault(); openDatePicker(); // Wait for the date picker dialog to open await nextTick(); // Set current date (today) if no date is selected if (!internalModelValue.value) { // Format the opened date picker date to YYYY-MM-DD formatTextInputDisplayValue(new Date()); emitValue(new Date()); } // Set initial focus initialFocusOnDatePickerOpen.value = calendarContainerRef.value?.$refs.calendarRef[0].focusOnDate( props.variant === 'month' ? startOfMonth(internalModelValue.value) : internalModelValue.value, true ) ?? undefined; }; </script> <script lang="ts"> /** * Date picker selector. * * @example * import { BentoDatePicker } from '@adyen/bento-vue2'; * * export default { * components: { BentoDatePicker }, * template: ` * <bento-date-picker * label="Label" * description="Supporting text" * required * optional * disabled * @input="onDateChanged" * v-model="selectedDate" * /> * `, * setup() { * const selectedDate = ref(new Date()); // reactive({ startDate: new Date(), endDate: new Date() }) * return { * selectedDate, * } * } * } */ export default { i18n: { messages }, name: 'bento-date-picker', model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./date-picker.scss" />
@@ -0,0 +1 @@
1
+ <template> <div class="b-date-range-picker-calendar"> <!-- Form --> <div class="b-date-range-picker-calendar__form-container" :class="computedFormContainerClasses" data-testid="date-range-picker-calendar-form-container" > <div class="b-date-range-picker-calendar__form"> <template v-if="hasSlot('title')"> <slot name="title" /> </template> <bento-dropdown v-if="quickSelectRanges" :aria-label="t('customRange')" :items="quickSelectRangeDefaultItems" :model-value="selectedRangeSelectorValue" @update:model-value="onRangeSelectorInput" /> <bento-segmented-control v-if="granularities" :items="granularityItems" :model-value="internalRangeDate.granularity" full-width @update:model-value="onGranularityInput" > </bento-segmented-control> <div> <bento-typography stronger el="span" class="b-date-range-picker-calendar__label"> From </bento-typography> <div :class="formInputWrapperConditionalClasses"> <bento-input-field :aria-label="t('dateFrom')" :model-value="dateTextInput.startDate" :error="startDateError" class="b-date-range-picker-calendar__form-input" @update:model-value="onStartDateInput" @keydown="onDateInputKeyDown" > <template #description>YYYY-MM-DD</template> </bento-input-field> <bento-input-field v-if="allowTimeInput" :aria-label="t('timeFrom')" :model-value="dateTextInput.startTime" :error="startTimeError" class="b-date-range-picker-calendar__form-input" @update:model-value="onStartTimeInput" @keydown="onTimeInputKeyDown" > <template #description>HH:MM:SS</template> </bento-input-field> </div> </div> <div> <bento-typography stronger el="span" class="b-date-range-picker-calendar__label"> To </bento-typography> <div :class="formInputWrapperConditionalClasses"> <bento-input-field class="b-date-range-picker-calendar__form-input" :aria-label="t('dateTo')" :model-value="dateTextInput.endDate" :error="endDateError" @update:model-value="onEndDateInput" @keydown="onDateInputKeyDown" > <template #description>YYYY-MM-DD</template> </bento-input-field> <bento-input-field v-if="allowTimeInput" :aria-label="t('timeTo')" :model-value="dateTextInput.endTime" :error="endTimeError" class="b-date-range-picker-calendar__form-input" @update:model-value="onEndTimeInput" @keydown="onTimeInputKeyDown" > <template #description>HH:MM:SS</template> </bento-input-field> </div> </div> </div> <div v-if="hasSlot('actions')"> <slot name="actions" /> </div> </div> <!-- Calendar --> <div v-if="!hasNoCalendars" class="b-date-range-picker-calendar__calendars-container" data-testid="date-range-picker-calendar-calendar-container" > <calendar :value="internalRangeDate" :first-day-of-week="firstDayOfWeek" :is-date-disabled="isDateDisabled" :max-range="maxRange" :min="computedMinMax.min" :max="computedMinMax.max" :number-of-months="numberOfMonths" :show-end-date-on-open="showEndDateOnOpen" :granularity="selectedGranularity" :variant="variant" is-range @input="onDateInput" @start-date-selected="onStartDateSelected" @end-date-selected="onEndDateSelected" /> </div> </div> </template> <script setup lang="ts"> import { computed, onMounted, type PropType, reactive, ref, toRaw, useSlots } from 'vue'; import { Calendar, type CalendarGranularityType } from '@/internal'; import { BentoTypography } from '@/components/typography'; import { BentoInputField } from '@/components/input-field'; import BentoDropdown from '@/components/dropdown/dropdown.vue'; import { BentoSegmentedControl, type BentoSegmentedControlItem } from '@/components/segmented-control'; import { getDateLocale } from '@/utils/ts/get-date-locale/get-date-locale'; import { useI18n } from '@/utils/ts/i18n'; import { parseDateTextToStartOfDay } from '../../utils/date-text-to-start-of-day'; import { debounce } from '@/utils/ts/debounce'; import { useHasSlot } from '@/composables'; import { useGranularityAdjustments } from '@/components/internal/calendar/composables/use-granularity-adjustments'; import { useGranularMinMaxDate } from '@/components/internal/calendar/components/calendar-month/composables/granular-min-max-date'; import { useGranularityMemory } from '../../composables/use-granularity-memory'; import { formatDateInputValue, validateDateInput } from '@/utils/ts/date-input'; import { DateRangePickerCalendarEvent, type DateRangePickerCalendarFormData, type DateRangePickerCalendarRangeSelectorItems, } from './date-range-picker-calendar.types'; import type { BentoDateRangePickerGranularityConfig, BentoDateRangePickerValue, } from '../../date-range-picker.types'; import { DateFormat } from '@/types/date-format'; import { formatTimeInputValue, validateTimeInput } from '@/utils/ts/time-input'; import { endOfDay } from 'date-fns/endOfDay'; import { format } from 'date-fns/format'; import { setHours } from 'date-fns/setHours'; import { setMinutes } from 'date-fns/setMinutes'; import { setSeconds } from 'date-fns/setSeconds'; import { startOfDay } from 'date-fns/startOfDay'; import { isSameSecond } from 'date-fns/isSameSecond'; import { isSameDay } from 'date-fns/isSameDay'; import { isEqual } from 'date-fns/isEqual'; import { isToday } from 'date-fns/isToday'; import { setMilliseconds } from 'date-fns/setMilliseconds'; import messages from './messages.json'; const FORM_INPUT_DEBOUNCE_TIME = 300; const RANGE_SELECTOR_CUSTOM_RANGE_KEY = 'customRange'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * Allows user to enter time values in the form */ allowTimeInput: { type: Boolean, default: false }, /** * Ranges form persistance data */ dateFormData: { type: Object as PropType<DateRangePickerCalendarFormData>, default: () => ({ startDate: undefined, endDate: undefined, startTime: undefined, endTime: undefined }), }, /** * Allows you to set the first day of the week. 0 is sunday, 1 is monday, 6 is saturday */ firstDayOfWeek: Calendar.props.firstDayOfWeek, /** * A list of available granularities. * If present, the date picker will display a segmented control to change granularity. */ granularities: { type: Array as PropType<Array<BentoDateRangePickerGranularityConfig>>, default: null, }, /** * Indicate if a date should be disabled or not */ isDateDisabled: Calendar.props.isDateDisabled, /** * Set a maximum number of dates to be selectable by the range. */ maxRange: Calendar.props.maxRange, /** * Sets a minimum limit for selectable dates - dates before `min` will not be selectable */ min: Calendar.props.min, /** * Sets a maximum limit for selectable dates - dates after `max` will not be selectable */ max: Calendar.props.max, /** * Number of months rendered on pane */ numberOfMonths: { type: Calendar.props.numberOfMonths.type, default: Calendar.props.numberOfMonths.default, validator: (n: number) => n >= 0, }, /** * Displays the end date's month when the calendar is opened. Defaults to false i.e. the start date's month is displayed. */ showEndDateOnOpen: { type: Boolean, default: false }, /** * Enables the custom range selector. * If provided, must be an array that sets the custom range dropdown items. Items are of the structure: * `label` - label of custom range item. * `value` - a unique key of the custom range item. * `data` - an object `{ startDate: Date; endDate: Date }` to set the date picker range to upon selecting. */ quickSelectRanges: { type: Array as PropType<DateRangePickerCalendarRangeSelectorItems>, default: undefined, }, /** * Selected date */ value: { type: Object as PropType<BentoDateRangePickerValue>, default: undefined }, /** * The type of calendar to display. Defaults to showing days. */ variant: Calendar.props.variant, }); const emit = defineEmits([ DateRangePickerCalendarEvent.CUSTOM_RANGE, DateRangePickerCalendarEvent.INPUT, DateRangePickerCalendarEvent.ERROR, DateRangePickerCalendarEvent.FORM_DATE, DateRangePickerCalendarEvent.START_DATE_SELECTED, ]); const slots = useSlots(); const hasSlot = useHasSlot(slots); const { locale, t } = useI18n<{ message: MessageSchema }>({ messages }); const locales = getDateLocale(); const calculatedIsRelativeToNowQuickSelectRanges = ref({}); const defaultGranularity = computed(() => (props.variant === 'month' ? 'monthly' : 'daily')); const hasNoCalendars = computed(() => props.numberOfMonths === 0); const internalRangeDate = reactive<BentoDateRangePickerValue>({ startDate: props.value?.startDate, endDate: props.value?.endDate, range: props.value?.range, ...(props.granularities ? { granularity: props.value?.granularity || defaultGranularity.value, } : {}), }); /** * Deserializes a date to transform it to a UTC standard so it can be compared regardless of timezone. * src: https://stackoverflow.com/a/38050824 * @param date Date to deserialize/normalize for comparisson */ function deserializeDate(date: Date) { if (!date) { return null; } return new Date( date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds() ); } /** * Currently range selector dropdown item that is selected. * The logic will try and find in each range item: * - If an endDate does not exist, then only check if the startDate day matches and if today's day matches. * - if time input has been enabled, then check if the seconds match. * - if an endDate does exist then just check if the startDate day and the endDate day match. */ const selectedRangeSelectorValue = computed(() => { return ( // eslint-disable-next-line consistent-return props?.quickSelectRanges?.find(({ value, data }) => { if (!data.endDate) { // Check if this quick select range is explicitly marked as "relative to now" if (data.isRelativeToNow) { // Retrieve previously calculated start/end dates for this relative range. // We use these if the user has already clicked on this time of quick range const newlyCalculatedQuickSelectDates = calculatedIsRelativeToNowQuickSelectRanges.value?.[value]; return newlyCalculatedQuickSelectDates ? isEqual( deserializeDate(newlyCalculatedQuickSelectDates.startDate), deserializeDate(internalRangeDate.startDate) ) && isEqual( deserializeDate(newlyCalculatedQuickSelectDates.endDate), deserializeDate(internalRangeDate.endDate) ) : // If not stored calculated dates, fall back to comparing the quick select item's value // with the internal range's stored range identifier. value === internalRangeDate.range; } // This block handles quick select ranges that do not have an endDate // AND are NOT explicitly marked as 'isRelativeToNow'. // This means the endDate will be today at the end of the day return ( isEqual(deserializeDate(data.startDate), deserializeDate(internalRangeDate.startDate)) && isEqual( deserializeDate(internalRangeDate.endDate), deserializeDate(endOfDay(internalRangeDate.endDate)) ) && isToday(deserializeDate(internalRangeDate.endDate)) && (props.granularities ? data.granularity === internalRangeDate.granularity : true) ); } if (props.allowTimeInput) { const res = isSameSecond(deserializeDate(data.startDate), deserializeDate(internalRangeDate.startDate)) && isSameSecond(deserializeDate(data.endDate), deserializeDate(internalRangeDate.endDate)) && (props.granularities ? data.granularity === internalRangeDate.granularity : true); return res; } if (data.endDate) { return ( isSameDay(data.startDate, internalRangeDate.startDate) && isSameDay(data.endDate, internalRangeDate.endDate) && (props.granularities ? data.granularity === internalRangeDate.granularity : true) ); } })?.value ?? RANGE_SELECTOR_CUSTOM_RANGE_KEY ); }); const formInputWrapperConditionalClasses = computed(() => ({ 'b-date-range-picker-calendar__form-input-wrapper': props.allowTimeInput, })); const quickSelectRangeDefaultItems = computed(() => [ { label: t('customRange'), value: RANGE_SELECTOR_CUSTOM_RANGE_KEY }, ...(props.quickSelectRanges ? props.quickSelectRanges : []), ]); const selectedGranularity = computed( () => props.granularities?.find(({ type }) => type === internalRangeDate.granularity) || null ); const granularityItems = computed<Array<BentoSegmentedControlItem>>( () => props.granularities?.map(({ type, disabled }) => ({ label: t(type), value: type, disabled, })) ?? [] ); const { getGranularCalendarLimits } = useGranularMinMaxDate(props); const computedMinMax = computed(() => props.granularities ? getGranularCalendarLimits(selectedGranularity.value) : { min: props.min ? startOfDay(props.min) : null, max: props.max ? endOfDay(props.max) : null } ); const { adjustDateForQuarterly, adjustDateForMonthly, adjustDateForWeekly, adjustStartDateInputForWeekly, adjustEndDateInputForWeekly, adjustStartDateInputForMonthly, adjustEndDateInputForMonthly, adjustStartDateInputForQuarterly, adjustEndDateInputForQuarterly, } = useGranularityAdjustments(props.firstDayOfWeek); const { savedRanges, saveRange, resetSavedRanges } = useGranularityMemory(); /** * Format date to a string value in format yyyy-MM-dd * @param {Date} dateToFormat Date to be formatted */ const formatDate = (dateToFormat?: Date) => { if (!dateToFormat) { return ''; } return format(dateToFormat, DateFormat.NUMERIC_DATE_YEAR_FIRST, { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, }); }; const formatTime = (dateToFormat?: Date) => { if (!dateToFormat) { return ''; } return format(dateToFormat, DateFormat.HOUR_MINUTE_SECONDS, { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, }); }; // Form error flags const startDateError = ref(false); const endDateError = ref(false); const startTimeError = ref(false); const endTimeError = ref(false); // Form input text used in range variant const dateTextInput = reactive({ startDate: props.dateFormData.startDate, endDate: props.dateFormData.endDate, startTime: props.dateFormData.startTime, endTime: props.dateFormData.endTime, }); /** * Sets the time of a date object from a time string. * @param dateToSetTimeIn The date object to set the time in. * @param time The time string in HH:MM:SS format. * @returns A new Date object with the time set. */ const setTimeInDateObject = (dateToSetTimeIn: Date, time: string) => { let dateWithTime = new Date(dateToSetTimeIn); const [hours, minutes, seconds] = time.split(':'); dateWithTime = setHours(dateWithTime, parseInt(hours, 10)); dateWithTime = setMinutes(dateWithTime, parseInt(minutes, 10)); dateWithTime = setSeconds(dateWithTime, parseInt(seconds, 10)); return dateWithTime; }; const isValidTimeString = (timeText: string) => /^(?:[01]\d|2[01234]):(?:[012345]\d):(?:[012345]\d)$/.test(timeText); /** * Checks if a time string is valid and within the min/max bounds if a date is provided. * @param timeText The time string to validate. * @param date The date string to check the time against. * @returns True if the time is valid, false otherwise. */ const isValidTime = (timeText: string, date?: string) => { const isTimeAllowed = () => { if (date && isValidDate(date)) { const dateToCheckWithTime = setTimeInDateObject(new Date(date), timeText); // Check if date is between min/max bounds, if there are any if ( (props.min && dateToCheckWithTime < setMilliseconds(props.min, 0)) || (props.max && dateToCheckWithTime > setMilliseconds(props.max, 0)) ) { return false; } } return true; }; return isValidTimeString(timeText) && isTimeAllowed(); }; /** * Sets the time in a date object if the time is valid and the variant is not 'month'. * @param date The date object to modify. * @param time The time string to set. * @returns The modified date object or the original if time is not set. */ const setTimeInDateRef = (date: Date, time: string) => { // Do not set time if month variant if (!time || !isValidTimeString(time) || props.variant === 'month') { return date; } return setTimeInDateObject(new Date(date), time); }; const onDateInput = (selectedRange: BentoDateRangePickerValue) => { dateTextInput.startTime = formatTime( props.min && props.allowTimeInput && isSameDay(props.min, selectedRange.startDate) ? props.min : startOfDay(selectedRange.startDate) ); dateTextInput.endTime = formatTime( props.max && props.allowTimeInput && isSameDay(props.max, selectedRange.endDate) ? props.max : endOfDay(selectedRange.endDate) ); internalRangeDate.startDate = setTimeInDateRef(selectedRange.startDate, dateTextInput.startTime); internalRangeDate.endDate = setTimeInDateRef(selectedRange.endDate, dateTextInput.endTime); // Resetting internal range date when selecting a custom range through the calendar internalRangeDate.range = null; if (props.granularities) { // Reset saved ranges on input resetSavedRanges(); } emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); }; /** * Checks if a date string is in YYYY-MM-DD format and if the date is enabled. * @param dateText The date string to validate. * @returns True if the date is valid and enabled, false otherwise. */ const isValidDate = (dateText: string) => { // Is date selectable const isDateEnabled = (date: Date) => { // Check if date is between or equal to min/max bounds // Set the time to the start of the day to disregard time if ( (props.min && startOfDay(date) < startOfDay(props.min)) || (props.max && startOfDay(date) > startOfDay(props.max)) ) { return false; } // Check if function is set, otherwise all dates are enabled if (props.isDateDisabled) { return !props.isDateDisabled(startOfDay(date)); } return true; }; // YYYY-MM-DD regex const dateRegex = /^\d{4}-(0?[1-9]|1[012])-(0?[1-9]|[12][0-9]|3[01])$/; // Date matches text regex return dateRegex.test(dateText) && isDateEnabled(new Date(dateText)); }; /** * When mounted, set the input error states to true if the values are not valid */ onMounted(() => { // Start date validation startDateError.value = !!(props.dateFormData.startDate && !isValidDate(props.dateFormData.startDate)); // Start time validation if (props.allowTimeInput) { startTimeError.value = !!( props.dateFormData.startTime && !isValidTime(props.dateFormData.startTime, props.dateFormData.startDate) ); } else { startTimeError.value = false; } // End date validation endDateError.value = !!(props.dateFormData.endDate && !isValidDate(props.dateFormData.endDate)); // End time validation if (props.allowTimeInput) { endTimeError.value = !!( props.dateFormData.endTime && !isValidTime(props.dateFormData.endTime, props.dateFormData.endDate) ); } else { endTimeError.value = false; } }); const onStartDateSelected = (newStartDate: Date) => { dateTextInput.startDate = formatDate(newStartDate); if (!dateTextInput.startTime) { // Set the time to the beginning of the day dateTextInput.startTime = formatTime(newStartDate); } else if (startTimeError.value) { // Set the form time if it was previously set dateTextInput.startTime = props.dateFormData?.startTime; } // Reset errors startDateError.value = false; startTimeError.value = false; emit(DateRangePickerCalendarEvent.START_DATE_SELECTED, newStartDate); }; const onEndDateSelected = (newEndDate: Date) => { dateTextInput.endDate = formatDate(newEndDate); if (!dateTextInput.endTime) { // Set the time to the end of the day dateTextInput.endTime = formatTime(endOfDay(newEndDate)); } else if (endTimeError.value) { // Set the form time if it was previously set dateTextInput.endTime = props.dateFormData?.endTime; } // Reset errors endDateError.value = false; endTimeError.value = false; }; const getDate = (keyName: keyof BentoDateRangePickerValue) => { const dateProp = (props.value as BentoDateRangePickerValue)[keyName]; if (dateProp) { return dateProp; } if (props.dateFormData[keyName] && isValidDate(props.dateFormData[keyName])) { return new Date(props.dateFormData[keyName]); } return undefined; }; const validateStartDate = debounce((startDateText: string) => { // Checks for YYYY-MM-DD format if (isValidDate(startDateText)) { let dateText = startDateText; startDateError.value = false; switch (selectedGranularity.value?.type) { case 'weekly': dateText = adjustStartDateInputForWeekly( startDateText, formatDate, computedMinMax.value.min, computedMinMax.value.max ); dateTextInput.startDate = dateText; break; case 'monthly': dateText = adjustStartDateInputForMonthly( startDateText, formatDate, computedMinMax.value.min, computedMinMax.value.max ); dateTextInput.startDate = dateText; break; case 'quarterly': dateText = adjustStartDateInputForQuarterly( startDateText, formatDate, computedMinMax.value.min, computedMinMax.value.max ); dateTextInput.startDate = dateText; break; case 'daily': default: break; } const startDate = parseDateTextToStartOfDay(dateText); internalRangeDate.startDate = setTimeInDateRef(startDate, dateTextInput.startTime); internalRangeDate.endDate = setTimeInDateRef( parseDateTextToStartOfDay(dateTextInput.endDate), dateTextInput.endTime ); if (isValidDate(dateTextInput.endDate)) { internalRangeDate.endDate = setTimeInDateRef( parseDateTextToStartOfDay(dateTextInput.endDate), dateTextInput.endTime ); } const endDate = getDate('endDate'); // Resetting internal range date when selecting a custom range through the calendar internalRangeDate.range = null; emit(DateRangePickerCalendarEvent.FORM_DATE, { startDate, endDate, }); emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); } else { // Indicate form error startDateError.value = true; // Emit Date picker calendar error emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); } }, FORM_INPUT_DEBOUNCE_TIME); const onStartDateInput = (startDateText: string) => { // Set the date as string when input through the form dateTextInput.startDate = formatDateInputValue(startDateText); validateStartDate(dateTextInput.startDate); }; const validateEndDate = debounce((endDateText: string) => { // Checks for YYYY-MM-DD format if (isValidDate(endDateText)) { let dateText = endDateText; endDateError.value = false; switch (selectedGranularity.value?.type) { case 'weekly': dateText = adjustEndDateInputForWeekly( endDateText, formatDate, computedMinMax.value.min, computedMinMax.value.max ); dateTextInput.endDate = dateText; break; case 'monthly': dateText = adjustEndDateInputForMonthly( endDateText, formatDate, computedMinMax.value.min, computedMinMax.value.max ); dateTextInput.endDate = dateText; break; case 'quarterly': dateText = adjustEndDateInputForQuarterly( endDateText, formatDate, computedMinMax.value.min, computedMinMax.value.max ); dateTextInput.endDate = dateText; break; case 'daily': default: break; } // Format date string to Date object const endDate = parseDateTextToStartOfDay(dateText); internalRangeDate.endDate = setTimeInDateRef(endDate, dateTextInput.endTime); if (isValidDate(dateTextInput.startDate)) { internalRangeDate.startDate = setTimeInDateRef( parseDateTextToStartOfDay(dateTextInput.startDate), dateTextInput.startTime ); } const startDate = getDate('startDate'); // Resetting internal range date when selecting a custom range through the calendar internalRangeDate.range = null; emit(DateRangePickerCalendarEvent.FORM_DATE, { startDate, endDate, }); emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); } else { // Indicate form error endDateError.value = true; // Emit Date picker calendar error emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); } }, FORM_INPUT_DEBOUNCE_TIME); const onEndDateInput = (endDateText: string) => { // Set the date as string when input through the form dateTextInput.endDate = formatDateInputValue(endDateText); validateEndDate(dateTextInput.endDate); }; const onDateInputKeyDown = (event: KeyboardEvent) => { validateDateInput(event); }; const validateStartTime = debounce((startTimeText: string) => { if (isValidTime(startTimeText, props.dateFormData.startDate)) { startTimeError.value = false; internalRangeDate.startDate = setTimeInDateRef(internalRangeDate.startDate, startTimeText); const endDate = getDate('endDate'); emit(DateRangePickerCalendarEvent.FORM_DATE, { startDate: internalRangeDate.startDate, endDate, startTime: startTimeText, endTime: dateTextInput.endTime ?? props.dateFormData.endTime, }); emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); } else { // Indicate form error startTimeError.value = true; // Emit Date picker calendar error emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); } }); const validateEndTime = debounce((endTimeText: string) => { if (isValidTime(endTimeText, props.dateFormData.endDate)) { endTimeError.value = false; internalRangeDate.endDate = setTimeInDateRef(internalRangeDate.endDate, endTimeText); emit(DateRangePickerCalendarEvent.FORM_DATE, { startDate: internalRangeDate.startDate, endDate: internalRangeDate.endDate, startTime: dateTextInput.startTime ?? props.dateFormData.startTime, endTime: endTimeText, }); emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); } else { // Indicate form error endTimeError.value = true; // Emit Date picker calendar error emit(DateRangePickerCalendarEvent.ERROR, { ...dateTextInput }); } }); const onStartTimeInput = (startTimeText: string) => { dateTextInput.startTime = formatTimeInputValue(startTimeText); validateStartTime(dateTextInput.startTime); }; const onEndTimeInput = (endDateText: string) => { dateTextInput.endTime = formatTimeInputValue(endDateText); validateEndTime(dateTextInput.endTime); }; const onTimeInputKeyDown = (event: KeyboardEvent) => { validateTimeInput(event); }; const setInternalRange = ( startDate: Date, endDate: Date, granularity?: CalendarGranularityType, range?: string ) => { internalRangeDate.startDate = startDate; internalRangeDate.endDate = endDate; if (granularity) { internalRangeDate.granularity = granularity; } if (range) { internalRangeDate.range = range; } }; const onRangeSelectorInput = (newCustomRangeValue: string) => { const foundResult = props.quickSelectRanges?.find(({ value }) => value === newCustomRangeValue); if (foundResult) { resetSavedRanges(); const date = foundResult.data; const newEndDate = date.endDate || (date.isRelativeToNow ? new Date(Date.now()) : endOfDay(new Date(Date.now()))); // If the time difference between start and end dates is available, // calculating start date dynamically const newStartDate = date.isRelativeToNow && date.timeDifference ? new Date(newEndDate.getTime() - date.timeDifference) : date.startDate; // Set Form input Dates dateTextInput.endDate = formatDate(newEndDate); dateTextInput.startDate = formatDate(newStartDate); setInternalRange(newStartDate, newEndDate, null, newCustomRangeValue); if (props.granularities) { internalRangeDate.granularity = date.granularity || defaultGranularity.value; } // Set Form input time to quick select range time dateTextInput.startTime = formatTime(newStartDate); dateTextInput.endTime = formatTime(newEndDate); // Set calendar selection internalRangeDate.startDate = newStartDate; internalRangeDate.endDate = newEndDate; if (date.isRelativeToNow) { // Store this calculated start and end dates to be used in the selectedRangeSelectorValue logic calculatedIsRelativeToNowQuickSelectRanges.value = { ...calculatedIsRelativeToNowQuickSelectRanges.value, [newCustomRangeValue]: { startDate: newStartDate, endDate: newEndDate, }, }; } // Emit everytime the date range changes emit(DateRangePickerCalendarEvent.CUSTOM_RANGE, { ...foundResult.data, startDate: newStartDate, endDate: newEndDate, range: foundResult.value, }); } }; const adjustDate = { weekly: adjustDateForWeekly, monthly: adjustDateForMonthly, quarterly: adjustDateForQuarterly, }; const onGranularityInput = (value: CalendarGranularityType) => { const previousValue = internalRangeDate.granularity; internalRangeDate.granularity = value; // Save previously selected range if (previousValue) { saveRange(previousValue, structuredClone(toRaw(internalRangeDate))); } if (internalRangeDate.startDate && internalRangeDate.endDate) { // Adjust date range based on new granularity switch (value) { case 'quarterly': case 'monthly': case 'weekly': { if (savedRanges[value]) { setInternalRange(savedRanges[value].startDate, savedRanges[value].endDate, value); break; } const { startDate, endDate } = adjustDate[value]( internalRangeDate.startDate, internalRangeDate.endDate, computedMinMax.value.min, computedMinMax.value.max ); setInternalRange(startDate, endDate, value); break; } case 'daily': if (savedRanges.daily) { setInternalRange(savedRanges.daily.startDate, savedRanges.daily.endDate, value); } break; default: break; } dateTextInput.startDate = formatDate(internalRangeDate.startDate); dateTextInput.endDate = formatDate(internalRangeDate.endDate); emit(DateRangePickerCalendarEvent.INPUT, { ...internalRangeDate, range: selectedRangeSelectorValue.value, }); } }; const computedFormContainerClasses = computed(() => ({ 'b-date-range-picker-calendar__form-container--no-calendars': hasNoCalendars.value, })); </script> <script lang="ts"> /** * Calendar for range dates. Contains the logic to handle the range forms. * * @example * import DatePickerCalendarRange from './date-range-picker-calendar.vue'; * * export default { * components: { DatePickerCalendarRange }, * template: ` * <date-range-picker-calendar * :value="{startDate: new Date(), endDate: new Date() }" * :first-day-of-week="BentoDatePickerFirstDayOfWeek.MONDAY" * :is-date-disabled="(date: Date) => boolean" * :number-of-months="2" * @input="({ startDate, endDate }) => void" * /> * ` * } */ export default { name: 'date-range-picker-calendar', i18n: { messages }, }; </script> <style lang="scss" scoped src="./date-range-picker-calendar.scss" />