@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
+ <template> <div ref="rootRef" class="b-empty-state"> <!-- illustration --> <img v-if="image && imageVariant" alt="" class="b-empty-state__image" :class="imageConditionalClasses" :src="imageSvg" /> <!-- heading --> <bento-typography v-bind="headingProps" class="b-empty-state__title"> <slot name="title"> {{ title }} </slot> </bento-typography> <!-- details --> <bento-typography v-if="variant !== 'condensed'" variant="body" class="b-empty-state__details"> <slot> {{ description }} </slot> </bento-typography> <!-- action --> <bento-button v-if="action" type="button" :variant="actionVariant" @click="action.event"> <template v-if="action.icon" #iconLeft> <component :is="action.icon" :svg-title="action.title"></component> </template> {{ action.title }} </bento-button> </div> </template> <script setup lang="ts"> import { computed, ref, watch } from 'vue'; import { BentoButton, type BentoButtonVariant } from '@/components/button'; import { BentoTypography, type BentoTypographyElement, type BentoTypographyVariant } from '@/components/typography'; import { observeSizeOfElement } from '@/utils/ts/resize'; import { BentoEmptyStateImageVariant, type BentoEmptyStateProps, BentoEmptyStateVariant, } from './empty-state.types'; const WIDTH_BREAKPOINT = 740; const props = withDefaults(defineProps<BentoEmptyStateProps>(), { action: null, description: null, headingEl: null, image: null, title: '', variant: 'basic', }); const rootRef = ref<HTMLDivElement>(null); const containerWidth = ref(0); const isBelowBreakpoint = computed(() => containerWidth.value < WIDTH_BREAKPOINT); const imageVariant = computed<BentoEmptyStateImageVariant>(() => { switch (props.variant) { case BentoEmptyStateVariant.FULL_PAGE: return isBelowBreakpoint.value ? BentoEmptyStateImageVariant.SMALL : BentoEmptyStateImageVariant.LARGE; case BentoEmptyStateVariant.EMBEDDED: return isBelowBreakpoint.value ? null : BentoEmptyStateImageVariant.SMALL; default: return null; } }); const imageSvg = ref(''); watch( [() => props.image, imageVariant], async () => { if (props.image && imageVariant.value) { // rollup only supports one variable in a dynamic path /* v8 ignore next 4 */ if (imageVariant.value === 'small') { imageSvg.value = (await import(`./assets/small/${props.image}.svg`)).default; } else { imageSvg.value = (await import(`./assets/large/${props.image}.svg`)).default; } } }, { immediate: true } ); const imageConditionalClasses = computed(() => ({ 'b-empty-state__image--small': imageVariant.value === 'small', 'b-empty-state__image--large': imageVariant.value === 'large', })); const HEADING_ELEMENT_MAP = { [BentoEmptyStateVariant.FULL_PAGE]: 'h2', [BentoEmptyStateVariant.EMBEDDED]: 'h3', [BentoEmptyStateVariant.BASIC]: 'h3', [BentoEmptyStateVariant.CONDENSED]: 'div', }; const headingElement = computed( () => (props.headingEl || HEADING_ELEMENT_MAP[props.variant]) as BentoTypographyElement ); const headingProps = computed(() => ({ el: headingElement.value, medium: props.variant !== BentoEmptyStateVariant.CONDENSED, strongest: props.variant === BentoEmptyStateVariant.CONDENSED, variant: (props.variant === BentoEmptyStateVariant.CONDENSED ? 'body' : 'title') as BentoTypographyVariant, })); const actionVariant = computed<`${BentoButtonVariant}`>(() => { switch (props.variant) { case BentoEmptyStateVariant.FULL_PAGE: return 'primary'; case BentoEmptyStateVariant.CONDENSED: return 'tertiary'; default: return 'secondary'; } }); // Lifecycle watch(rootRef, () => { // Use ResizeObserver so that image is only fetched if needed (visible). observeSizeOfElement(rootRef.value, () => { if (rootRef.value) { containerWidth.value = rootRef.value.offsetWidth; } }); }); </script> <script lang="ts"> /** * Empty states are moments in the user experience when there is nothing to display. * This component can be used to provide: * - Information about system status * - Contextual learning cues * - Direct pathways for key tasks * * @example * import { BentoEmptyState } from '@adyen/bento-vue2'; * * export default { * components: { BentoEmptyState }, * template: ` * <bento-empty-state * title="No results were found" * image="no-results-found" * variant="full-page" * :action="{ title: 'Reset filters', event: () => {} }" * > * Try a different term or reset search filters * </bento-empty-state> * ` * } */ export default {}; </script> <style lang="scss" scoped src="./empty-state.scss" />
@@ -0,0 +1 @@
1
+ <template> <Transition v-if="isVue2" :name="name" mode="out-in" :css="showFileIcon"> <slot /> </Transition> <TransitionGroup v-else :name="name" :css="showFileIcon"> <slot /> </TransitionGroup> </template> <script setup lang="ts"> import { isVue2 } from 'vue-demi'; defineProps<{ name: string; showFileIcon: boolean; }>(); </script>
@@ -0,0 +1 @@
1
+ import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import { FileUploaderFileCard } from './'; import type { Meta, StoryObj } from '@storybook/vue'; import { computed } from 'vue'; import { FileErrorType, FileState, FileType } from '@/components/file-uploader/file-uploader.types'; const meta: Meta = { title: 'File uploader/File uploader file card', component: FileUploaderFileCard, argTypes: { file: { control: { disable: true, }, }, fileName: { control: { type: 'text', }, }, fileContent: { control: { type: 'text', }, }, // Enum props fileType: { options: Object.values(FileType), mapping: FileType, control: { type: 'select', }, }, fileState: { options: Object.values(FileState), mapping: FileState, control: { type: 'select', }, }, fileErrors: { control: 'check', options: Object.values(FileErrorType), }, // Events cancel: { name: 'cancel', table: { type: { summary: '() => void' } }, control: { disable: true }, }, remove: { name: 'remove', table: { type: { summary: '() => void' } }, control: { disable: true }, }, retry: { name: 'retry', table: { type: { summary: '() => void' } }, control: { disable: true }, }, }, }; export default meta; type Story = StoryObj<typeof FileUploaderFileCard>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { FileUploaderFileCard }, props: Object.keys(argTypes), template: ` <div style="max-width: 500px"> <file-uploader-file-card :file="file" :supportedFileTypes="args.supportedFileTypes" @cancel="triggerEvent('cancel')" @remove="triggerEvent('remove')" @retry="triggerEvent('retry')" /> </div> `, setup(props) { const args = isVue2 ? props : _args; const file = computed(() => ({ id: 'stub-file', type: args.fileType, data: new File([args.fileContent], args.fileName), state: args.fileErrors.length ? FileState.ERROR : args.fileState, errors: args.fileErrors, progress: null, })); const triggerEvent = (eventName: string) => action(eventName)(); return { // Values args, file, // Events triggerEvent, }; }, }), args: { fileName: 'FileName', fileType: FileType.IMAGE, fileState: FileState.SUCCESS, fileContent: 'blob', fileErrors: [], supportedFileTypes: 'video files, image files', }, };
@@ -0,0 +1 @@
1
+ <template> <div class="b-file-uploader-file-card" :class="conditionalClasses"> <div class="b-file-uploader-file-card__details"> <div class="b-file-uploader-file-card__info"> <file-uploader-file-card-animation :name=" isVue2 ? 'b-file-uploader-file-card__animation' : 'b-file-uploader-file-card__animation--vue3' " :show-file-icon="showFileIcon" > <component :is="fileUploaderIcon" :key="JSON.stringify(fileUploaderIcon)" small aria-hidden="true" class="b-file-uploader-file-card__icon" :class="conditionalIconClasses" data-testid="file-uploader-item-icon" /> </file-uploader-file-card-animation> <div ref="fileNameContainerRef" class="b-file-uploader-file-card__file-name"> <bento-link aria-hidden="true" tabindex="-1" :download="file.data.name" :to="downloadUrl" disable-typography is-not-routing variant="quiet" > <bento-typography ref="fileNameRef" v-bento-tooltip-directive="isFileNameTruncated ? file.data.name : null" stronger el="span" > {{ file.data.name }} </bento-typography> </bento-link> </div> <bento-typography v-if="file.progress" el="span">{{ progressLabel }}</bento-typography> <bento-typography v-else-if="file.data.size" class="b-file-uploader-file-card__file-size" aria-hidden="true" el="span" > {{ sizeLabel }} </bento-typography> </div> <div v-if="hasSizeError || hasDimensionsError || (hasTypeError && supportedFileTypes)" :id="fileErrorsSectionId" role="alert" > <bento-typography v-if="hasSizeError" class="b-file-uploader-file-card__error-message"> {{ t('fileExceedsSizeLimit') }} </bento-typography> <bento-typography v-if="hasDimensionsError" class="b-file-uploader-file-card__error-message"> {{ t('imageDimensionsExceedLimits') }} </bento-typography> <bento-typography v-if="hasTypeError && supportedFileTypes" class="b-file-uploader-file-card__error-message" > {{ t('fileTypeMustBe', { list: supportedFileTypes }) }} </bento-typography> </div> </div> <div v-if="!readonly" class="b-file-uploader-file-card__actions"> <bento-button v-if="hasLoadError" ref="retryFileButtonRef" :aria-labelledby="`${fileErrorsSectionId} ${retryFileButtonId}`" variant="tertiary" @click="emit('retry')" > <template #iconLeft> <refresh-icon :id="retryFileButtonId" :svg-title="`${t('retry')} - ${file.data.name} `" /> </template> {{ t('retry') }} </bento-button> <bento-button v-if="isLoading || hasError" ref="cancelFileButtonRef" :aria-labelledby="`${fileErrorsSectionId} ${cancelFileButtonId}`" variant="tertiary" @click="emit('cancel')" > <template #iconLeft> <cross-icon :id="cancelFileButtonId" :svg-title="`${t('cancel')} - ${file.data.name}`" /> </template> </bento-button> <bento-button v-else ref="removeFileButtonRef" :aria-labelledby="`${fileErrorsSectionId} ${removeFileButtonId}`" variant="tertiary" @click="emit('remove')" > <template #iconLeft> <bin-icon :id="removeFileButtonId" :svg-title="`${t('remove')} - ${file.data.name}`" /> </template> </bento-button> </div> </div> </template> <script setup lang="ts"> import { computed, ref, watch } from 'vue'; import { isVue2 } from 'vue-demi'; import { FileUploaderFileCardAnimation } from './components'; import BinIcon from '@adyen/ui-assets-icons-16/vue/bin'; import CheckmarkCircleFilledIcon from '@adyen/ui-assets-icons-16/vue/checkmark-circle-fill'; import CrossIcon from '@adyen/ui-assets-icons-16/vue/cross'; import RefreshIcon from '@adyen/ui-assets-icons-16/vue/refresh'; import WarningFilledIcon from '@adyen/ui-assets-icons-16/vue/warning-filled'; import { BentoTooltipDirective as vBentoTooltipDirective } from '@/directives'; import { BentoButton } from '@/components/button'; import { BentoLink } from '@/components/link'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoTypography } from '@/components/typography'; import { useTruncatedFileName } from './composables/use-truncated-file-name'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { formatFileSize } from '../../utils'; import { getFileIcon, getFileIconColor } from './utils'; import { FileErrorType, FileState } from '@/components/file-uploader/file-uploader.types'; import type { FileUploaderFileCardProps } from './file-uploader-file-card.types'; import type { VueInstance } from '@/types/elements'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const ICON_TIMER = 1200; const props = withDefaults(defineProps<FileUploaderFileCardProps>(), { supportedFileTypes: null, }); const emit = defineEmits<{ /** * Triggered when the "cancel" button is clicked */ (e: 'cancel'): void; /** * Triggered when the "remove" button is clicked */ (e: 'remove'): void; /** * Triggered when the "retry" button is clicked */ (e: 'retry'): void; }>(); const { t, locale } = useI18n<{ message: MessageSchema }>({ messages }); const removeFileButtonRef = ref(null); const cancelFileButtonRef = ref(null); const retryFileButtonRef = ref(null); const fileErrorsSectionId = generateUid('file-errors-section'); const cancelFileButtonId = generateUid('cancel-file-button'); const removeFileButtonId = generateUid('remove-file-button'); const retryFileButtonId = generateUid('retry-file-button'); // If initial state is success, immediately show file icon const showFileIcon = ref(props.file.state === FileState.SUCCESS); const downloadUrl = computed(() => URL.createObjectURL(props.file.data)); const hasError = computed(() => props.file.state === FileState.ERROR); const hasLoadError = computed(() => props.file.errors.includes(FileErrorType.ERROR_LOAD)); const hasSizeError = computed(() => props.file.errors.includes(FileErrorType.ERROR_SIZE)); const hasDimensionsError = computed(() => props.file.errors.includes(FileErrorType.ERROR_DIMENSIONS)); const hasTypeError = computed(() => props.file.errors.includes(FileErrorType.ERROR_TYPE)); const isLoading = computed(() => props.file.state === FileState.LOADING); const progressLabel = computed(() => props.file.progress && `(${props.file.progress.toFixed(0)}%)`); const sizeLabel = computed(() => formatFileSize(locale.value, props.file.data.size)); // Truncation tooltip const fileNameRef = ref<VueInstance>(); const fileNameContainerRef = ref<HTMLDivElement>(); const isFileNameTruncated = useTruncatedFileName(fileNameRef, fileNameContainerRef); const conditionalClasses = computed(() => ({ 'b-file-uploader-file-card--error': hasError.value, })); const conditionalIconClasses = computed(() => ({ [`b-file-uploader-file-card__icon--${getFileIconColor(props.file)}`]: showFileIcon.value, 'b-file-uploader-file-card__icon--error': hasError.value, })); // Compute the icon to be displayed const fileUploaderIcon = computed(() => { if (isLoading.value) { return BentoLoadingIndicator; } if (hasError.value) { return WarningFilledIcon; } return showFileIcon.value ? getFileIcon(props.file) : CheckmarkCircleFilledIcon; }); // Animation when success icon turns to file icon. watch( () => props.file.state, value => { if (value === FileState.SUCCESS && !showFileIcon.value) { const changeTimer = setTimeout(() => { showFileIcon.value = true; clearTimeout(changeTimer); }, ICON_TIMER); } } ); /** * Places the focus on the button element. */ function focus() { if (retryFileButtonRef.value) { retryFileButtonRef.value.$el.focus(); return; } if (removeFileButtonRef.value) { removeFileButtonRef.value.$el.focus(); return; } if (cancelFileButtonRef.value) { cancelFileButtonRef.value.$el.focus(); return; } } defineExpose({ focus, }); </script> <script lang="ts"> /** * * Individual file items that are listed inside "bento-file-uploader" that can * be retried, removed or cancelled as they are being uploaded. * * @example * import { FileUploaderFileCard, FileObject } from './'; * * export default { * components: { FileUploaderFileCard }, * template: ` * <file-uploader-file-card * :file="file" * supportedFileTypes="video files, image files" * @retry="retryFile(file)" * @cancel="removeFile(file)" * @remove="removeFile(file)" * /> * `, * setup() { * const file = ref<FileObject>({ * id: 'stub-file', * type: FileType.AUDIO, * data: new File(['blob'], 'File name'), * state: FileState.SUCCESS, * progress: 99, * }); * } * } */ export default { name: 'file-uploader-file-card', i18n: { messages }, }; </script> <style lang="scss" scoped src="./file-uploader-file-card.scss" />
@@ -0,0 +1 @@
1
+ import { isVue2 } from 'vue-demi'; import { FileUploaderRestrictions } from './'; import type { Meta, StoryObj } from '@storybook/vue'; const meta: Meta = { title: 'File uploader/File uploader restrictions', component: FileUploaderRestrictions, }; export default meta; type Story = StoryObj<typeof FileUploaderRestrictions>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { FileUploaderRestrictions }, props: Object.keys(argTypes), template: ` <div style="max-width: 500px"> <file-uploader-restrictions v-bind="args" /> </div> `, setup(props) { return { // Values args: isVue2 ? props : _args, }; }, }), args: { condensed: false, disabled: false, error: false, maxCount: { value: 1, error: false, }, maxDimensions: { value: { width: null, height: null }, error: false, }, maxSize: { value: 3, error: false, }, supportedFileTypes: { value: 'video files, image files', error: false, }, }, };
@@ -0,0 +1 @@
1
+ <template> <div class="b-file-uploader-restrictions" :class="conditionalClasses"> <bento-typography v-if="supportedFileTypes" :class="setRestrictionErrorClass(supportedFileTypes.error)"> {{ t('supportedFileTypes', { list: supportedFileTypes.value }) }} </bento-typography> <bento-typography v-if="maxSize" :class="setRestrictionErrorClass(maxSize.error)"> {{ t('maxFileSize', { size: formatFileSize(locale, maxSize.value) }) }} </bento-typography> <bento-typography v-if="maxDimensions" :class="setRestrictionErrorClass(maxDimensions.error)"> {{ maxDimensionsLabel }} </bento-typography> <bento-typography v-if="maxCount" :class="setRestrictionErrorClass(maxCount.error)"> {{ tc('maxUpload', maxCount.value, { count: n(maxCount.value) }) }} </bento-typography> </div> </template> <script setup lang="ts"> import { computed } from 'vue'; import { BentoTypography } from '@/components/typography'; import { useI18n } from '@/utils/ts/i18n'; import { formatFileSize } from '../../utils'; import type { FileUploaderRestrictionsProps } from './file-uploader-restrictions.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<FileUploaderRestrictionsProps>(), { condensed: null, disabled: null, maxCount: null, maxDimensions: null, maxSize: null, supportedFileTypes: null, }); const { t, tc, n, locale } = useI18n<{ message: MessageSchema }>({ messages }); const conditionalClasses = computed(() => ({ ['b-file-uploader-restrictions--condensed']: props.condensed, ['b-file-uploader-restrictions--disabled']: props.disabled, })); /** * Returns the error class to be set if a restriction has an error * @param error Indicates if the restriction has errored * @returns Conditional error class */ function setRestrictionErrorClass(error: boolean) { return { 'b-file-uploader-restrictions--error': error, }; } const maxDimensionsLabel = computed(() => { if (!props.maxDimensions) { return null; } const { value: { width, height }, } = props.maxDimensions; if (width && height) { return t('maxImageDimensions', { width: n(width), height: n(height) }); } if (width) { return t('maxImageWidth', { width: n(width) }); } if (height) { return t('maxImageHeight', { height: n(height) }); } return null; }); </script> <script lang="ts"> /** * List of restrictions defined for "bento-file-uploader". * * @example * import { FileUploaderRestrictions } from './'; * * export default { * components: { FileUploaderRestrictions }, * template: ` * <file-uploader-restrictions * :maxCount="1" * :maxSize="5000" * :maxDimensions="{ width: 100, height: 200 }" * supportedFileTypes="audio/*,.pdf,.jpg" * /> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./file-uploader-restrictions.scss" />
@@ -0,0 +1,117 @@
1
+ import { Canvas, Meta, Source } from '@storybook/blocks';
2
+ import * as FileUploaderStories from './file-uploader.stories';
3
+
4
+ <Meta of={FileUploaderStories} />
5
+
6
+ # File uploader
7
+
8
+ The file uploader component enables users to upload one or multiple files from their device to our system.
9
+
10
+ <Canvas of={FileUploaderStories.Default} />
11
+
12
+ ## Use Cases
13
+
14
+ Use BentoFileUploader when:
15
+
16
+ - Files need to be validated and provide clear error messages is necessary
17
+
18
+ ## Variations
19
+
20
+ ### Upload area (default)
21
+
22
+ Has a large drop area for when users need to upload multiple files at once and space allows for a bigger field.
23
+
24
+ Enabled with `condensed` set to `false`
25
+
26
+ <Source
27
+ dark
28
+ language="html"
29
+ code={`<bento-file-uploader
30
+ :condensed="false"
31
+ />`}
32
+ />
33
+
34
+ ### Upload field
35
+
36
+ Has a smaller drop area for when users need to upload file(s) as part of a form submission with other input elements
37
+
38
+ Enabled with `condensed` set to `true`
39
+
40
+ <Source
41
+ dark
42
+ language="html"
43
+ code={`<bento-file-uploader
44
+ :condensed="true"
45
+ />`}
46
+ />
47
+
48
+ ### Using model-value
49
+
50
+ Providing a model-value to the component will pre-upload the files to the component, but must be provided as a FileList.
51
+ This can be achieved using the DataTransfer API.
52
+
53
+ <Source
54
+ dark
55
+ language="js"
56
+ code={`
57
+ const dataTransfer = new DataTransfer()
58
+
59
+ dataTransfer.items.add({YOUR_FILE});
60
+
61
+ const fileList = dataTransfer.files; `} />
62
+
63
+ ## Modifiers
64
+
65
+ ### Supporting text
66
+
67
+ Enabled by setting `description`, lets you show custom text requirements or explanations.
68
+
69
+ ### Allowed extensions
70
+
71
+ Enabled through the `accept` property.
72
+
73
+ This restriction limits the types of files that can be uploaded based on their file extensions (for example `.jpg`,
74
+ `.png`, `.pdf`, etc.). The allowed file types are specified by the
75
+ [HTML `accept` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept), which outlines the
76
+ [unique file type specifiers](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept#unique_file_type_specifiers).
77
+ available.
78
+
79
+ When this restriction is enabled the users can only select or drag and drop files that match the allowed extensions.
80
+
81
+ If they try to upload a file with an unsupported extension an error message will be displayed.
82
+
83
+ ### Max file size (in bytes)
84
+
85
+ Enabled through the `maxSize` property.
86
+
87
+ This restriction sets a maximum file size limit for each file that can be uploaded.
88
+
89
+ When this restriction is enabled any file that exceeds the specified size limit will be rejected, and an error message
90
+ will be displayed.
91
+
92
+ ### Max number of files and multiple files
93
+
94
+ Enabled through the `maxCount` property.
95
+
96
+ This allows multiple files to be uploaded (when `maxCount` is set to a value greater than 1) and sets a restriction on
97
+ the maximum number of files that can be uploaded.
98
+
99
+ When this restriction is enabled if user tries to upload more than the allowed number, an error will be shown asking
100
+ user to remove excess files.
101
+
102
+ ## Accessibility
103
+
104
+ ### Keyboard interaction
105
+
106
+ | Keyboard interaction | Action |
107
+ | ---------------------------- | ----------------------------------------------------------- |
108
+ | `Tab` | Focus visibly moves to the drop zone |
109
+ | `Spacebar` | Activates file selection |
110
+ | `Enter` | Activates file selection |
111
+ | `Tab (when uploading files)` | Focus visibly moves to “x” or “bin” to cancel/remove upload |
112
+ | `Spacebar` | Activates the action |
113
+ | `Enter` | Activates the action |
114
+
115
+ ## Resources
116
+
117
+ - [Figma link](https://www.figma.com/design/uLabwF3243jdMDsNSP7U9I/Bento---Components?node-id=27850-290851&p=f&m=dev)
@@ -0,0 +1 @@
1
+ import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoFileUploader from './file-uploader.vue'; import type { Meta, StoryObj } from '@storybook/vue'; const meta: Meta = { title: 'File uploader', component: BentoFileUploader, argTypes: { // Slots accept: { control: 'text', table: { type: { summary: "HTMLInputElement['accept']" }, }, }, // Events change: { table: { type: { summary: '(value?: FileList) => void' } }, control: { disable: true }, }, 'update:model-value': { table: { type: { summary: '(value?: FileList) => void' } }, control: { disable: true }, }, input: { description: 'Deprecated since version 2.0. Use `v-model` or `update:model-value` instead', table: { type: { summary: '(value?: FileList) => void' }, category: 'Deprecated', }, control: { disable: true }, }, 'error:upload': { table: { type: { summary: '(value: boolean) => void' } }, control: { disable: true }, }, placeholder: { table: { disable: true } }, }, }; const DEFAULT_PROPS = { label: 'Label', description: 'Custom supporting text', }; export default meta; type Story = StoryObj<typeof BentoFileUploader>; const defaultCode = ` <template> <bento-file-uploader label="${DEFAULT_PROPS.label}" description="${DEFAULT_PROPS.description}" accept="image/*,video/*,.pdf,.png" :condensed="false" :disabled="false" errorMessage="Custom error message" :maxCount="1" :maxDimensions="{ width: 200, height: 100 }" :maxSize="1000" v-model="files" :optional="false" required tooltipText="Useful information" /> </template> <script setup lang="ts"> import { BentoFileUploader, type BentoFileValue } from '@adyen/bento-vue2' import { ref } from 'vue' const files = ref<BentoFileValue>() </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoFileUploader }, props: Object.keys(argTypes), template: ` <div style="max-width: 500px"> <bento-file-uploader v-bind="args" @error:upload="errorAction" @change="changeAction" /> </div> `, setup(props) { const changeAction = value => action('change')(value); // As 'value' is a boolean, it needs to be a template literal otherwise // passing 'false' indicates there is no args const errorAction = value => action('error:upload')(`${value}`); return { // Values args: isVue2 ? props : _args, // Events changeAction, errorAction, }; }, }), args: { ...DEFAULT_PROPS, condensed: false, }, parameters: storybookDocsParameter(defaultCode), }; export const ModelValue: Story = { render: (_args, { argTypes }) => ({ components: { BentoFileUploader }, props: Object.keys(argTypes), template: ` <div style="max-width: 500px"> <bento-file-uploader v-bind="args" v-model="mockFileList" @error:upload="errorAction" @change="changeAction"/> </div> `, setup(props) { const changeAction = fileList => { action('change')(fileList); }; // As 'value' is a boolean, it needs to be a template literal otherwise // passing 'false' indicates there is no args const errorAction = value => action('error:upload')(`${value}`); const dataTransfer = new DataTransfer(); const dummyFile = new File(['foo'], 'foo.pdf', { type: 'text/pdf' }); dataTransfer.items.add(dummyFile); const mockFileList = dataTransfer.files; return { // Values args: isVue2 ? props : _args, mockFileList, // Events changeAction, errorAction, }; }, }), args: { ...DEFAULT_PROPS, condensed: false, }, parameters: storybookDocsParameter(defaultCode), };
@@ -0,0 +1 @@
1
+ <template> <div class="b-file-uploader" :class="conditionalClasses"> <field-label class="b-file-uploader__label" :for="inputFieldId" :label="label" :optional="optional" :required="required" :tooltip-text="tooltipText" /> <input :id="inputFieldId" ref="inputRef" aria-hidden="true" type="file" tabindex="-1" data-testid="input" class="b-file-uploader__input" :accept="accept" :disabled="disabled" :multiple="isMultiple" @change="onInputChange" @click="onInputClick" /> <!-- Used to separate the a11y text with breaks for the reader. Hidden to users --> <span :id="commaTextId" aria-hidden="true" class="b-file-uploader__invisible-comma"> , </span> <div v-if="!hideArea" ref="dropAreaRef" :aria-labelledby="fileUploadAriaLabelledBy" class="b-file-uploader__area" role="button" :tabindex="disabled ? -1 : 0" @click="inputRef.click()" @dragenter.stop.prevent="onDragEnter" @dragleave.stop.prevent="onDragLeave" @drop.stop.prevent="onDrop" @dragover="onDragOver" @keydown.enter.prevent.capture="inputRef.click()" @keydown.space.prevent.capture="inputRef.click()" > <div class="b-file-uploader__icon" aria-hidden="true"> <warning-filled-icon v-if="hasError" /> <upload-icon v-else /> </div> <template v-if="!condensed"> <bento-typography :id="dropFilesTextId" el="span" strongest class="b-file-uploader__title"> {{ tc('dropFiles', maxCount) }} </bento-typography> <bento-typography v-if="description" :id="customTextId" el="span" class="b-file-uploader__description"> {{ description }} </bento-typography> <file-uploader-restrictions v-bind="fileUploadRestrictionsProps" :id="fileUploaderRestrictionsId" /> </template> <bento-typography :id="browseFilesButtonId" stronger class="b-file-uploader__button"> {{ t('browseFiles') }} </bento-typography> </div> <error-message v-if="hasError && errorMessage" :id="errorId" :error-message="errorMessage" class="b-file-uploader__error-message" /> <bento-typography v-if="condensed && description" :id="customTextId" aria-hidden="true" el="span" class="b-file-uploader__description b-file-uploader__description--condensed" > {{ description }} </bento-typography> <file-uploader-restrictions v-if="condensed" v-bind="fileUploadRestrictionsProps" :id="fileUploaderRestrictionsId" /> <bento-alert v-if="maxFileCountError" type="critical"> {{ t('tooManyFiles') }} <template #description> {{ t('youHaveExceededTheMaximum', { count: n(props.maxCount) }) }} </template> </bento-alert> <div v-show="files?.length" class="b-file-uploader__files"> <file-uploader-file-card v-for="(file, index) in files" :ref="registerFileCardRef(index)" :key="file.id" :file="file" :supported-file-types="supportedFileTypeList" :readonly="readonly" @cancel="removeFile(file)" @remove="removeFile(file)" /> </div> <bento-typography v-if="!files?.length && readonly" class="b-file-uploader__no-files" wide>{{ t('noFilesUploaded') }}</bento-typography> </div> </template> <script setup lang="ts"> import { computed, nextTick, onMounted, reactive, ref, toRef, watch } from 'vue'; import UploadIcon from '@adyen/ui-assets-icons-16/vue/upload'; import WarningFilledIcon from '@adyen/ui-assets-icons-16/vue/warning-filled'; import { BentoAlert } from '@/components/alert'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; import { FileUploaderFileCard } from './components/file-uploader-file-card'; import { FileUploaderRestrictions } from './components/file-uploader-restrictions'; import { useI18n } from '@/utils/ts/i18n'; import { generateUid } from '@/core/utils/ts'; import { getFileType, readFile } from './utils'; import { useFileTypeList } from './composables'; import { useDragDropState } from '@/components/file-uploader/composables/use-drag-drop-state/use-drag-drop-state'; import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { type BentoFileUploaderProps, type BentoFileUploaderValue, FileErrorType, type FileObject, FileState, FileUploaderState, } from './file-uploader.types'; import type { FileUploaderRestrictionsProps } from '@/components/file-uploader/components/file-uploader-restrictions/file-uploader-restrictions.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t, tc, n } = useI18n<{ message: MessageSchema }>({ messages }); const emit = defineEmits<{ /** * Emitted when files are added or removed. * Provides the entire list of files available. * Can be undefined if the list is empty */ (e: 'change', value?: FileList): void; /** * Vue2: Emitted when files are added or removed. Updates the v-model. * Provides the entire list of files available. * Can be undefined if the list is empty * @deprecated from version 2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', value?: FileList): void; /** * Vue3: Emitted when files are added or removed. Updates the v-model. * Provides the entire list of files available. * Can be undefined if the list is empty */ (e: 'update:model-value', value?: FileList): void; /** * Emitted when any file uploaded has state === FileState.ERROR. * Provides parent component with boolean. * Emits false when no files have errors. */ (e: 'error:upload', value: boolean): void; }>(); const props = withDefaults(defineProps<BentoFileUploaderProps>(), { accept: null, condensed: false, description: null, disabled: false, errorMessage: null, label: null, maxCount: 1, // One file by default maxDimensions: null, maxSize: null, modelValue: null, optional: false, required: false, tooltipText: null, }); const { emitValue } = useFormFieldEmits<BentoFileUploaderValue>(emit); /** * Model value checks * Filter out type string, array and file as FileList is the only type un-deprecated for modelValue * */ /** * Type guard to check if the value is a FileList. * @param files The value to check. */ function isFileList(files: BentoFileUploaderValue): files is FileList { return ( !!files && typeof (files as FileList).length === 'number' && typeof (files as FileList).item === 'function' ); } /** * Used as vitest recognizes FileList as an Array of Files which then * causes any tests with pre-uploaded files to fail as isFileList will fail. * */ onMounted(() => { // Model is not a FileList if (!isFileList(props.modelValue)) { deprecate( 'BentoFileUploaderValue types: string | Array<string> | File | Array<File>', 'Use FileList instead.', '2.0.0' ); } }); const { supportedFileTypeList } = useFileTypeList(toRef(props, 'accept')); const browseFilesButtonId = generateUid('browse-button'); const customTextId = generateUid('custom-text'); const dropFilesTextId = generateUid('drop-files-text'); const commaTextId = generateUid('comma-text'); const fileUploaderRestrictionsId = generateUid('file-uploader-restrictions'); const files = ref<Array<FileObject>>(null); const fileCardRefs = ref<Array<InstanceType<typeof FileUploaderFileCard>>>([]); const inputRef = ref<HTMLInputElement>(null); const dropAreaRef = ref<HTMLDivElement>(null); const inputFieldId = generateUid('input'); const errorId = generateUid('input-error'); const fileUploadAriaLabelledBy = computed(() => { const formattedDropFileTextId = !props.condensed ? `${dropFilesTextId} ${commaTextId}` : ''; const formattedCustomTextId = props.description ? `${customTextId} ${commaTextId}` : ''; const formattedFileUploaderRestrictionsId = `${fileUploaderRestrictionsId} ${commaTextId}`; return `${formattedDropFileTextId} ${formattedCustomTextId} ${formattedFileUploaderRestrictionsId} ${browseFilesButtonId}`; }); const { dragState, dragErrors, resetDragState, onDragEnterValidations } = useDragDropState({ accept: toRef(props, 'accept'), maxCount: toRef(props, 'maxCount'), }); const fileUploadRestrictionsProps = computed<FileUploaderRestrictionsProps>(() => ({ condensed: props.condensed, disabled: props.disabled, maxCount: isMultiple.value ? { value: props.maxCount, error: dragErrors.maxCountOverflow || maxFileCountError.value, } : null, maxDimensions: props.maxDimensions ? { value: props.maxDimensions, error: files.value?.some(file => file.errors.includes(FileErrorType.ERROR_DIMENSIONS)), } : null, maxSize: props.maxSize ? { value: props.maxSize, error: files.value?.some(file => file.errors.includes(FileErrorType.ERROR_SIZE)), } : null, supportedFileTypes: supportedFileTypeList.value ? { value: supportedFileTypeList.value, error: dragErrors.invalidTypes || dragErrors.partiallyInvalidTypes || files.value?.some(file => file.errors.includes(FileErrorType.ERROR_TYPE)), } : null, })); // Prevents triggering "dragleave" events on children elements const enterTarget = ref<EventTarget>(); const conditionalClasses = computed(() => ({ [`b-file-uploader--${FileUploaderState.DISABLED}`]: props.disabled, [`b-file-uploader--${FileUploaderState.DRAG_OVER}`]: dragState.dragging, [`b-file-uploader--${FileUploaderState.ERROR}`]: hasError.value, [`b-file-uploader--condensed`]: props.condensed, })); const isMultiple = computed(() => props.maxCount > 1); const hideArea = computed(() => (props.maxCount && files.value?.length >= props.maxCount) || props.readonly); const loadingStateExists = computed(() => files.value?.some(file => file.state === 'loading')); const registerFileCardRef = (cardIndex: number) => fileCardRef => { fileCardRefs.value[cardIndex] = fileCardRef; }; /** * Verifies the new list of files and emits the `change` and `update:modelValue` events. * @param newList The new list of files to process and emit. */ function emitUpdate(newList: Array<FileObject>) { if (newList) { verifyFileCount(newList); verifyFileSize(newList); const value = convertToFileList(newList); emit('change', value); emitValue(value); } else { emit('change', undefined); emitValue(undefined); } } /** Errors */ const hasError = computed(() => !!props.errorMessage || dragState.error); const maxFileCountError = computed(() => files.value?.length > props.maxCount); const maxFileError = ref(false); const hasRestrictionError = computed(() => files.value?.some(file => file.state === 'error')); watch(hasRestrictionError, newValue => { // Without this, when files are initially uploaded the component // will immediately and unnecessarily emit 'false' and then // emit 'true' once processing has done, if errors exists. if (loadingStateExists.value) { return; } emit('error:upload', newValue); }); /** * Places the focus to the "drop area" after adding or removing a file. * If more files can be added, the focus is moved to the "drop area". * If no more files can be added, the focus is moved to the first file card. */ async function resetFocus() { await nextTick(); if (files.value.length === props.maxCount) { fileCardRefs.value[0]?.focus(); return; } dropAreaRef.value.focus(); } /** * Removes the file from the list of files when the * "remove" or "cancel" events are triggered from * the file card component * @param file File to be removed */ async function removeFile(file: FileObject) { files.value = files.value.filter(({ id }) => id !== file.id); await resetFocus(); emitUpdate(files.value); } /** Drag n Drop functions */ /** * Creates a FileObject from a native File and tracks its client-load progress. * @param file The native File object. * @returns A FileObject. */ function processFile(file: File) { const fileObject = reactive<FileObject>({ id: generateUid('file'), data: file, errors: [], progress: null, state: FileState.LOADING, type: getFileType(file), }); readFile(fileObject, props.accept, props.maxDimensions); return fileObject; } /** * Prevent opening the file browser when no * more files can be added (until one file is removed). * @param event Triggered event */ function onInputClick(event: MouseEvent) { if (files.value?.length === props.maxCount) { event.preventDefault(); } } /** * Sets the file/files to track their progress while being processed * @param fileList File or list of files to be processed. */ async function onFileInput(fileList: FileList) { // Prevent adding many files if maxCount is 1 if (props.maxCount === 1 && fileList.length > props.maxCount) { resetEnterTarget(); resetDragState(); return; } if (isMultiple.value) { // Add the new files to the file selection. // Removing files should be done by interacting with the file list below the area. files.value = [...(files.value || []), ...Array.from(fileList).map(file => processFile(file))]; } else { files.value = [processFile(fileList[0])]; await resetFocus(); } emitUpdate(files.value); } /** * Handles the "dragenter" event, triggered when the mouse enters the draggable area. * Enables the dragging state and sets the dragging error to true to * prevent dropping files from being added if there's a general error. * @param event Drag event */ function onDragEnter(event: DragEvent) { // Prevent changing state if disabled if (props.disabled) { return; } /** * Allows internal components as part of the dragable area. * Prevents trigerring "dragleave" events on children elements. * Set the "dragarea" as target */ enterTarget.value = event.target; const totalFilesCount = (event.dataTransfer.items?.length ?? 0) + (files.value?.length ?? 0); onDragEnterValidations(totalFilesCount, event); } /** * Resets the reference to the target. * This helps preventing loosing the state when dragging over children */ function resetEnterTarget() { enterTarget.value = null; } /** * Handles the "dragleave" event, triggered when the mouse leaves the draggable area. * Maintains the dragging state while dragging over children. * Resets the states when the mouse leaves the draggable area * @param event Drag event */ function onDragLeave(event: DragEvent) { // If target enter and leave are the same it means the drag has left the "dragarea" if (enterTarget.value === event.target) { resetEnterTarget(); resetDragState(); } } /** * Handles the "dragover" event, triggered when the mouse moves over the draggable area. * Disables the cursor when the component is disabled. * @param event Drag event */ function onDragOver(event: DragEvent) { event.preventDefault(); // Disable drag n drop and change the cursor if disabled if (props.disabled) { // eslint-disable-next-line no-param-reassign event.dataTransfer.dropEffect = 'none'; } } /** * Hadles the "drop" event, triggered when files are dropped inside the draggable area. * Processes the files that are dropped. * @param event Drag event */ async function onDrop(event: DragEvent) { // Prevent events when the input is disabled if (props.disabled) { return; } if (!dragErrors.invalidTypes && event.dataTransfer.files?.length) { onFileInput(event.dataTransfer.files); } resetEnterTarget(); resetDragState(); } /** * Handles the "change" event over the "input" element. * Processes the files that are selected through the browser's file explorer. */ async function onInputChange() { if (inputRef.value.files?.length) { onFileInput(inputRef.value.files); } // Reset input value inputRef.value.value = ''; await resetFocus(); } /** End of drag and drop functions */ /** * Watches the modelValue and if set * and the type is not deprecated, it processes the files * but doesn't emit them as the user already has those files. */ watch( () => props.modelValue, (newValue: BentoFileUploaderValue) => { // Sets the new value if it is a `FileList`. This ignores deprecated value types. if (newValue && isFileList(newValue)) { const fileList = newValue as FileList; const fileListArray: Array<File> = Array.from(fileList); files.value = fileListArray.map(file => processFile(file)); } else if (!newValue && files.value) { // Clear the file list when the value is programmatically unset. files.value = null; } }, { immediate: true } ); function verifyFileCount(fileList: Array<FileObject>) { const isValid = props.maxCount && fileList.length <= props.maxCount; maxFileError.value = !isValid; } function verifyFileSize(fileList: Array<FileObject>) { if (props.maxSize) { fileList.forEach(file => { const item = file; // Prevent eslint no-param-reassign if (item.data.size > props.maxSize) { item.state = FileState.ERROR; item.errors.push(FileErrorType.ERROR_SIZE); } }); } } function convertToFileList(fileList: Array<FileObject>) { // Filter out files with errors const validFiles = fileList.filter( ({ errors }) => !errors.includes(FileErrorType.ERROR_LOAD) && !errors.includes(FileErrorType.ERROR_SIZE) ); if (validFiles.length) { const dataTransfer = new DataTransfer(); validFiles.forEach(({ data, id }) => { dataTransfer.items.add(data); }); return dataTransfer.files; } return undefined; } </script> <script lang="ts"> /** * The file uploader component enables users to upload one or multiple files from their device to our system. * * @example * import { BentoFileUploader, type BentoFileValue } from '@adyen/bento-vue2'; * * export default { * components: { BentoFileUploader }, * template: ` * <bento-file-uploader * accept="image/*,video/*,.pdf,.png" * condensed * description="Custom supporting text" * disabled * errorMessage="Custom error message" * label="Bento file uploader" * :maxCount="1" * :maxDimensions="{ width: 200, height: 100 }" * :maxSize="1000" * v-model="files" * :optional="false" * required * tooltipText="Useful information" * /> * `, * setup() { * const files = ref<BentoFileValue>() * } * } */ export default { model: { prop: 'modelValue' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./file-uploader.scss" />
@@ -0,0 +1 @@
1
+ import { ref } from 'vue'; import { subDays } from 'date-fns/subDays'; import { action } from '@storybook/addon-actions'; import BentoAllFiltersModal from './all-filters-modal.vue'; import { type BentoDateRangeFilterOptions, type BentoFilterBarModel, BentoFilterItemType, } from '@/components/filter-bar/filter-bar.types'; import type { Meta, StoryObj } from '@storybook/vue'; import { BentoCountryCode } from '@/components/country'; import { BentoCurrencyISOCode } from '@/components/currency'; import { type BentoListboxOptions } from '@/types/listbox'; // Generate array from 1 to # // { label: 'Option #', value: # }, const DEFAULT_OPTIONS: BentoListboxOptions = new Array(10) .fill(0) .map((_, index) => ({ label: `Option ${index + 1}`, value: index + 1 })); const FILTERS = [ { field: 'input', label: 'Input filter', description: 'Optional short filter description goes here. Some filters may not be self-explanatory just from the name itself.', value: undefined, type: BentoFilterItemType.INPUT, }, { field: 'country', label: 'Country/Region', value: undefined, options: { listboxItems: [ { label: 'Option 1', value: BentoCountryCode.NL, }, { label: 'Option 2', value: BentoCountryCode.BE, }, ], multiple: true, variant: 'capital', }, type: BentoFilterItemType.SELECT_COUNTRY, }, { field: 'avatar', label: 'Avatar', value: undefined, options: { listboxItems: [ { label: 'Avatar 1', value: 'avatar-1', }, { label: 'Avatar 2', value: 'avatar-2', }, ], }, type: BentoFilterItemType.SELECT_AVATAR, }, { field: 'merchant', label: 'Merchant', value: 2, options: { listboxItems: DEFAULT_OPTIONS, }, type: BentoFilterItemType.SELECT, }, { field: 'company', label: 'Company', value: [1, 3, 5], options: { listboxItems: DEFAULT_OPTIONS, multiple: true, }, type: BentoFilterItemType.SELECT, }, { field: 'currency', label: 'Currency select', value: [], options: { listboxItems: [ { label: 'Euro', value: BentoCurrencyISOCode.EUR, }, { label: 'US Dollars', value: BentoCurrencyISOCode.USD, }, ], multiple: true, }, type: BentoFilterItemType.SELECT_CURRENCY, }, { field: 'radio-group', label: 'Radio group', value: null, type: BentoFilterItemType.RADIO_GROUP, options: { items: [ { label: 'Option 1', value: 'option-1' }, { label: 'Option 2', value: 'option-2' }, ], }, }, { field: 'range', label: 'Range', value: ['', ''], type: BentoFilterItemType.RANGE, }, { field: 'date', label: 'Date', value: new Date(), type: BentoFilterItemType.DATE, }, { field: 'date-range', label: 'Date range', value: { startDate: subDays(new Date(), 2), endDate: new Date() }, options: { numberOfMonths: 1, } as BentoDateRangeFilterOptions, type: BentoFilterItemType.DATE_RANGE, }, { field: 'paymentMethod', label: 'Payment method', value: undefined, options: { listboxItems: [ { label: 'iDEAL', value: 'ideal' }, { label: 'Visa', value: 'visa' }, ], multiple: true, }, type: BentoFilterItemType.SELECT_PAYMENT_METHOD, }, { field: 'tag', label: 'Tag', value: undefined, options: { listboxItems: [ { label: 'Offline', value: 'unique-id-1', variant: 'red' }, { label: 'Online', value: 'unique-id-2', variant: 'green' }, { label: 'Standby', value: 'unique-id-3', variant: 'orange' }, { label: 'Busy', value: 'unique-id-4', variant: 'blue' }, ], multiple: true, }, type: BentoFilterItemType.SELECT_TAG, }, ]; const meta: Meta = { title: 'Filter bar/All filters modal', component: BentoAllFiltersModal, argTypes: { // Events input: { name: 'input', defaultValue: '', description: 'Triggered whenever the filters are updated.', table: { type: { summary: '([{ field, label, value, type }]) => void' } }, control: { disable: true }, }, }, }; export default meta; type Story = StoryObj<typeof BentoAllFiltersModal>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoAllFiltersModal }, props: Object.keys(argTypes), setup(props) { const filtersSet = ref<BentoFilterBarModel>(props.value); const updateFilters = (newFilters: BentoFilterBarModel) => { filtersSet.value = newFilters; action('input')(newFilters); }; return { // Values filtersSet, // Events updateFilters, }; }, template: ` <bento-all-filters-modal :value="filtersSet" @input="updateFilters" /> `, }), }; Default.args = { value: FILTERS };
@@ -0,0 +1 @@
1
+ <template> <div class="b-all-filters-modal"> <filter-bar-button :is-filter-open="isModalOpen" :applied-filters-count="numberOfActiveFilters" no-applied-state-styles @click="onClickAllFiltersModal" > {{ t('allFilters') }} <template #iconLeft> <sliders-icon aria-hidden="true" /> </template> </filter-bar-button> <bento-modal class="'b-all-filters-modal__modal'" :is-open="isModalOpen" :with-column-layout="true" size="large" :actions="modalActions" :is-dismissible="false" hide-close-button @close-modal="cancel" > <template #default> {{ t('allFilters') }} </template> <template #content-col-1> <div ref="allFiltersModalFiltersSection" class="b-all-filters-modal__filters" @keydown.right="onKeydownRight" > <bento-search-bar class="b-all-filters-modal__search-bar" :value="searchTerm" :input-field-aria-label="t('searchBar').toString()" @input="searchTerm = $event" @clear="onSearchbarClear" /> <div v-if="isEmptyResultForSearchFilter" class="b-all-filters-modal__empty-search-message"> <bento-typography el="span"> {{ t('noFiltersMatchThisSearch') }}</bento-typography> </div> <div v-if="activeFilters.length" class="b-all-filters-modal__filters-section"> <bento-typography :id="`active-filters-${allFiltersModalUniqueId}`" class="b-all-filters-modal__filters-section-title" el="span" variant="caption" > {{ t('activeFilters') }} </bento-typography> <ul class="b-all-filters-modal__filters-list" :aria-labelledby="`active-filters-${allFiltersModalUniqueId}`" > <li v-for="item in activeFilters" :key="`${item.label}-${item.field}`"> <all-filters-modal-button :ref="el => (allFiltersModalFilterButtons[item.label] = el)" :label="item.label" :applied-filters-count="getFilterCount(item)" :is-filter-open="selectedFilter.label === item.label" :disabled="item.disabled" @click="selectFilter(item.label, item.field)" /> </li> </ul> </div> <div v-if="otherFilters.length" class="b-all-filters-modal__filters-section"> <bento-typography v-if="activeFilters.length" :id="`other-filters-${allFiltersModalUniqueId}`" class="b-all-filters-modal__filters-section-title" el="span" variant="caption" > {{ t('otherFilters') }} </bento-typography> <ul class="b-all-filters-modal__filters-list" :aria-labelledby="`other-filters-${allFiltersModalUniqueId}`" > <li v-for="item in otherFilters" :key="`${item.label}-${item.field}`"> <all-filters-modal-button :ref="el => (allFiltersModalFilterButtons[item.label] = el)" :label="item.label" :disabled="item.disabled" :applied-filters-count="getFilterCount(item)" :is-filter-open="selectedFilter.label === item.label" @click="selectFilter(item.label, item.field)" /> </li> </ul> </div> </div> </template> <template #content-col-2> <div class="b-all-filters-modal__selected-filter-content" @keydown.left="onKeydownLeft"> <div v-if="shouldShowFilterTitle" class="b-all-filters-modal__selected-filter-text"> <bento-typography el="div" variant="title" medium> {{ selectedFilter.label }} </bento-typography> <bento-typography v-if="selectedFilter.description" el="div" variant="body" wide> {{ selectedFilter.description }} </bento-typography> </div> <div class="b-all-filters-modal__selected-filter-component"> <component :is="selectedFilter.type" ref="selectedFilterRef" :key="selectedFilter.field" :description="selectedFilter.description" :field="selectedFilter.field" :label="selectedFilter.label" :options="selectedFilter.options" :value="selectedFilter.value" :default-value="selectedFilter.defaultValue" :only-slot="true" :disabled="selectedFilter.disabled" @input="updateFilter" /> </div> <div class="b-all-filters-modal__clear-button"> <bento-button variant="secondary" :disabled="isClearButtonDisabled" @click="clearFilter">{{ t('clear') }}</bento-button> </div> </div> </template> </bento-modal> </div> </template> <script lang="ts"> import { computed, defineComponent, onBeforeUpdate, onMounted, type PropType, ref, toRef, watch } from 'vue'; import BentoModal from '@/components/modal/modal.vue'; import { BentoButton } from '@/components/button'; import { type BentoFilterBarModel, BentoFilterItemType, type BentoFilterModel, type BentoSelectFilterOptions, } from '@/components/filter-bar/filter-bar.types'; import { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoToggleFilter, } from '../index'; import { type BentoDateRangePickerValue } from '@/components/date-range-picker'; import BentoSearchBar from '@/components/search-bar/search-bar.vue'; import { BentoTypography } from '@/components/typography'; import { useSearchBarFilter } from '@/components/search-bar'; import { generateUid } from '@/core/utils/ts'; import { getNextFocusableElement } from '@/utils/ts/focus-trap.utils'; import { FilterBarButton } from '../filter-bar-button'; import { AllFiltersModalEvent } from './all-filters-modal.types'; import { useI18n } from '@/utils/ts/i18n'; import AllFiltersModalButton from './components/all-filters-modal-button/all-filters-modal-button.vue'; import SlidersIcon from '@adyen/ui-assets-icons-16/vue/sliders-1'; import messages from './messages.json'; import { type BentoCheckboxGroupFilterOptions } from '../checkbox-group-filter'; type MessageSchema = (typeof messages)['en-US']; /** * A modal that acts as sort of filter manager for the BentoFilterBar. * * @example * import { BentoAllFiltersModal } from '@adyen/bento-vue2'; * * export default { * components: { BentoAllFiltersModal }, * template: ` * <bento-all-filters-modal> * ...Example code of the component * </bento-all-filters-modal> * ` * } */ export default defineComponent({ i18n: { messages }, name: 'all-filters-modal', components: { AllFiltersModalButton, BentoBooleanFilter, BentoButton, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoModal, BentoRadioGroupFilter, BentoRangeFilter, BentoSearchBar, BentoSelectAvatarFilter, BentoSelectCurrencyFilter, BentoSelectCountryFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoToggleFilter, BentoTypography, FilterBarButton, SlidersIcon, }, props: { /** * Array of filters based on {@see FilterBarModel}. * Each one of the elements in the array correspond to a filter that will * be created and rendered in the AllFiltersModal. * * Can be used with v-model or independently with the @input event. * * @example * [{ * field: 'company', * label: 'Company', * value: '200', * type: FilterType.TEXT, * }, { * field: 'name', * label: 'Name', * value: null, * type: FilterType.TEXT, * }] */ value: { type: Array as PropType<BentoFilterBarModel>, default: () => [], }, }, emits: [AllFiltersModalEvent.INPUT], setup(props, { emit }) { const filtersInModal = ref(props.value); const allFiltersModalUniqueId = generateUid('all-filters-modal'); const allFiltersModalFiltersSection = ref([]); const isModalOpen = ref(false); const selectedFilterIndex = ref(0); const selectedFilterRef = ref(null); const searchTerm = ref<string>(''); const { t, n } = useI18n<{ message: MessageSchema }>({ messages }); const filteredItems = useSearchBarFilter<BentoFilterModel>(searchTerm, toRef(filtersInModal, 'value')); const allFiltersModalFilterButtons = ref({}); onBeforeUpdate(() => { allFiltersModalFilterButtons.value = []; }); watch( () => props.value, () => { /** * Values need to be preserve when the modal is opened given that * when the "select" filter is enabled with external filtering inside the modal * the "searchTerm" gets reset and doing so resets all the unsaved values of the array */ if (isModalOpen.value) { filtersInModal.value = props.value.map((filter, index) => { return { ...filter, value: filtersInModal.value[index].value }; }); } else { filtersInModal.value = props.value; } } ); const sortAlphabetically = (array: BentoFilterBarModel) => array.sort((a, b) => (a.label > b.label ? 1 : b.label > a.label ? -1 : 0)); const activeFilters = computed(() => sortAlphabetically( filteredItems.value.filter(el => Array.isArray(el.value) ? el.value?.length && el.value?.find(item => item !== '') : el.value ) ) ); const otherFilters = computed(() => sortAlphabetically( filteredItems.value.filter(el => Array.isArray(el.value) ? el.value?.length === 0 || el.value?.every(item => item === '') : !el.value ) ) ); const isEmptyResultForSearchFilter = computed(() => searchTerm.value && filteredItems.value?.length === 0); const selectedFilter = computed(() => filtersInModal.value[selectedFilterIndex.value]); const selectOpenedFilter = () => { const openedFilter = activeFilters.value.length ? activeFilters.value[0] : otherFilters.value[0]; const openedFilterIndex = filtersInModal.value.findIndex( el => el.label === openedFilter.label && el.field === openedFilter.field ); selectedFilterIndex.value = openedFilterIndex; }; onMounted(() => { selectOpenedFilter(); }); const computeCounts = (filterValue: BentoFilterBarModel) => filterValue.map(filter => { switch (filter.type) { case BentoFilterItemType.SELECT: case BentoFilterItemType.SELECT_AVATAR: case BentoFilterItemType.SELECT_COUNTRY: case BentoFilterItemType.SELECT_CURRENCY: case BentoFilterItemType.SELECT_PAYMENT_METHOD: case BentoFilterItemType.SELECT_TAG: switch ((filter.options as BentoSelectFilterOptions)?.multiple) { case true: return { label: filter.label, field: filter.field, count: (filter.value as Array<string | number>)?.length ? (filter.value as Array<string | number>)?.length : null, }; default: return { label: filter.label, field: filter.field, count: filter.value == null ? null : 1, }; } case BentoFilterItemType.DATE_RANGE: return { label: filter.label, field: filter.field, count: (filter.value as BentoDateRangePickerValue)?.startDate != null && (filter.value as BentoDateRangePickerValue)?.endDate != null ? 1 : null, }; case BentoFilterItemType.RANGE: return { label: filter.label, field: filter.field, count: (filter.value as Array<string>)?.find(el => el !== '') ? 1 : null, }; case BentoFilterItemType.CHECKBOX_GROUP: return { label: filter.label, field: filter.field, count: (filter.value as Array<string | number>)?.length ? (filter.value as Array<string | number>)?.length : null, }; case BentoFilterItemType.TOGGLE: return { label: filter.label, field: filter.field, count: filter.value ? 1 : null, }; case BentoFilterItemType.INPUT: case BentoFilterItemType.BOOLEAN: case BentoFilterItemType.DATE: case BentoFilterItemType.RADIO_GROUP: default: return { label: filter.label, field: filter.field, count: filter.value != null ? 1 : null, }; } }); const hasValueSelected = computed(() => Array.isArray(selectedFilter.value.value) ? selectedFilter.value?.value.length && selectedFilter.value.value.find(item => item !== '') : !!filtersInModal.value[selectedFilterIndex.value].value ); /** * Hides filter title if the selected filter is Toggle. * The Toggle filter shows a checkbox next to the "title". */ const shouldShowFilterTitle = computed(() => { return selectedFilter.value.type !== BentoFilterItemType.TOGGLE; }); const isClearButtonDisabled = computed(() => !hasValueSelected.value || selectedFilter.value.disabled); const numberOfActiveFilters = computed( () => computeCounts(props.value)?.filter(({ count }) => !!count).length ); const onClickAllFiltersModal = () => { if (!isModalOpen.value) { isModalOpen.value = true; } else { isModalOpen.value = false; } }; const getFilterCount = (filter: BentoFilterModel) => { const filterCount = computeCounts(filtersInModal.value).find(el => el.field === filter.field).count; const isBentoSelectFilter = typeof filter.type === 'string' && // Type guard to avoid casting filter.type.includes('BentoSelect') && (filter.options as BentoSelectFilterOptions)?.multiple && filterCount === (filter.options as BentoSelectFilterOptions)?.listboxItems?.length; const isBentoCheckboxGroupFilter = typeof filter.type === 'string' && filter.type === BentoFilterItemType.CHECKBOX_GROUP && filterCount === (filter.options as BentoCheckboxGroupFilterOptions)?.checkboxItems?.length; if (isBentoSelectFilter || isBentoCheckboxGroupFilter) { return t('all'); } return filterCount === null ? null : n(filterCount); }; const selectFilter = (filterLabel: string, filterField: string) => { selectedFilterIndex.value = filtersInModal.value.findIndex( el => el.label === filterLabel && el.field === filterField ); }; const onSearchbarClear = () => { searchTerm.value = ''; }; const cancel = () => { isModalOpen.value = false; filtersInModal.value = props.value; onSearchbarClear(); selectOpenedFilter(); // TODO: scroll filters section to top }; const updateFilter = (updatedFilter: BentoFilterModel) => { const updatedFilters = filtersInModal.value.map(({ value, ...otherFields }, index) => { if (index === selectedFilterIndex.value) { return { value: updatedFilter.value, ...otherFields }; } return { value, ...otherFields }; }); filtersInModal.value = updatedFilters; }; const clearFilter = () => { updateFilter({ field: selectedFilter.value.field, label: selectedFilter.value.label, type: selectedFilter.value.type, value: undefined, }); }; const clearAll = () => { filtersInModal.value = props.value.map(({ value, ...otherFields }) => ({ ...otherFields, value: undefined, })); }; const applyAll = () => { emit(AllFiltersModalEvent.INPUT, filtersInModal.value); isModalOpen.value = false; }; const isApplyAllButtonDisabled = computed( () => JSON.stringify(filtersInModal.value) === JSON.stringify(props.value) ); const modalActions = computed(() => [ { title: t('applyAll'), disabled: isApplyAllButtonDisabled.value, event: applyAll, }, { title: t('cancel'), event: cancel, }, { title: t('clearAll'), disabled: !numberOfActiveFilters.value, event: clearAll, }, ]); const onKeydownRight = () => { const focusableEl = getNextFocusableElement(1, selectedFilterRef.value.$el); focusableEl.focus(); }; const onKeydownLeft = () => { allFiltersModalFilterButtons.value[selectedFilter.value.label].$el.focus(); }; return { // Values allFiltersModalUniqueId, allFiltersModalFiltersSection, allFiltersModalFilterButtons, numberOfActiveFilters, isEmptyResultForSearchFilter, isModalOpen, isClearButtonDisabled, activeFilters, otherFilters, modalActions, selectedFilter, selectedFilterRef, searchTerm, shouldShowFilterTitle, // Methods onClickAllFiltersModal, getFilterCount, cancel, selectFilter, clearFilter, updateFilter, onSearchbarClear, onKeydownRight, onKeydownLeft, // Translations t, }; }, }); </script> <style lang="scss" scoped src="./all-filters-modal.scss" />
@@ -0,0 +1 @@
1
+ <template> <base-filter-button class="b-all-filters-modal-button" :class="conditionalClasses" :amount-applied-values="appliedFiltersCount" :is-filter-open="isFilterOpen" :disabled="disabled" @click="onClick" > {{ label }} </base-filter-button> </template> <script lang="ts"> import { computed, defineComponent } from 'vue'; import BaseFilterButton from '../../../base-filter-button/base-filter-button.vue'; export default defineComponent({ name: 'all-filters-modal-button', components: { BaseFilterButton }, props: { /** * Button label to be displayed */ label: { type: String, required: true }, /** * Total count of applied filters */ appliedFiltersCount: { type: [Number, String], default: null }, /** * Indicates whether the filter popover tied to this button is showing */ isFilterOpen: { type: Boolean, default: false }, /** * Disables button functionality. */ disabled: { type: Boolean, default: false }, }, emits: ['click'], setup(props, { emit }) { const conditionalClasses = computed(() => ({ [`b-all-filters-modal-button--with-counter`]: props.appliedFiltersCount, [`b-all-filters-modal-button--filter-open`]: props.isFilterOpen, })); const onClick = () => { emit('click'); }; return { conditionalClasses, onClick, }; }, }); </script> <style lang="scss" scoped src="./all-filters-modal-button.scss" />
@@ -0,0 +1,90 @@
1
+ import { Canvas, Story, Meta, Source } from '@storybook/blocks';
2
+ import * as BaseFilterStories from './base-filter.stories';
3
+ import SingleValueExample from '../../__tests__/filter-bar-custom-filter-sub-component-single-value-example.vue?raw';
4
+ import MultipleValueExample from '../../__tests__/filter-bar-custom-filter-sub-component-multiple-value-example.vue?raw';
5
+
6
+ <Meta title="Base filter" of={BaseFilterStories} />
7
+
8
+ # Base filter
9
+
10
+ The `BentoBaseFilter` is a wrapper component that provides the basic structure and functionality for creating custom
11
+ filters to be used within the `FilterBar`. It handles the popover, action buttons (Apply, Clear, Reset), and state
12
+ management, allowing you to focus on building the UI and logic for your specific filter.
13
+
14
+ <Canvas withSource="closed">
15
+ <Story of={BaseFilterStories.Default} />
16
+ </Canvas>
17
+
18
+ ## Creating a Custom Filter
19
+
20
+ To create a custom filter, you'll need to wrap your filter's implementation within the `BentoBaseFilter` component. The
21
+ recommended approach is to use the `useBentoBaseFilter` composable to manage the filter's state and interactions.
22
+
23
+ ### Props and Events
24
+
25
+ Your custom filter component should extend the `BentoBaseFilterProps` and `BentoBaseFilterEvents` types. These are
26
+ generic types that you should augment with the type of your filter's value.
27
+
28
+ - `BentoBaseFilterProps<T>`: Use this for your component's props, where `T` is the type of the `value` your filter
29
+ manages (e.g., `boolean`, `string`, `object`).
30
+ - `BentoBaseFilterEvents<T>`: Use this for your component's emits, where `T` is the type of your props interface.
31
+
32
+ ### Mandatory Event Emits
33
+
34
+ It is a mandatory requirement for any custom filter to emit two events when its value is applied: `input` and `update`.
35
+
36
+ - **`emit('update', { ...props, value: internalFilterValue.value })`**: This event is used by the `FilterBar` to update
37
+ its state when a filter is applied directly from the bar.
38
+ - **`emit('input', { ...props, value: internalFilterValue.value })`**: This event is used when the filter is applied
39
+ from within the "All filters" modal.
40
+
41
+ Typically, you will have an `updateFilter` function that is called when the user clicks the "Apply" button, and this
42
+ function will be responsible for emitting both events.
43
+
44
+ ### The `useBentoBaseFilter` Composable
45
+
46
+ The `useBentoBaseFilter<T>` composable simplifies state management. It takes the filter's `value` and `defaultValue` as
47
+ `ref`s and returns:
48
+
49
+ - `internalFilterValue`: A ref holding the temporary state of the filter before it's applied. You should bind this to
50
+ your filter's input controls (e.g., `v-model`).
51
+ - `isApplyButtonDisabled`: A computed ref to determine if the "Apply" button should be disabled.
52
+ - `isSecondaryButtonDisabled`: A computed ref to determine if the "Clear" or "Reset" buttons should be disabled.
53
+ - `clearFilter`: A function to clear the filter's value. Connect this to the `@clear` event on `BentoBaseFilter`.
54
+ - `openFilter`: A function that runs when the filter is opened. Connect this to the `@open` event.
55
+ - `resetFilter`: A function to reset the filter to its default value. Connect this to the `@reset` event.
56
+
57
+ ## Examples
58
+
59
+ Here are two examples demonstrating how to create custom filters using `BentoBaseFilter`.
60
+
61
+ ### Single Value Filter Example
62
+
63
+ This example shows a filter with a single boolean value, controlled by a `BentoToggle`.
64
+
65
+ <Source dark language="html" code={SingleValueExample} />
66
+
67
+ **Key points from the example:**
68
+
69
+ - The component's props and emits are typed with `BentoBaseFilterProps<boolean>` and `BentoBaseFilterEvents`.
70
+ - `useBentoBaseFilter<boolean>` is used to manage the state.
71
+ - The `updateFilter` function is triggered by the `@apply` event and emits both `input` and `update`.
72
+ - A computed property `buttonSecondaryLabel` is used to display the filter's current state ("On") in the filter bar
73
+ button.
74
+
75
+ ### Multiple Value Filter Example
76
+
77
+ This example shows a more complex filter that manages an object with multiple boolean values.
78
+
79
+ <Source dark code={MultipleValueExample} language="html" />
80
+
81
+ **Key points from the example:**
82
+
83
+ - The value type is an object: `ICustomFilterValues`.
84
+ - `appliedFiltersCount` is a ref used to display the number of active selections in the filter button. This is a common
85
+ pattern for multi-select filters.
86
+ - The `updateFilter` function calculates the `appliedFiltersCount` and emits the `input` and `update` events with the
87
+ new value object.
88
+ - The default slot of `BentoBaseFilter` is used to lay out the two `BentoToggle` components.
89
+ - Custom styles are applied to the content in the default slot, as `BentoBaseFilter` does not style this slot by
90
+ default.
@@ -0,0 +1 @@
1
+ import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import BentoBaseFilter from './base-filter.vue'; import type { Meta, StoryObj } from '@storybook/vue'; const meta: Meta = { title: 'Filter bar/Base filter', component: BentoBaseFilter, argTypes: { // Slots default: { name: 'default', type: { name: 'string', required: false }, description: 'Content to be displayed inside the Popover of the filter', control: 'text', table: { type: { summary: 'VNode[]' }, }, }, // Events apply: { name: 'apply', defaultValue: '', description: '`apply` event handler. Called whenever the "Apply" button is clicked.', table: { type: { summary: '() => void' } }, control: { disable: true }, }, clear: { name: 'clear', defaultValue: '', description: '`clear` event handler. Called whenever the "Clear" button is clicked.', table: { type: { summary: '() => void' } }, control: { disable: true }, }, close: { name: 'close', defaultValue: '', description: '`close` event handler. Called whenever the "Apply" button is clicked or when the filter button is clicked while having the Popover open', table: { type: { summary: '() => void' } }, control: { disable: true }, }, dismiss: { name: 'dismiss', defaultValue: '', description: '`dismiss` event handler. Called whenever the "X" (dismiss) button is clicked', table: { type: { summary: '() => void' } }, control: { disable: true }, }, open: { name: 'open', defaultValue: '', description: '`open` event handler. Called whenever the filter button (with the label/value) is clicked while the Popover is closed', table: { type: { summary: '() => void' } }, control: { disable: true }, }, }, }; export default meta; type Story = StoryObj<typeof BentoBaseFilter>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoBaseFilter }, props: Object.keys(argTypes), template: ` <div style="padding-top: 24px"> <bento-base-filter v-bind="args" @apply="applyAction" @clear="clearAction" @close="closeAction" @dismiss="dismissAction" @open="openAction" > <div style="height: 50px; width: 200px"></div> </bento-base-filter> </div> `, setup(props) { return { args: isVue2 ? props : _args, }; }, methods: { applyAction: action('apply'), clearAction: action('clear'), closeAction: action('close'), dismissAction: action('dismissed'), openAction: action('open'), }, }), args: { label: 'Company', field: 'company', value: 'hello', isOpen: true, }, }; export const Controlled: Story = { render: (_args, { argTypes }) => ({ components: { BentoBaseFilter }, props: Object.keys(argTypes), data: () => ({ isFilterOpen: false, }), setup: props => ({ args: isVue2 ? props : _args }), template: ` <div style="padding-top: 24px"> <bento-base-filter v-bind="args" :is-open="isFilterOpen" @apply="applyAction" @dismiss="dismissAction" @clear="clearAction" @close="closeAction" @open="openAction" > <div style="height: 50px; width: 200px"></div> </bento-base-filter> </div> `, methods: { applyAction() { action('apply')(); }, closeAction() { this.isFilterOpen = false; action('close')(); }, clearAction() { action('clear'); }, dismissAction() { action('dismissed')(); }, openAction() { this.isFilterOpen = true; action('open')(); }, }, }), args: { label: 'Company', field: 'company', }, };
@@ -0,0 +1 @@
1
+ <template> <div v-if="onlySlot"> <slot></slot> </div> <div v-else class="b-base-filter"> <filter-bar-button-group v-bento-tooltip-directive:[tooltipPosition]="tooltipText" :is-active="isFilterOpen" :skip-grouping="!isValueChanged" > <filter-bar-button ref="filterButtonRef" :aria-label="computedAriaLabel" :aria-controls="isFilterOpen ? popoverId : null" :applied-filters-count="appliedFiltersCount" :disabled="disabled" :is-persistent-filter="isPersistentFilter" :is-filter-open="isFilterOpen" :secondary-label="buttonSecondaryLabel" @click="onClickFilter" @keydown.esc="closePopover" > {{ computedButtonText }} </filter-bar-button> <filter-bar-button v-if="isValueChanged" :applied-filters-count="appliedFiltersCount" :disabled="disabled" :is-persistent-filter="isPersistentFilter" :is-filter-open="isFilterOpen" :secondary-label="buttonSecondaryLabel" @click="resetFilter" > <template #iconLeft> <cross-small-icon :svg-title="computedClearButtonLabel"></cross-small-icon> </template> </filter-bar-button> </filter-bar-button-group> <bento-teleport v-if="filterButtonRef"> <bento-popover v-if="filterButtonRef" :id="popoverId" ref="popoverRef" class="b-base-filter__popover" divider :aria-label="label" :open="isFilterOpen" :actions="controlled ? null : actions" actions-layout="space-between" :target-element="filterButtonRef" :title="controlled ? null : label" :fit-content="fitContent" position="bottom-start" :without-space="controlled" :fallback-position="['bottom-end']" overflow-visible > <div @keydown.esc="onCancel"> <slot></slot> </div> </bento-popover> </bento-teleport> </div> </template> <script setup lang="ts"> import { computed, nextTick, ref, watch } from 'vue'; import BentoPopover from '@/components/popover/popover.vue'; import { BentoTooltipDirective as vBentoTooltipDirective } from '@/directives/tooltip/tooltip'; import { FilterBarButton } from '../filter-bar-button'; import FilterBarButtonGroup from '../filter-bar-button-group/filter-bar-button-group.vue'; import { useI18n } from '@/utils/ts/i18n'; import { BaseFilterEvent, type BentoBaseFilterProps } from './base-filter.types'; import messages from './messages.json'; import { generateUid } from '@/core/utils/ts/generate-uid'; import { useClickOutside } from '@/composables/use-click-outside'; import BentoTeleport from '@/internal/teleport/teleport.vue'; import type { BentoFilterBarValue } from '../../filter-bar.types'; import CrossSmallIcon from '@adyen/ui-assets-icons-16/vue/cross-small'; type MessageSchema = (typeof messages)['en-US']; const emit = defineEmits([ BaseFilterEvent.APPLY, BaseFilterEvent.CLEAR, BaseFilterEvent.OPEN, BaseFilterEvent.CLOSE, BaseFilterEvent.RESET, BaseFilterEvent.CANCEL, ]); const props = withDefaults(defineProps<BentoBaseFilterProps<BentoFilterBarValue, string>>(), { appliedFiltersCount: null, buttonLabel: undefined, buttonSecondaryLabel: undefined, disabled: false, defaultValue: null, onlySlot: false, controlled: false, disableApplyButton: false, disableSecondaryButton: false, fitContent: undefined, tooltipText: undefined, tooltipPosition: undefined, value: undefined, }); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const popoverId = generateUid('base-filter-popover'); const isFilterOpen = ref(false); const filterButtonRef = ref(null); const popoverRef = ref(null); const isPersistentFilter = computed(() => props.defaultValue !== null); const isValueChanged = computed(() => { // If it's not a persistent filter, its value can't have changed. if (!isPersistentFilter.value) { return props.appliedFiltersCount || props.buttonSecondaryLabel; } // Use JSON.stringify for a simple but effective deep comparison // of objects, arrays, and primitives. return JSON.stringify(props.value) !== JSON.stringify(props.defaultValue); }); const computedAriaLabel = computed(() => { let ariaLabelValuePart = props.buttonSecondaryLabel ? `: ${props.buttonSecondaryLabel}` : ''; if (props.appliedFiltersCount) { ariaLabelValuePart = `: ${t('numberSelected', { numberOfItemsSelected: props.appliedFiltersCount })}`; } return (isPersistentFilter.value ? props.label : computedButtonText.value) + ariaLabelValuePart; }); const computedClearButtonLabel = computed(() => isPersistentFilter.value ? t('resetFilter', { function: computedButtonText.value, }) : t('clearFilter', { function: computedButtonText.value, }) ); const computedButtonText = computed(() => props.buttonLabel || props.label); const resetFocus = async () => { await nextTick(); filterButtonRef.value?.$el?.focus(); }; const closePopover = () => { if (!isFilterOpen.value) { return; } isFilterOpen.value = false; resetFocus(); emit(BaseFilterEvent.CLOSE); }; const resetFilter = () => { if (isPersistentFilter.value) { emit(BaseFilterEvent.RESET); } else { emit(BaseFilterEvent.CLEAR); } resetFocus(); }; const onClickSecondaryAction = () => { closePopover(); resetFilter(); }; const onClickFilter = () => { if (isFilterOpen.value) { closePopover(); } else { isFilterOpen.value = true; emit(BaseFilterEvent.OPEN); } }; const onClickApply = () => { closePopover(); emit(BaseFilterEvent.APPLY); }; const onCancel = () => { closePopover(); emit(BaseFilterEvent.CANCEL); }; const onClickOutside = () => onCancel(); const actions = computed(() => [ { title: t('apply') as string, event: onClickApply, disabled: props.disableApplyButton, }, { title: isPersistentFilter.value ? (t('reset') as string) : (t('clear') as string), event: onClickSecondaryAction, disabled: props.disableSecondaryButton, }, ]); watch( () => isFilterOpen.value, (value, prevValue) => { if (!value && prevValue) { if (filterButtonRef.value?.filterBarButtonRef?.bentoBaseButtonRef) { filterButtonRef.value.filterBarButtonRef.bentoBaseButtonRef.focus(); } } } ); // Ignore the events inside the Button and the Popover useClickOutside(popoverRef, onClickOutside, { ignore: [filterButtonRef] }); defineExpose({ closePopover, }); </script> <script lang="ts"> export default { i18n: { messages }, name: 'bento-base-filter', }; </script> <style lang="scss" scoped src="./base-filter.scss"></style>
@@ -0,0 +1 @@
1
+ <template> <base-button ref="baseFilterButtonRef" class="b-base-filter-button" :class="conditionalClasses" :disabled="disabled" :aria-label="computedAriaLabel" :aria-controls="ariaAttributes['aria-controls']" type="button" @click="onClickButton" > <div class="b-base-filter-button__default-container"> <div class="b-base-filter-button__label-container"> <bento-typography el="span" variant="body" :stronger="bold" class="b-base-filter-button__label"> <slot /> </bento-typography> </div> <div v-if="amountAppliedValues && !hasChevron" class="b-base-filter-button__counter-wrapper"> <bento-typography class="b-base-filter-button__counter" el="span" variant="body" stronger> {{ amountAppliedValues }} </bento-typography> </div> </div> <template v-if="hasChevron" #iconRight> <chevron-up-icon v-if="isFilterOpen" aria-hidden="true" /> <chevron-down-icon v-else aria-hidden="true" /> </template> </base-button> </template> <script lang="ts"> import { BentoTypography } from '@/components/typography'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useHasSlot } from '@/composables'; import { computed, defineComponent, ref } from 'vue'; import { BaseButton, BentoBaseButtonEvent } from '@/components/internal/base-button'; import { getSlotText } from '@/utils/ts/get-slot-text'; import ChevronUpIcon from '@adyen/ui-assets-icons-16/vue/chevron-up'; import ChevronDownIcon from '@adyen/ui-assets-icons-16/vue/chevron-down'; /** * A filter button used as a base to extend all sorts of filter buttons. * Used in the bento-filter-bar and all-filter-modals components. * * @usage * export default { * components: { BaseFilterButton }, * template: ` * <base-filter-button disabled> * Filter * </base-filter-button> * `, * } */ export default defineComponent({ name: 'base-filter-button', components: { BaseButton, BentoTypography, ChevronUpIcon, ChevronDownIcon }, props: { /** * The number of applies values within the filter. * e.g. if 2 items in a dropdown filter have been applied, the number 2 will be shown here. */ amountAppliedValues: { type: [Number, String], default: null }, /** * Disables button functionality. */ disabled: { type: Boolean, default: false }, /** * Signifies if the filter is open or closed. */ isFilterOpen: { type: Boolean, default: false }, /** * Toggles if a chevron icon should be displayed in the button. * The chevron will change depending on the open state of the filter. */ hasChevron: { type: Boolean, default: false }, /** * Renders the text in a stronger variant. */ bold: { type: Boolean, default: false }, }, emits: [BentoBaseButtonEvent.CLICK], setup(props, { slots, emit, attrs }) { const labelSlot = computed((): string => (slots.default ? getSlotText(slots)('default') : null)); const computedAriaLabel = useAriaLabel({ ariaLabel: labelSlot }); const baseFilterButtonRef = ref(null); const ariaAttributes = computed(() => ({ 'aria-controls': attrs['aria-controls']?.toString() as string, })); const conditionalClasses = computed(() => ({ [`b-base-filter-button--with-chevron`]: props.hasChevron, [`b-base-filter-button--filter-open`]: props.isFilterOpen, })); const onClickButton = () => { if (!props.disabled) { emit(BentoBaseButtonEvent.CLICK); } }; return { // Values ariaAttributes, computedAriaLabel, conditionalClasses, // Methods onClickButton, hasSlot: useHasSlot(slots), // Refs baseFilterButtonRef, }; }, }); </script> <style lang="scss" scoped src="./base-filter-button.scss" />
@@ -0,0 +1 @@
1
+ <template> <bento-base-filter v-bind="$props" :value="translatedValue" :default-value="translatedDefaultValue" :button-label="buttonLabel" :button-secondary-label="buttonSecondaryLabel" :disable-apply-button="isApplyButtonDisabled" :disable-secondary-button="isSecondaryButtonDisabled" :only-slot="onlySlot" :tooltip-position="options?.tooltipPosition" :tooltip-text="options?.tooltipText" @apply="updateFilter" @clear="clearFilter" @open="openFilter" @reset="resetFilter" > <div class="b-boolean-filter" :class="conditionalClasses" @keydown.enter="updateFilter"> <bento-radio-group :items="items" :label="label" :model-value="internalFilterValue" :disabled="disabled" hide-label @update:model-value="onInput" ></bento-radio-group> </div> </bento-base-filter> </template> <script lang="ts"> import { computed, defineComponent, onBeforeUpdate, onMounted, type PropType } from 'vue'; import { BentoBaseFilter, useBaseFilter } from '@/components/filter-bar/components/base-filter'; import { BentoRadioGroup } from '@/components/radio-group'; import { useI18n } from '@/utils/ts/i18n'; import { truncate } from '@/utils/ts/truncate'; import { deprecate } from '@/utils/ts/deprecate'; import { type BentoBooleanFilterOptions, BentoFilterEvent } from '../../filter-bar.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; /** * Boolean filter * * @deprecated Use `BentoFilterItemType.RADIO_GROUP` instead. * * @example * import { BentoBooleanFilter } from '@adyen/bento-vue2'; * * export default { * components: { BentoBooleanFilter }, * template: ` * <bento-boolean-filter> * ...Example code of the component * </bento-boolean-filter> * ` * } */ export default defineComponent({ name: 'bento-boolean-filter', i18n: { messages }, components: { BentoBaseFilter, BentoRadioGroup }, props: { /** * Disables the filter from being interacted with. */ disabled: { type: Boolean, default: false }, /** * Default value of the filter to reset to. */ defaultValue: { type: [String, Boolean], default: null }, /** * Filter name/key referencing the actual property name in the data set. * Should be unique per filter bar. */ field: { type: String, required: true }, /** * Label text to be displayed */ label: { type: String, required: true }, /** * Current value of the filter */ value: { type: [String, Boolean], default: null }, /** * Renders the base filter's slot content only - leaving out the button and the popover. * Used for the allFiltersModal component. */ onlySlot: { type: Boolean, default: false }, /** * List of options that are needed to setup the filter * Available options: * - positiveOption: The positive option label/value * - negativeOption: The positive option label/value * - tooltipPosition: The position of the tooltip attached to the filter button * - tooltipText: The content of the tooltip attached to the filter button */ options: { type: Object as PropType<BentoBooleanFilterOptions>, default: undefined, }, }, emits: [BentoFilterEvent.INPUT, BentoFilterEvent.UPDATE], setup(props, { emit }) { const { t } = useI18n<{ message: MessageSchema }>({ messages }); /** * Translates a value, that possibly is a boolean, always to a string. */ const translateBooleanToString = (aStringOrBooleanValue?: string | boolean) => { if (typeof aStringOrBooleanValue === 'boolean') { return aStringOrBooleanValue?.toString(); } return aStringOrBooleanValue; }; /** * Translates a value that back to a boolean if the value property was given as a boolean. */ const translateBackToOriginalValueType = (aStringOrBooleanValue: string | boolean) => { if ( props?.options?.positiveOption?.value !== undefined && props?.options?.negativeOption?.value !== undefined && typeof props.options.positiveOption.value === 'boolean' && typeof props.options.negativeOption.value === 'boolean' && typeof aStringOrBooleanValue === 'string' ) { return aStringOrBooleanValue === 'true'; } return aStringOrBooleanValue; }; const translatedDefaultValue = computed(() => translateBooleanToString(props.defaultValue)); const translatedValue = computed(() => translateBooleanToString(props.value)); const items = computed(() => { if (props.options && props.options.positiveOption && props.options.negativeOption) { return [ { ...props.options.positiveOption, value: translateBooleanToString(props.options.positiveOption.value), }, { ...props.options.negativeOption, value: translateBooleanToString(props.options.negativeOption.value), }, ]; } return [ { value: 'yes', label: t('yes') as string }, { value: 'no', label: t('no') as string }, ]; }); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled } = useBaseFilter( translatedValue, translatedDefaultValue ); const buttonLabel = computed(() => props.label); const buttonSecondaryLabel = computed(() => truncate(items.value.find(({ value }) => value === translatedValue.value)?.label, 12) ); const conditionalClasses = computed(() => ({ [`b-boolean-filter--only-slot`]: props.onlySlot, })); onBeforeUpdate(() => { // Set value to null when the value is unset if (!internalFilterValue.value) { internalFilterValue.value = null; } }); const onInput = event => { internalFilterValue.value = event; emit(BentoFilterEvent.INPUT, { ...props, value: translateBackToOriginalValueType(internalFilterValue.value), }); }; const openFilter = () => { internalFilterValue.value = translatedValue.value; }; const updateFilter = () => { emit(BentoFilterEvent.UPDATE, { ...props, value: translateBackToOriginalValueType(internalFilterValue.value), }); }; const resetFilter = () => emit(BentoFilterEvent.UPDATE, { ...props, value: props.defaultValue }); const clearFilter = () => { internalFilterValue.value = null; updateFilter(); }; /** * Deprecations */ onMounted(() => { deprecate( 'Boolean filter "BentoFilterItemType.BOOLEAN"', `Use the Radio group filter type "BentoFilterItemType.RADIO_GROUP" instead.`, '2.0.0' ); }); return { // Values items, conditionalClasses, buttonLabel, buttonSecondaryLabel, internalFilterValue, isApplyButtonDisabled, isSecondaryButtonDisabled, translatedValue, translatedDefaultValue, // Events onInput, updateFilter, clearFilter, openFilter, resetFilter, }; }, }); </script> <style lang="scss" scoped src="./boolean-filter.scss" />
@@ -0,0 +1 @@
1
+ import { type Meta, type StoryObj } from '@storybook/vue'; import { ref } from 'vue'; import { action } from '@storybook/addon-actions'; import BentoCheckboxGroupFilter from './checkbox-group-filter.vue'; import { isVue2 } from 'vue-demi'; import { type BentoCheckboxGroupFilterProps } from './checkbox-group-filter.types'; const meta: Meta = { title: 'Filter bar/Checkbox Group Filter', component: BentoCheckboxGroupFilter, argTypes: { update: { name: 'update', defaultValue: '', description: '`update` event handler. Called whenever the "Apply" button is clicked.', table: { type: { summary: '({ field, label, value }) => void' } }, control: { disable: true }, }, }, }; export default meta; type Story = StoryObj<typeof BentoCheckboxGroupFilter>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoCheckboxGroupFilter }, props: Object.keys(argTypes), setup(props) { const selectedValues = ref(props.selectedValue); const updateFilter = (newFilterValue: BentoCheckboxGroupFilterProps) => { selectedValues.value = newFilterValue.value; action('update')(newFilterValue); }; return { args: isVue2 ? props : _args, selectedValues, updateFilter, }; }, template: ` <div style="padding-top: 24px"> <bento-checkbox-group-filter v-bind="args" :value="selectedValues" @update="updateFilter" /> </div> `, }), args: { label: 'Status', field: 'status', value: ['active'], options: { checkboxItems: [ { label: 'Active', value: 'active', }, { label: 'Inactive', value: 'inactive', }, { label: 'Pending', value: 'pending', }, ], }, }, }; export const Persistent = { ...Default, args: { ...Default.args, defaultValue: ['active', 'inactive'], }, };
@@ -0,0 +1 @@
1
+ <template> <bento-base-filter v-bind="$props" :applied-filters-count="appliedFiltersCount" :button-label="buttonLabel" :button-secondary-label="buttonSecondaryLabel" :disable-apply-button="isApplyButtonDisabled" :disable-secondary-button="isSecondaryButtonDisabled" :tooltip-position="options?.tooltipPosition" :tooltip-text="options?.tooltipText" @apply="updateFilter(internalFilterValue)" @clear="clearFilter(updateFilter)" @open="openFilter" @reset="resetFilter(updateFilter)" > <div class="b-checkbox-group-filter" :class="conditionalClasses" @keydown.enter="updateFilter(internalFilterValue)" > <bento-checkbox-group :model-value="internalFilterValue" :items="options?.checkboxItems" :label="label" :disabled="disabled" hide-label @update:model-value="onModelValueUpdate" /> </div> </bento-base-filter> </template> <script setup lang="ts"> import { computed, ref, toRef } from 'vue'; import BentoCheckboxGroup from '../../../checkbox/components/checkbox-group/checkbox-group.vue'; import { BentoBaseFilter } from '../base-filter'; import { useBentoBaseFilter } from '@/composables'; import { useI18n } from '@/utils/ts/i18n'; import { truncate } from '@/utils/ts/truncate'; import { type BentoCheckboxGroupFilterProps, type BentoCheckboxGroupFilterValue, } from './checkbox-group-filter.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoCheckboxGroupFilterProps>(), { disabled: false, defaultValue: undefined, onlySlot: false, options: undefined, value: undefined, }); const emit = defineEmits<{ (e: 'input', payload: BentoCheckboxGroupFilterProps): void; (e: 'update', payload: BentoCheckboxGroupFilterProps): void; }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled, clearFilter, resetFilter, openFilter, } = useBentoBaseFilter<BentoCheckboxGroupFilterValue>(toRef(props, 'value'), toRef(props, 'defaultValue')); const displayedValues = ref<BentoCheckboxGroupFilterValue>(internalFilterValue.value ?? []); const conditionalClasses = computed(() => ({ [`b-checkbox-group-filter--only-slot`]: props.onlySlot, })); const buttonLabel = computed(() => props.label); const buttonSecondaryLabel = computed(() => displayedValues.value.length === 1 ? truncate( props.options?.checkboxItems.find(item => displayedValues.value[0] === item.value)?.label ?? '', 12 ) : null ); const appliedFiltersCount = computed(() => { if (!displayedValues.value.length || buttonSecondaryLabel.value) { return null; } if (displayedValues.value.length === props.options?.checkboxItems.length) { return t('all'); } return displayedValues.value.length; }); const updateFilter = (value: BentoCheckboxGroupFilterValue) => { displayedValues.value = value ?? []; emit('update', { ...props, value }); }; const onModelValueUpdate = (value: BentoCheckboxGroupFilterValue) => { internalFilterValue.value = value; emit('input', { ...props, value: internalFilterValue.value }); }; </script> <script lang="ts"> /** * The Checkbox Group filter displays a group of checkboxes that allows * to select multiple values. * * @example * import { BentoCheckboxGroupFilter } from '@adyen/bento-vue2'; * * export default { * components: { BentoCheckboxGroupFilter }, * template: ` * <bento-checkbox-group-filter * :field="filter.field" * :label="filter.label" * :options="filter.options" * :value="filter.value" * @update="updateFilterHandler" * /> * ` * } */ export default { name: 'bento-checkbox-group-filter', i18n: { messages }, }; </script> <style lang="scss" scoped src="./checkbox-group-filter.scss" />
@@ -0,0 +1 @@
1
+ import { type Meta, type StoryObj } from '@storybook/vue'; import { ref } from 'vue'; import { action } from '@storybook/addon-actions'; import BentoDateFilter from './date-filter.vue'; import { BentoDatePickerFirstDayOfWeek } from '@/components/date-picker'; import { isVue2 } from 'vue-demi'; const meta: Meta = { title: 'Filter bar/Date filter', component: BentoDateFilter, argTypes: { update: { name: 'update', defaultValue: '', description: '`update` event handler. Called whenever the "Apply" button is clicked.', table: { type: { summary: '({ field, label, value }) => void' } }, control: { disable: true }, }, }, }; export default meta; type Story = StoryObj<typeof BentoDateFilter>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDateFilter }, props: Object.keys(argTypes), setup(props) { const selectedValues = ref(props.selectedValue); const updateFilter = newFilterValue => { action('update')(newFilterValue); selectedValues.value = newFilterValue.value; }; return { args: isVue2 ? props : _args, selectedValues, updateFilter, }; }, template: ` <div style="padding-top: 24px"> <bento-date-filter v-bind="args" :value="selectedValues" @update="updateFilter" /> </div> `, }), args: { label: 'Date', field: 'date', selectedValue: new Date(), options: { firstDayOfWeek: BentoDatePickerFirstDayOfWeek.MONDAY, isDateDisabled: () => false, }, }, }; export const Persistent = { ...Default, args: { ...Default.args, defaultValue: new Date(), }, };
@@ -0,0 +1 @@
1
+ <template> <bento-base-filter :button-label="buttonLabel" :button-secondary-label="buttonSecondaryLabel" :disabled="disabled" :default-value="defaultValue" :disable-apply-button="isApplyButtonDisabled" :disable-secondary-button="isSecondaryButtonDisabled" :field="field" :label="label" :only-slot="onlySlot" :tooltip-position="options?.tooltipPosition" :tooltip-text="options?.tooltipText" fit-content @apply="updateFilter" @clear="clearFilter" @open="openFilter" @reset="resetFilter" > <bento-date-picker v-if="onlySlot" :value="internalFilterValue" :first-day-of-week="options?.firstDayOfWeek" :is-date-disabled="options?.isDateDisabled" :variant="options?.variant" :disabled="disabled" @input="onDateInputSelected" /> <template v-else> <bento-divider class="b-date-filter__divider" /> <div class="b-date-filter"> <calendar :first-day-of-week="options?.firstDayOfWeek" :is-date-disabled="options?.isDateDisabled" :number-of-months="1" :value="internalFilterValue" :variant="options?.variant" @input="onDateSelected" /> </div> </template> </bento-base-filter> </template> <script lang="ts"> import { computed, defineComponent, type PropType, toRef } from 'vue'; import { BentoBaseFilter, useBaseFilter } from '../base-filter'; import { type BentoDateFilterOptions, type BentoDateFilterValue, BentoFilterEvent } from '../../filter-bar.types'; import { Calendar } from '@/internal'; import { BentoDivider } from '@/components/divider'; import { BentoDatePicker } from '@/components/date-picker'; import { useI18n } from '@/utils/ts/i18n'; import { getDateTimeFormatter } from '@/utils/ts/format-date'; /** * The Date filter displays a calendar that allows * to select a single date. * * @example * import { BentoDateFilter } from '@adyen/bento-vue2'; * * export default { * components: { BentoDateFilter }, * template: ` * <bento-date-filter * :field="filter.field" * :label="filter.label" * :options="filter.options" * :value="filter.value" * @update="updateFilterHandler" * > * ` * } */ export default defineComponent({ name: 'bento-date-filter', components: { BentoBaseFilter, BentoDatePicker, BentoDivider, Calendar, }, props: { /** * Disables the filter from being interacted with. */ disabled: { type: Boolean, default: false }, isDateInput: { type: Boolean, default: true }, /** * Default value of the filter to reset to. */ defaultValue: { type: Date as PropType<BentoDateFilterValue>, default: null }, /** * Filter name/key referencing the actual property name in the data set. * Should be unique per filter bar. */ field: { type: String, required: true }, /** * Label text to be displayed */ label: { type: String, required: true }, /** * Renders the base filter's slot content only - leaving out the button and the popover. * Used for the allFiltersModal component. */ onlySlot: { type: Boolean, default: false }, /** * List of options that are needed to setup the filter * Available options: * - firstDayOfWeek: Allows you to set the first day of the week. 0 is sunday, 1 is monday, 6 is saturday * - isDateDisabled: Indicate if a date should be disabled or not * - tooltipPosition: The position of the tooltip attached to the filter button * - tooltipText: The content of the tooltip attached to the filter button */ options: { type: Object as PropType<BentoDateFilterOptions>, default: undefined, }, /** * Current value of the filter */ value: { type: Date as PropType<BentoDateFilterValue>, default: undefined }, }, emits: [BentoFilterEvent.INPUT, BentoFilterEvent.UPDATE], setup(props, { emit }) { const { locale } = useI18n(undefined, true); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled } = useBaseFilter( toRef(props, 'value'), toRef(props, 'defaultValue') ); const formattedButtonDateText = computed(() => { const dateFormatOptions: Intl.DateTimeFormatOptions = props?.options?.variant === 'month' ? { month: 'long', year: 'numeric' } : { day: 'numeric', month: 'short', year: 'numeric' }; return props.value ? getDateTimeFormatter(locale.value, dateFormatOptions).format(props.value) : null; }); const buttonLabel = computed(() => props.label); const buttonSecondaryLabel = computed(() => formattedButtonDateText.value); const updateFilter = () => { emit(BentoFilterEvent.UPDATE, { ...props, value: internalFilterValue.value }); }; const clearFilter = () => { internalFilterValue.value = null; updateFilter(); }; const resetFilter = () => { internalFilterValue.value = props.defaultValue; updateFilter(); }; const openFilter = () => { internalFilterValue.value = props.value; }; const onDateSelected = (selectedDate: Date) => { internalFilterValue.value = selectedDate; emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; // Update internal date and emit selected date when using date-picker input const onDateInputSelected = (selectedDate: Date) => { internalFilterValue.value = selectedDate; onDateSelected(selectedDate); updateFilter(); }; return { // Values internalFilterValue, isApplyButtonDisabled, isSecondaryButtonDisabled, buttonLabel, buttonSecondaryLabel, // Events clearFilter, onDateSelected, openFilter, updateFilter, resetFilter, onDateInputSelected, }; }, }); </script> <style lang="scss" scoped src="./date-filter.scss" />
@@ -0,0 +1 @@
1
+ import { ref } from 'vue'; import { action } from '@storybook/addon-actions'; import BentoDateRangeFilter from './date-range-filter.vue'; import { subDays } from 'date-fns/subDays'; import { BentoDatePickerFirstDayOfWeek } from '@/components/date-picker'; import { isVue2 } from 'vue-demi'; export default { title: 'Filter bar/Date range filter', component: BentoDateRangeFilter, argTypes: { update: { name: 'update', defaultValue: '', description: '`update` event handler. Called whenever the "Apply" button is clicked.', table: { type: { summary: '({ field, label, value }) => void' } }, control: { disable: true }, }, }, }; const Template = (_args, { argTypes }) => ({ components: { BentoDateRangeFilter }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const selectedValues = ref(args.selectedValue); const updateFilter = newFilterValue => { action('update')(newFilterValue); selectedValues.value = newFilterValue.value; }; return { selectedValues, updateFilter, // Vue 3 storybook fixes args, }; }, template: ` <div style="padding-top: 24px"> <bento-date-range-filter v-bind="args" :value="selectedValues" @update="updateFilter" /> </div> `, }); export const Default = Template.bind({}); Default.args = { label: 'Date range', field: 'date-range', selectedValue: { startDate: subDays(new Date(), 2), endDate: new Date() }, options: { firstDayOfWeek: BentoDatePickerFirstDayOfWeek.MONDAY, numberOfMonths: 2, }, }; export const Persistent = Template.bind({}); Persistent.args = { ...Default.args, defaultValue: new Date(), }; export const MonthVariant = Template.bind({}); MonthVariant.args = { ...Default.args, defaultValue: new Date(), options: { variant: 'month', granularities: [{ type: 'monthly' }, { type: 'quarterly' }], }, };
@@ -0,0 +1 @@
1
+ <template> <bento-base-filter ref="baseFilter" class="b-base-filter-date-range-filter" :button-label="buttonLabel" :button-secondary-label="buttonSecondaryLabel" :controlled="!onlySlot" :disabled="disabled" :disable-apply-button="isApplyButtonDisabled" :disable-secondary-button="isSecondaryButtonDisabled" :default-value="defaultValue" :field="field" :label="label" :only-slot="onlySlot" :tooltip-position="options?.tooltipPosition" :tooltip-text="options?.tooltipText" fit-content @open="openFilter" @clear="clearFilter" @reset="resetFilter" @cancel="onCancel" > <bento-date-range-picker v-if="onlySlot" :allow-time-input="options?.allowTimeInput" :model-value="internalFilterValue" :first-day-of-week="options?.firstDayOfWeek" :is-date-disabled="options?.isDateDisabled" :number-of-months="options?.numberOfMonths" :quick-select-ranges="options?.quickSelectRanges" :variant="options?.variant" :disabled="disabled" :granularities="options?.granularities" :max-range="options?.maxRange" :min="options?.min" :max="options?.max" :date-form-data="formData" :show-end-date-on-open="options?.showEndDateOnOpen" @update:model-value="onDateInputSelected" /> <template v-else> <div class="b-date-range-filter"> <date-range-picker-calendar :allow-time-input="options?.allowTimeInput" :first-day-of-week="options?.firstDayOfWeek" :is-date-disabled="options?.isDateDisabled" :granularities="options?.granularities" :max-range="options?.maxRange" :min="options?.min" :max="options?.max" :number-of-months="options?.numberOfMonths" :quick-select-ranges="adjustedQuickSelectRanges" :value="internalFilterValue" :date-form-data="formData" :show-end-date-on-open="options?.showEndDateOnOpen" :variant="options?.variant" @input="onDateSelected" @custom-range="onRangeSelectorInput" @error="onDatePickerRangeError" @form-date="onDatePickerRangeFormDateInputUpdate" @start-date-selected="isSelectingDate = true" > <template #title> <bento-typography strongest variant="body" el="span"> {{ label }} </bento-typography> </template> <template #actions> <bento-divider class="b-date-range-filter__divider" /> <bento-button-actions class="b-date-range-filter__actions" :actions="actions" layout="space-between" /> </template> </date-range-picker-calendar> </div> </template> </bento-base-filter> </template> <script lang="ts"> import { computed, defineComponent, type PropType, reactive, ref, toRef, watch } from 'vue'; import { BentoBaseFilter, useBaseFilter } from '../base-filter'; import { type BentoDateRangeFilterOptions, type BentoDateRangeFilterValue, BentoFilterEvent, } from '../../filter-bar.types'; import { DateRangePickerCalendar } from '@/components/date-range-picker/components/date-range-picker-calendar'; import { BentoDateRangePicker, type BentoDateRangePickerValue } from '@/components/date-range-picker'; import { BentoDivider } from '@/components/divider'; import { BentoTypography } from '@/components/typography'; import { BentoButtonActions, type BentoButtonActionsList } from '@/components/button/components/button-actions'; import { useI18n } from '@/utils/ts/i18n'; import { getDateLocale } from '@/utils/ts/get-date-locale/get-date-locale'; import { DateFormat } from '@/types/date-format'; import { isSameDay } from 'date-fns/isSameDay'; import { format } from 'date-fns/format'; import { isEqual } from 'date-fns/isEqual'; import { isToday } from 'date-fns/isToday'; import messages from './messages.json'; import { type DateRangePickerCalendarFormData, type DateRangePickerCalendarRangeSelectorItem, type DateRangePickerCalendarRangeSelectorItems, } from '@/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.types'; type MessageSchema = (typeof messages)['en-US']; /** * The Date filter displays a calendar that allows * to select a single date. * * @example * import { BentoDateRangeFilter } from '@adyen/bento-vue2'; * * export default { * components: { BentoDateRangeFilter }, * template: ` * <bento-date-range-filter * :field="filter.field" * :label="filter.label" * :options="filter.options" * :value="filter.value" * @update="updateFilterHandler" * > * ` * } */ export default defineComponent({ i18n: { messages }, name: 'bento-date-range-filter', components: { BentoBaseFilter, BentoButtonActions, BentoDateRangePicker, BentoTypography, BentoDivider, DateRangePickerCalendar, }, props: { /** * Disables the filter from being interacted with. */ disabled: { type: Boolean, default: false }, /** * Default value of the filter to reset to. */ defaultValue: { type: Object as PropType<BentoDateRangeFilterValue>, default: null }, /** * Filter name/key referencing the actual property name in the data set. * Should be unique per filter bar. */ field: { type: String, required: true }, /** * Label text to be displayed */ label: { type: String, required: true }, /** * Renders the base filter's slot content only - leaving out the button and the popover. * Used for the allFiltersModal component. */ onlySlot: { type: Boolean, default: false }, /** * List of options that are needed to setup the filter * Available options: * - allowTimeInput: Allows user to enter time values in the form * - firstDayOfWeek: Allows user to set the first day of the week. 0 is sunday, 1 is monday, 6 is saturday * - isDateDisabled: Indicate if a date should be disabled or not * - maxRange: Set a maximum number of dates to be selectable by the range * - numberOfMonths: Number of months rendered on pane * - quickSelectRanges: An array that sets the custom range dropdown items. @see BentoDateRangePicker * - showEndDateOnOpen: Upon opening date range picker, end date's month will be pre-selected. * - granularities: A list of available granularities. * - variant: The type of calendar to display. Defaults to showing days. * - tooltipPosition: The position of the tooltip attached to the filter button * - tooltipText: The content of the tooltip attached to the filter button */ options: { type: Object as PropType<BentoDateRangeFilterOptions>, default: undefined, }, /** * Current value of the filter */ value: { type: Object as PropType<BentoDateRangeFilterValue>, default: () => ({ startDate: undefined, endDate: undefined }), }, }, emits: [BentoFilterEvent.INPUT, BentoFilterEvent.UPDATE], setup(props, { emit }) { const { locale, t } = useI18n<{ message: MessageSchema }>({ messages }); const locales = getDateLocale(); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled } = useBaseFilter( toRef(props, 'value'), toRef(props, 'defaultValue') ); const isSelectingDate = ref(false); const adjustedQuickSelectRanges = computed<Array<DateRangePickerCalendarRangeSelectorItem>>(() => { return ( props?.options?.quickSelectRanges && props.options.quickSelectRanges.map((range: DateRangePickerCalendarRangeSelectorItem) => { const timeDiff = !range.data.endDate ? new Date(Date.now()).getTime() - range.data.startDate.getTime() : undefined; return { ...range, data: { ...range.data, // Adding timeDifference to calculate an up-to-date endDate on range selection timeDifference: timeDiff, }, }; }) ); }); const formatFormDate = (date: Date) => date ? format(date, DateFormat.NUMERIC_DATE_YEAR_FIRST, { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, }) : undefined; const formatFormTime = (dateToFormat?: Date) => { if (!dateToFormat) { return ''; } return format(dateToFormat, DateFormat.HOUR_MINUTE_SECONDS, { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, }); }; const baseFilter = ref(null); const isDateIncorrect = ref(false); const formData = reactive({ startDate: formatFormDate(props.value?.startDate), endDate: formatFormDate(props.value?.endDate), startTime: formatFormTime(props.value?.startDate), endTime: formatFormTime(props.value?.endDate), }); watch( () => props.value, () => { formData.startDate = formatFormDate(props.value?.startDate); formData.endDate = formatFormDate(props.value?.endDate); formData.startTime = formatFormTime(props.value?.startDate); formData.endTime = formatFormTime(props.value?.endDate); } ); function isQuickSelectAnArray( quickSelectRanges: BentoDateRangeFilterOptions['quickSelectRanges'] ): quickSelectRanges is DateRangePickerCalendarRangeSelectorItems { return (quickSelectRanges as DateRangePickerCalendarRangeSelectorItems)?.length !== undefined; } const calculateSecondaryLabel = (currentValue: BentoDateRangeFilterValue) => { if (!currentValue || (!currentValue?.startDate && !currentValue.endDate)) { return null; } // If quick select range selected then display its label if ( isQuickSelectAnArray(props.options?.quickSelectRanges) && props.options?.quickSelectRanges?.length > 0 ) { const customRangeSelected = props.options?.quickSelectRanges?.find(({ value, data }) => { if (currentValue) { if (currentValue.range) { return value === currentValue.range; } if (!currentValue.startDate || !currentValue.endDate) { return false; } if (!data?.endDate) { // Autoselecting the correct value for open-ended ranges return ( isEqual(data?.startDate, currentValue.startDate) && isToday(currentValue.endDate) ); } // If no range selected but start and end dates correspond to one of the preselected ranges, select it return ( isEqual(data?.startDate, currentValue.startDate) && isEqual(data?.endDate, currentValue.endDate) ); } return false; }); if (customRangeSelected) { return customRangeSelected.label; } } if (!currentValue.startDate || !currentValue.endDate) { return null; } return isSameDay(currentValue.startDate, currentValue.endDate) && props?.options?.allowTimeInput ? `${format(currentValue.startDate, 'dd MMM, yyyy HH:mm:ss', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })} - ${format(currentValue.endDate, 'HH:mm:ss', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })}` : `${format(currentValue.startDate, 'dd MMM, yyyy', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })} - ${format(currentValue.endDate, 'dd MMM, yyyy', { locale: locales[locale.value?.substring(0, 2)] || locales.enUS, })}`; }; const buttonSecondaryLabel = ref(calculateSecondaryLabel(props.value)); const buttonLabel = computed(() => props.label); const updateFilter = () => { baseFilter.value.closePopover(); // Omit the onlySlot prop as it is not needed const { onlySlot, ...cleanedProps } = props; emit(BentoFilterEvent.UPDATE, { ...cleanedProps, value: internalFilterValue.value }); }; const clearFilter = () => { internalFilterValue.value = undefined; updateFilter(); }; const resetFilter = () => { internalFilterValue.value = props.defaultValue; updateFilter(); }; const openFilter = () => { internalFilterValue.value = props.value; }; // If we click outside or press escape this should cancel and reset the form inputs // set all internal states back to their initial values const onCancel = () => { internalFilterValue.value = props.value; isSelectingDate.value = false; formData.startDate = formatFormDate(props.value?.startDate); formData.endDate = formatFormDate(props.value?.endDate); formData.startTime = formatFormTime(props.value?.startDate); formData.endTime = formatFormTime(props.value?.endDate); }; const onDateSelected = (selectedDate: BentoDateRangePickerValue) => { isSelectingDate.value = false; internalFilterValue.value = selectedDate; formData.startDate = formatFormDate(selectedDate.startDate); formData.endDate = formatFormDate(selectedDate.endDate); emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const onRangeSelectorInput = (quickSelectRanges?: { startDate: Date; endDate: Date }) => { if (quickSelectRanges?.startDate && quickSelectRanges?.endDate) { internalFilterValue.value = quickSelectRanges; formData.startDate = formatFormDate(quickSelectRanges.startDate); formData.endDate = formatFormDate(quickSelectRanges.endDate); emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); } }; // Need to update the internal value and emit the date when using the date-picker input const onDateInputSelected = (selectedDate: BentoDateRangePickerValue) => { onDateSelected(selectedDate); emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const actions = computed( () => [ { title: t('apply') as string, event: updateFilter, disabled: isSelectingDate.value || isDateIncorrect.value || isApplyButtonDisabled.value, }, { title: (props.defaultValue ? t('reset') : t('clear')) as string, event: (props.defaultValue ? resetFilter : clearFilter) as () => void, disabled: isDateIncorrect.value && isSecondaryButtonDisabled.value, }, ] as BentoButtonActionsList ); const onDatePickerRangeError = () => { isDateIncorrect.value = true; internalFilterValue.value = { startDate: undefined, endDate: undefined }; emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const onDatePickerRangeFormDateInputUpdate = (dateFormDate: DateRangePickerCalendarFormData) => { isDateIncorrect.value = false; if (dateFormDate.startTime) { formData.startTime = dateFormDate.startTime; } if (dateFormDate.endTime) { formData.endTime = dateFormDate.endTime; } }; watch( () => props.value, newValue => { buttonSecondaryLabel.value = calculateSecondaryLabel(newValue); }, { deep: true } ); return { // Refs baseFilter, // Values actions, formData, internalFilterValue, isApplyButtonDisabled, isSecondaryButtonDisabled, isSelectingDate, buttonLabel, buttonSecondaryLabel, adjustedQuickSelectRanges, // Events onCancel, onDateSelected, openFilter, clearFilter, resetFilter, updateFilter, onDatePickerRangeError, onDatePickerRangeFormDateInputUpdate, onDateInputSelected, onRangeSelectorInput, }; }, }); </script> <style lang="scss" scoped src="./date-range-filter.scss" />
@@ -0,0 +1 @@
1
+ <template> <base-button ref="filterBarButtonRef" v-bind="$attrs" class="b-filter-bar-button" :class="conditionalClasses" :disabled="disabled" :aria-label="computedAriaLabel" :aria-controls="ariaAttributes['aria-controls']" type="button" @click="onClickButton" > <div class="b-filter-bar-button__container"> <template v-if="hasSlot('iconLeft')"> <slot name="iconLeft" /> </template> <div class="b-filter-bar-button__label-container"> <bento-typography el="span" variant="body" stronger class="b-filter-bar-button__label"> <span> <slot /> </span> <bento-typography v-if="secondaryLabel && !isIconOnly" el="span" variant="body" stronger class="b-filter-bar-button__label" >:</bento-typography > </bento-typography> <bento-typography v-if="secondaryLabel && !isIconOnly" el="span" variant="body" stronger class="b-filter-bar-button__secondary-label" > {{ secondaryLabel }} </bento-typography> </div> </div> <template #iconRight> <bento-typography v-if="appliedFiltersCount && !isIconOnly" class="b-filter-bar-button__counter" el="span" variant="body" stronger > {{ appliedFiltersCount }} </bento-typography> </template> </base-button> </template> <script setup lang="ts"> import { BaseButton } from '@/components/internal/base-button'; import { BentoTypography } from '@/components/typography'; import { useHasSlot } from '@/composables'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { computed, ref, useAttrs, useSlots } from 'vue'; const props = defineProps({ /** * Allows to set the button as active without setting a "secondaryLabel" */ active: { type: Boolean, default: false }, /** * The number of applied values within the filter. * Text can also be applied here such as 'All'. * e.g. if 2 items in a dropdown filter have been applied, the number 2 will be shown here. */ appliedFiltersCount: { type: [String, Number], default: null }, /** * Disables button functionality. */ disabled: { type: Boolean, default: false }, /** * Signifies if the filter is open or closed. */ isFilterOpen: { type: Boolean, default: false }, /** * Signifies if the filter is a persistent filter */ isPersistentFilter: { type: Boolean, default: false }, /** * Renders the button without the applied state styles */ noAppliedStateStyles: { type: Boolean, default: false }, /** * A secondary label to be added next to the primary for additional filtering information. * i.e. Can be used to show the set value for said filter. */ secondaryLabel: { type: String, default: null }, }); const emit = defineEmits<{ /** * Emitted when the button is clicked */ (e: 'click', event: MouseEvent); }>(); const slots = useSlots(); const attrs = useAttrs(); const hasSlot = useHasSlot(slots); const labelSlot = computed((): string => (slots.default ? getSlotText(slots)('default') : null)); const isIconOnly = computed(() => !hasSlot('default') && hasSlot('iconLeft')); const computedAriaLabel = useAriaLabel({ ariaLabel: labelSlot }); const filterBarButtonRef = ref(null); const ariaAttributes = computed(() => ({ 'aria-controls': attrs['aria-controls']?.toString() as string, })); const isAppliedFilter = computed( () => (props.appliedFiltersCount || props.secondaryLabel || props.isPersistentFilter) && !props.noAppliedStateStyles ); const conditionalClasses = computed(() => ({ 'b-filter-bar-button--applied': isAppliedFilter.value || props.active, 'b-filter-bar-button--filter-open': props.isFilterOpen && !isAppliedFilter.value, 'b-filter-bar-button--filter-open-applied': props.isFilterOpen && isAppliedFilter.value, 'b-filter-bar-button--with-secondary-label': props.secondaryLabel, 'b-filter-bar-button--with-counter': props.appliedFiltersCount, 'b-filter-bar-button--icon-only': isIconOnly.value, })); const onClickButton = (event: MouseEvent) => { event.preventDefault(); if (!props.disabled) { emit('click', event); } }; </script> <script lang="ts"> /** * A filter button used as a base to extend all sorts of filter buttons. * Used in the bento-filter-bar and all-filter-modals components. * * @usage * export default { * components: { FilterBarButton }, * template: ` * <filter-bar-button> * Filter * </filter-bar-button> * `, * } */ export default {}; </script> <style lang="scss" scoped src="./filter-bar-button.scss" />
@@ -0,0 +1 @@
1
+ <template> <div class="b-filter-bar-button-group" :class="conditionalClasses"> <slot></slot> </div> </template> <script setup lang="ts"> import { type BentoFilterBarButtonGroupProps } from './filter-bar-button-group.types'; import { computed } from 'vue'; const props = withDefaults(defineProps<BentoFilterBarButtonGroupProps>(), { isActive: false, skipGrouping: false, }); const conditionalClasses = computed(() => ({ 'b-filter-bar-button-group--active': props.isActive, 'b-filter-bar-button-group--grouped': !props.skipGrouping, })); </script> <script lang="ts"> /** * A button group used to group buttons in the filter bar. * * @usage * import { FilterBarButtonGroup } from '@adyen/bento-vue2'; * * export default { * components: { FilterBarButtonGroup }, * template: ` * <filter-bar-button-group> * <filter-bar-button>Filter</filter-bar-button> * <filter-bar-button>Clear</filter-bar-button> * </filter-bar-button-group> * `, * } */ export default {}; </script> <style lang="scss" scoped src="./filter-bar-button-group.scss" />
@@ -0,0 +1,18 @@
1
+ import { Canvas, Story, Meta } from '@storybook/blocks';
2
+ import { ArgTypes } from '@storybook/blocks';
3
+ import * as InputFilterStories from './input-filter.stories';
4
+
5
+ <Meta of={InputFilterStories} />
6
+
7
+ # Input filter
8
+
9
+ A **TextFilter** component provides a filter text input and allows you to filter and render a list of data based on the
10
+ entered filter text.
11
+
12
+ <Canvas>
13
+ <Story of={InputFilterStories.Default} />
14
+ </Canvas>
15
+
16
+ ## Props
17
+
18
+ <ArgTypes of={InputFilterStories} />
@@ -0,0 +1 @@
1
+ import { action } from '@storybook/addon-actions'; import BentoInputFilter from './input-filter.vue'; import { ref } from 'vue'; import { isVue2 } from 'vue-demi'; export default { title: 'Filter bar/Input filter', component: BentoInputFilter, argTypes: { dismiss: { name: 'dismiss', defaultValue: '', description: '`dismiss` event handler. Called whenever the "X" (dismiss) button is clicked or the value is set to empty (null)', table: { type: { summary: '({ field, label, value }) => void' } }, control: { disable: true }, }, update: { name: 'update', defaultValue: '', description: '`update` event handler. Called whenever the "Apply" button is clicked.', table: { type: { summary: '({ field, label, value }) => void' } }, control: { disable: true }, }, }, }; export const Default = (_args, { argTypes }) => ({ components: { BentoInputFilter }, props: Object.keys(argTypes), template: ` <div style="padding-top: 24px"> <bento-input-filter v-bind="args" :value="value" @update="updateFilter" @dismiss="dismissFilter" /> </div> `, setup(props) { const value = ref(null); const updateFilter = newFilterValue => { action('update')(newFilterValue); value.value = newFilterValue.value; }; const dismissFilter = newFilterValue => { action('dismiss')(newFilterValue); value.value = null; }; return { args: isVue2 ? props : _args, value, updateFilter, dismissFilter, }; }, }); Default.args = { label: 'Company', field: 'company', };