@adyen/bento-mcp 0.10.0 → 0.12.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 (429) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +24 -2
  3. package/SKILL.md +37 -7
  4. package/dist/assets/components/accordion/accordion.stories.ts +1 -1
  5. package/dist/assets/components/accordion/components/accordion-item.stories.ts +1 -1
  6. package/dist/assets/components/accordion/components/accordion-item.vue +1 -1
  7. package/dist/assets/components/action-bar/action-bar.docs.mdx +10 -3
  8. package/dist/assets/components/action-bar/action-bar.internal.types.ts +1 -0
  9. package/dist/assets/components/action-bar/action-bar.stories.ts +1 -1
  10. package/dist/assets/components/action-bar/action-bar.types.ts +1 -1
  11. package/dist/assets/components/action-bar/action-bar.vue +1 -1
  12. package/dist/assets/components/ai-tag/ai-tag.stories.ts +1 -1
  13. package/dist/assets/components/ai-tag/ai-tag.vue +1 -1
  14. package/dist/assets/components/alert/alert.stories.ts +1 -1
  15. package/dist/assets/components/alert/alert.types.ts +1 -1
  16. package/dist/assets/components/anchor-scroller/anchor-scroller.stories.ts +1 -1
  17. package/dist/assets/components/anchor-scroller/anchor-scroller.vue +1 -1
  18. package/dist/assets/components/avatar/avatar.internal.types.ts +1 -0
  19. package/dist/assets/components/avatar/avatar.stories.ts +1 -1
  20. package/dist/assets/components/avatar/avatar.types.ts +1 -1
  21. package/dist/assets/components/button/button.docs.mdx +4 -3
  22. package/dist/assets/components/button/button.stories.ts +1 -1
  23. package/dist/assets/components/button/button.types.ts +1 -1
  24. package/dist/assets/components/button/components/ai-button/ai-button.stories.ts +1 -1
  25. package/dist/assets/components/button/components/ai-button/ai-button.vue +1 -1
  26. package/dist/assets/components/button/components/button-actions/button-actions.docs.mdx +3 -0
  27. package/dist/assets/components/button/components/button-actions/button-actions.internal.types.ts +1 -0
  28. package/dist/assets/components/button/components/button-actions/button-actions.stories.ts +1 -1
  29. package/dist/assets/components/button/components/button-actions/button-actions.types.ts +1 -1
  30. package/dist/assets/components/button/components/button-actions/button-actions.vue +1 -1
  31. package/dist/assets/components/button/components/loading-button/loading-button.docs.mdx +26 -23
  32. package/dist/assets/components/button/components/loading-button/loading-button.stories.ts +1 -1
  33. package/dist/assets/components/button/components/loading-button/loading-button.vue +1 -1
  34. package/dist/assets/components/button/components/toggle-button/toggle-button.stories.ts +1 -1
  35. package/dist/assets/components/card/card.docs.mdx +18 -1
  36. package/dist/assets/components/card/card.stories.ts +1 -1
  37. package/dist/assets/components/card/card.types.ts +1 -1
  38. package/dist/assets/components/card/card.vue +1 -1
  39. package/dist/assets/components/checkbox/checkbox.stories.ts +1 -1
  40. package/dist/assets/components/checkbox/checkbox.types.ts +1 -1
  41. package/dist/assets/components/checkbox/checkbox.vue +1 -1
  42. package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.docs.mdx +9 -14
  43. package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.stories.ts +1 -1
  44. package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.types.ts +1 -1
  45. package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.vue +1 -1
  46. package/dist/assets/components/chip/chip.stories.ts +1 -1
  47. package/dist/assets/components/chip/chip.vue +1 -1
  48. package/dist/assets/components/click-outside/click-outside.stories.ts +1 -1
  49. package/dist/assets/components/code-snippet/code-snippet.types.ts +1 -1
  50. package/dist/assets/components/code-snippet/code-snippet.vue +1 -1
  51. package/dist/assets/components/country/country.stories.ts +1 -1
  52. package/dist/assets/components/country/country.types.ts +1 -1
  53. package/dist/assets/components/country/country.vue +1 -1
  54. package/dist/assets/components/currency/currency.stories.ts +1 -1
  55. package/dist/assets/components/currency/currency.vue +1 -1
  56. package/dist/assets/components/dashed-underline/dashed-underline.stories.ts +1 -1
  57. package/dist/assets/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.types.ts +1 -1
  58. package/dist/assets/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.vue +1 -1
  59. package/dist/assets/components/data-grid/components/data-grid-config-settings/data-grid-config-settings.vue +1 -1
  60. package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.vue +1 -1
  61. package/dist/assets/components/data-grid/components/data-grid-static-icon/data-grid-static-icon.stories.ts +1 -1
  62. package/dist/assets/components/data-grid/components/data-grid-summary-cell/data-grid-summary-cell.stories.ts +1 -1
  63. package/dist/assets/components/data-grid/components/data-grid-summary-cell/data-grid-summary-cell.vue +1 -1
  64. package/dist/assets/components/data-grid/components/data-grid-tag-with-text-cell/data-grid-tag-with-text-cell.stories.ts +1 -1
  65. package/dist/assets/components/data-grid/components/data-grid-trend-cell/data-grid-trend-cell.stories.ts +1 -1
  66. package/dist/assets/components/data-grid/components/data-grid-trend-cell/data-grid-trend-cell.vue +1 -1
  67. package/dist/assets/components/data-grid/data-grid.docs.mdx +3 -60
  68. package/dist/assets/components/data-grid/data-grid.stories.ts +1 -1
  69. package/dist/assets/components/data-grid/data-grid.vue +1 -1
  70. package/dist/assets/components/date/date.stories.ts +1 -1
  71. package/dist/assets/components/date-picker/date-picker.stories.ts +1 -1
  72. package/dist/assets/components/date-picker/date-picker.types.ts +1 -1
  73. package/dist/assets/components/date-picker/date-picker.vue +1 -1
  74. package/dist/assets/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.vue +1 -1
  75. package/dist/assets/components/date-range-picker/components/date-time-input/date-time-input.types.ts +1 -1
  76. package/dist/assets/components/date-range-picker/components/date-time-input/date-time-input.vue +1 -1
  77. package/dist/assets/components/date-range-picker/date-range-picker.docs.mdx +31 -6
  78. package/dist/assets/components/date-range-picker/date-range-picker.stories.ts +1 -1
  79. package/dist/assets/components/date-range-picker/date-range-picker.types.ts +1 -1
  80. package/dist/assets/components/date-range-picker/date-range-picker.vue +1 -1
  81. package/dist/assets/components/date-time-picker/date-time-picker.stories.ts +1 -1
  82. package/dist/assets/components/date-time-picker/date-time-picker.types.ts +1 -1
  83. package/dist/assets/components/divider/divider.internal.types.ts +1 -0
  84. package/dist/assets/components/divider/divider.stories.ts +1 -1
  85. package/dist/assets/components/divider/divider.types.ts +1 -1
  86. package/dist/assets/components/divider/divider.vue +1 -1
  87. package/dist/assets/components/draggable/components/draggable-handle/draggable-handle.stories.ts +1 -1
  88. package/dist/assets/components/draggable/draggable.stories.ts +1 -1
  89. package/dist/assets/components/draggable/draggable.vue +1 -1
  90. package/dist/assets/components/drawer/drawer.stories.ts +1 -1
  91. package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.internal.types.ts +1 -0
  92. package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.types.ts +1 -1
  93. package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue +1 -1
  94. package/dist/assets/components/dropdown/components/dropdown-default-textbox/dropdown-default-textbox.vue +1 -1
  95. package/dist/assets/components/dropdown/components/dropdown-options-container/dropdown-options-container.vue +1 -1
  96. package/dist/assets/components/dropdown/components/dropdown-small-textbox/dropdown-small-textbox.vue +1 -1
  97. package/dist/assets/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.stories.ts +1 -1
  98. package/dist/assets/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.types.ts +1 -1
  99. package/dist/assets/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.vue +1 -1
  100. package/dist/assets/components/dropdown/components/variants/dropdown-country/dropdown-country.stories.ts +1 -1
  101. package/dist/assets/components/dropdown/components/variants/dropdown-country/dropdown-country.types.ts +1 -1
  102. package/dist/assets/components/dropdown/components/variants/dropdown-country/dropdown-country.vue +1 -1
  103. package/dist/assets/components/dropdown/components/variants/dropdown-currency/dropdown-currency.stories.ts +1 -1
  104. package/dist/assets/components/dropdown/components/variants/dropdown-currency/dropdown-currency.types.ts +1 -1
  105. package/dist/assets/components/dropdown/components/variants/dropdown-currency/dropdown-currency.vue +1 -1
  106. package/dist/assets/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.stories.ts +1 -1
  107. package/dist/assets/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.types.ts +1 -1
  108. package/dist/assets/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.vue +1 -1
  109. package/dist/assets/components/dropdown/components/variants/dropdown-tag/dropdown-tag.stories.ts +1 -1
  110. package/dist/assets/components/dropdown/components/variants/dropdown-tag/dropdown-tag.types.ts +1 -1
  111. package/dist/assets/components/dropdown/components/variants/dropdown-tag/dropdown-tag.vue +1 -1
  112. package/dist/assets/components/dropdown/dropdown.docs.mdx +4 -40
  113. package/dist/assets/components/dropdown/dropdown.internal.types.ts +1 -0
  114. package/dist/assets/components/dropdown/dropdown.stories.ts +1 -1
  115. package/dist/assets/components/dropdown/dropdown.types.ts +1 -1
  116. package/dist/assets/components/dropdown/dropdown.vue +1 -1
  117. package/dist/assets/components/empty-state/empty-state.stories.ts +1 -1
  118. package/dist/assets/components/empty-state/empty-state.types.ts +1 -1
  119. package/dist/assets/components/file-uploader/components/file-uploader-file-card/file-uploader-file-card.stories.ts +1 -1
  120. package/dist/assets/components/file-uploader/components/file-uploader-file-card/file-uploader-file-card.types.ts +1 -1
  121. package/dist/assets/components/file-uploader/components/file-uploader-file-card/file-uploader-file-card.vue +1 -1
  122. package/dist/assets/components/file-uploader/components/file-uploader-restrictions/file-uploader-restrictions.types.ts +1 -1
  123. package/dist/assets/components/file-uploader/components/file-uploader-restrictions/file-uploader-restrictions.vue +1 -1
  124. package/dist/assets/components/file-uploader/file-uploader.internal.types.ts +1 -0
  125. package/dist/assets/components/file-uploader/file-uploader.stories.ts +1 -1
  126. package/dist/assets/components/file-uploader/file-uploader.types.ts +1 -1
  127. package/dist/assets/components/file-uploader/file-uploader.vue +1 -1
  128. package/dist/assets/components/filter-bar/components/all-filters-modal/all-filters-modal.stories.ts +1 -1
  129. package/dist/assets/components/filter-bar/components/all-filters-modal/all-filters-modal.types.ts +1 -1
  130. package/dist/assets/components/filter-bar/components/all-filters-modal/all-filters-modal.vue +1 -1
  131. package/dist/assets/components/filter-bar/components/all-filters-modal/components/all-filters-modal-filter-selection/all-filters-modal-filter-selection.types.ts +1 -0
  132. package/dist/assets/components/filter-bar/components/all-filters-modal/components/all-filters-modal-filter-selection/all-filters-modal-filter-selection.vue +1 -0
  133. package/dist/assets/components/filter-bar/components/all-filters-modal/components/all-filters-modal-selected-filter/all-filters-modal-selected-filter.vue +1 -0
  134. package/dist/assets/components/filter-bar/components/base-filter/base-filter.internal.types.ts +1 -0
  135. package/dist/assets/components/filter-bar/components/base-filter/base-filter.types.ts +1 -1
  136. package/dist/assets/components/filter-bar/components/base-filter/base-filter.vue +1 -1
  137. package/dist/assets/components/filter-bar/components/boolean-filter/boolean-filter.vue +1 -1
  138. package/dist/assets/components/filter-bar/components/date-filter/date-filter.vue +1 -1
  139. package/dist/assets/components/filter-bar/components/date-range-filter/date-range-filter.vue +1 -1
  140. package/dist/assets/components/filter-bar/components/input-filter/input-filter.types.ts +1 -1
  141. package/dist/assets/components/filter-bar/components/input-filter/input-filter.vue +1 -1
  142. package/dist/assets/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.stories.ts +1 -1
  143. package/dist/assets/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.vue +1 -1
  144. package/dist/assets/components/filter-bar/components/select-filter/components/select-filter-button/select-filter-button.vue +1 -1
  145. package/dist/assets/components/filter-bar/components/select-filter/select-filter.vue +1 -1
  146. package/dist/assets/components/filter-bar/components/select-filter/variants/select-avatar-filter/select-avatar-filter.stories.ts +1 -1
  147. package/dist/assets/components/filter-bar/components/select-filter/variants/select-country-filter/select-country-filter.stories.ts +1 -1
  148. package/dist/assets/components/filter-bar/components/select-filter/variants/select-country-filter/select-country-filter.vue +1 -1
  149. package/dist/assets/components/filter-bar/components/toggle-filter/toggle-filter.vue +1 -1
  150. package/dist/assets/components/filter-bar/filter-bar.docs.mdx +4 -62
  151. package/dist/assets/components/filter-bar/filter-bar.stories.ts +1 -1
  152. package/dist/assets/components/filter-bar/filter-bar.types.ts +1 -1
  153. package/dist/assets/components/filter-bar/filter-bar.vue +1 -1
  154. package/dist/assets/components/form-layout/form-layout.docs.mdx +0 -23
  155. package/dist/assets/components/form-layout/form-layout.stories.ts +1 -1
  156. package/dist/assets/components/header/header.stories.ts +1 -1
  157. package/dist/assets/components/header/header.types.ts +1 -1
  158. package/dist/assets/components/header/header.vue +1 -1
  159. package/dist/assets/components/header-with-views/components/header-with-views-all-views/header-with-views-all-views.vue +1 -1
  160. package/dist/assets/components/header-with-views/components/header-with-views-item-edit-form/header-with-views-edit-form.types.ts +1 -1
  161. package/dist/assets/components/header-with-views/components/header-with-views-item-edit-form/header-with-views-edit-form.vue +1 -1
  162. package/dist/assets/components/header-with-views/header-with-views.internal.types.ts +1 -0
  163. package/dist/assets/components/header-with-views/header-with-views.stories.ts +1 -1
  164. package/dist/assets/components/header-with-views/header-with-views.types.ts +1 -1
  165. package/dist/assets/components/header-with-views/header-with-views.vue +1 -1
  166. package/dist/assets/components/image/image.stories.ts +1 -1
  167. package/dist/assets/components/info-icon/info-icon.stories.ts +1 -1
  168. package/dist/assets/components/input-field/input-field.docs.mdx +1 -13
  169. package/dist/assets/components/input-field/input-field.stories.ts +1 -1
  170. package/dist/assets/components/input-field/input-field.types.ts +1 -1
  171. package/dist/assets/components/input-field/input-field.vue +1 -1
  172. package/dist/assets/components/input-field-password/input-field-password.internal.types.ts +1 -0
  173. package/dist/assets/components/input-field-password/input-field-password.stories.ts +1 -1
  174. package/dist/assets/components/input-field-password/input-field-password.types.ts +1 -1
  175. package/dist/assets/components/input-field-password/input-field-password.vue +1 -1
  176. package/dist/assets/components/input-field-phone-number/input-field-phone-number.stories.ts +1 -1
  177. package/dist/assets/components/input-field-phone-number/input-field-phone-number.types.ts +1 -1
  178. package/dist/assets/components/input-field-phone-number/input-field-phone-number.vue +1 -1
  179. package/dist/assets/components/input-time/input-time.docs.mdx +17 -0
  180. package/dist/assets/components/input-time/input-time.stories.ts +1 -1
  181. package/dist/assets/components/input-time/input-time.types.ts +1 -1
  182. package/dist/assets/components/input-time/input-time.vue +1 -1
  183. package/dist/assets/components/inspector/inspector.stories.ts +1 -1
  184. package/dist/assets/components/inspector/inspector.vue +1 -1
  185. package/dist/assets/components/layout/components/layout-row/layout-row.stories.ts +1 -1
  186. package/dist/assets/components/layout/components/layout-row/layout-row.vue +1 -1
  187. package/dist/assets/components/layout/layout.docs.mdx +10 -0
  188. package/dist/assets/components/layout/layout.internal.types.ts +1 -0
  189. package/dist/assets/components/layout/layout.stories.ts +1 -1
  190. package/dist/assets/components/layout/layout.types.ts +1 -1
  191. package/dist/assets/components/link/link.stories.ts +1 -1
  192. package/dist/assets/components/link/link.types.ts +1 -1
  193. package/dist/assets/components/link/link.vue +1 -1
  194. package/dist/assets/components/list/list.stories.ts +1 -1
  195. package/dist/assets/components/loading-indicator/loading-indicator.stories.ts +1 -1
  196. package/dist/assets/components/menu/components/menu-item/menu-item.types.ts +1 -1
  197. package/dist/assets/components/menu/components/menu-item/menu-item.vue +1 -1
  198. package/dist/assets/components/menu/components/menu-item-list/menu-item-list.vue +1 -1
  199. package/dist/assets/components/menu/components/sub-menu-item-list/sub-menu-item-list.vue +1 -1
  200. package/dist/assets/components/menu/menu.docs.mdx +17 -7
  201. package/dist/assets/components/menu/menu.stories.ts +1 -1
  202. package/dist/assets/components/menu/menu.types.ts +1 -1
  203. package/dist/assets/components/menu/menu.vue +1 -1
  204. package/dist/assets/components/modal/components/base-modal/base-modal.vue +1 -1
  205. package/dist/assets/components/modal/components/modal-page/modal-page.vue +1 -1
  206. package/dist/assets/components/modal/modal.internal.types.ts +1 -0
  207. package/dist/assets/components/modal/modal.stories.ts +1 -1
  208. package/dist/assets/components/modal/modal.types.ts +1 -1
  209. package/dist/assets/components/modal/modal.vue +1 -1
  210. package/dist/assets/components/modal-fullscreen/components/modal-fullscreen-page.vue +1 -1
  211. package/dist/assets/components/modal-fullscreen/modal-fullscreen.stories.ts +1 -1
  212. package/dist/assets/components/modal-fullscreen/modal-fullscreen.vue +1 -1
  213. package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.internal.types.ts +1 -0
  214. package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.types.ts +1 -1
  215. package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.vue +1 -1
  216. package/dist/assets/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.vue +1 -1
  217. package/dist/assets/components/navigation-menu/navigation-menu.internal.types.ts +1 -0
  218. package/dist/assets/components/navigation-menu/navigation-menu.stories.ts +1 -1
  219. package/dist/assets/components/navigation-menu/navigation-menu.types.ts +1 -1
  220. package/dist/assets/components/navigation-menu/navigation-menu.vue +1 -1
  221. package/dist/assets/components/pagination/components/pagination-context/pagination-context.vue +1 -1
  222. package/dist/assets/components/pagination/components/pagination-results-per-page/pagination-results-per-page.vue +1 -1
  223. package/dist/assets/components/pagination/pagination.stories.ts +1 -1
  224. package/dist/assets/components/pagination/pagination.types.ts +1 -1
  225. package/dist/assets/components/pagination/pagination.vue +1 -1
  226. package/dist/assets/components/payment-method/payment-method.stories.ts +1 -1
  227. package/dist/assets/components/payment-method/payment-method.types.ts +1 -1
  228. package/dist/assets/components/popover/popover.docs.mdx +32 -9
  229. package/dist/assets/components/popover/popover.stories.ts +1 -1
  230. package/dist/assets/components/popover/popover.types.ts +1 -1
  231. package/dist/assets/components/popover/popover.vue +1 -1
  232. package/dist/assets/components/popper-container/popper-container.types.ts +1 -1
  233. package/dist/assets/components/popper-container/popper-container.vue +1 -1
  234. package/dist/assets/components/progress-bar/progress-bar.docs.mdx +102 -0
  235. package/dist/assets/components/progress-bar/progress-bar.stories.ts +1 -0
  236. package/dist/assets/components/progress-bar/progress-bar.types.ts +1 -0
  237. package/dist/assets/components/progress-bar/progress-bar.vue +1 -0
  238. package/dist/assets/components/promo-banner/promo-banner.stories.ts +1 -1
  239. package/dist/assets/components/promo-banner/promo-banner.types.ts +1 -1
  240. package/dist/assets/components/promo-banner/promo-banner.vue +1 -1
  241. package/dist/assets/components/radio-group/components/radio-button/radio-button.vue +1 -1
  242. package/dist/assets/components/radio-group/radio-group.docs.mdx +1 -6
  243. package/dist/assets/components/radio-group/radio-group.stories.ts +1 -1
  244. package/dist/assets/components/radio-group/radio-group.types.ts +1 -1
  245. package/dist/assets/components/radio-group/radio-group.vue +1 -1
  246. package/dist/assets/components/rich-text-editor/rich-text-editor.internal.types.ts +1 -0
  247. package/dist/assets/components/rich-text-editor/rich-text-editor.stories.ts +1 -1
  248. package/dist/assets/components/rich-text-editor/rich-text-editor.types.ts +1 -1
  249. package/dist/assets/components/rich-text-editor/rich-text-editor.vue +1 -1
  250. package/dist/assets/components/search-bar/search-bar.stories.ts +1 -1
  251. package/dist/assets/components/search-bar/search-bar.vue +1 -1
  252. package/dist/assets/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue +1 -1
  253. package/dist/assets/components/secondary-nav/secondary-nav.stories.ts +1 -1
  254. package/dist/assets/components/secondary-nav/secondary-nav.vue +1 -1
  255. package/dist/assets/components/segmented-control/components/segmented-control-button/segmented-control-button.vue +1 -1
  256. package/dist/assets/components/segmented-control/segmented-control.stories.ts +1 -1
  257. package/dist/assets/components/segmented-control/segmented-control.types.ts +1 -1
  258. package/dist/assets/components/segmented-control/segmented-control.vue +1 -1
  259. package/dist/assets/components/selection-card/components/selection-card-group/selection-card-group.docs.mdx +12 -13
  260. package/dist/assets/components/selection-card/components/selection-card-group/selection-card-group.stories.ts +1 -1
  261. package/dist/assets/components/selection-card/components/selection-card-group/selection-card-group.vue +1 -1
  262. package/dist/assets/components/selection-card/selection-card.stories.ts +1 -1
  263. package/dist/assets/components/selection-card/selection-card.types.ts +1 -1
  264. package/dist/assets/components/selection-card/selection-card.vue +1 -1
  265. package/dist/assets/components/sidepanel/sidepanel.docs.mdx +1 -1
  266. package/dist/assets/components/sidepanel/sidepanel.stories.ts +1 -1
  267. package/dist/assets/components/sidepanel/sidepanel.vue +1 -1
  268. package/dist/assets/components/status/status.stories.ts +1 -1
  269. package/dist/assets/components/status/status.types.ts +1 -1
  270. package/dist/assets/components/stepper/components/step/step.vue +1 -1
  271. package/dist/assets/components/stepper/stepper.stories.ts +1 -1
  272. package/dist/assets/components/stepper/stepper.vue +1 -1
  273. package/dist/assets/components/structured-list/structured-list.stories.ts +1 -1
  274. package/dist/assets/components/structured-list/structured-list.vue +1 -1
  275. package/dist/assets/components/summary-grid/components/summary-grid-item/summary-grid-item.types.ts +1 -1
  276. package/dist/assets/components/summary-grid/summary-grid.stories.ts +1 -1
  277. package/dist/assets/components/summary-grid/summary-grid.types.ts +1 -1
  278. package/dist/assets/components/table-of-contents/components/table-of-contents-list/table-of-contents-list.vue +1 -1
  279. package/dist/assets/components/table-of-contents/table-of-contents.internal.types.ts +1 -0
  280. package/dist/assets/components/table-of-contents/table-of-contents.stories.ts +1 -1
  281. package/dist/assets/components/table-of-contents/table-of-contents.types.ts +1 -1
  282. package/dist/assets/components/table-of-contents/table-of-contents.vue +1 -1
  283. package/dist/assets/components/tabs/components/tab.internal.types.ts +1 -0
  284. package/dist/assets/components/tabs/components/tab.types.ts +1 -1
  285. package/dist/assets/components/tabs/components/tab.vue +1 -1
  286. package/dist/assets/components/tabs/tabs.stories.ts +1 -1
  287. package/dist/assets/components/tabs/tabs.vue +1 -1
  288. package/dist/assets/components/tag/tag.docs.mdx +1 -1
  289. package/dist/assets/components/tag/tag.stories.ts +1 -1
  290. package/dist/assets/components/tag/tag.types.ts +1 -1
  291. package/dist/assets/components/tag/tag.vue +1 -1
  292. package/dist/assets/components/textarea/textarea.docs.mdx +0 -7
  293. package/dist/assets/components/textarea/textarea.stories.ts +1 -1
  294. package/dist/assets/components/textarea/textarea.types.ts +1 -1
  295. package/dist/assets/components/textarea/textarea.vue +1 -1
  296. package/dist/assets/components/timeline/components/timeline-item/timeline-item.stories.ts +1 -1
  297. package/dist/assets/components/timeline/components/timeline-item/timeline-item.vue +1 -1
  298. package/dist/assets/components/timeline/timeline.docs.mdx +0 -10
  299. package/dist/assets/components/timeline/timeline.internal.types.ts +1 -0
  300. package/dist/assets/components/timeline/timeline.stories.ts +1 -1
  301. package/dist/assets/components/timeline/timeline.types.ts +1 -1
  302. package/dist/assets/components/timeline/timeline.vue +1 -1
  303. package/dist/assets/components/toast/components/toast-item/toast-item.vue +1 -1
  304. package/dist/assets/components/toggle/toggle.stories.ts +1 -1
  305. package/dist/assets/components/toggle/toggle.types.ts +1 -1
  306. package/dist/assets/components/toggle/toggle.vue +1 -1
  307. package/dist/assets/components/tutorial/tutorial.stories.ts +1 -1
  308. package/dist/assets/components/typography/typography.stories.ts +1 -1
  309. package/dist/assets/components/typography/typography.types.ts +1 -1
  310. package/dist/assets/components/typography/typography.vue +1 -1
  311. package/dist/assets/components.json +1 -0
  312. package/dist/assets/composables/use-bento-base-filter/index.ts +1 -0
  313. package/dist/assets/composables/use-bento-base-filter/use-bento-base-filter.ts +1 -0
  314. package/dist/assets/composables/use-bento-base-filter/utils/index.ts +1 -0
  315. package/dist/assets/composables/use-bento-base-filter/utils/utils.ts +1 -0
  316. package/dist/assets/composables/use-bento-currency/index.ts +1 -0
  317. package/dist/assets/composables/use-bento-currency/use-bento-currency.stories.ts +1 -1
  318. package/dist/assets/composables/use-bento-currency/use-bento-currency.ts +1 -0
  319. package/dist/assets/composables/use-bento-delayed-hover/index.ts +1 -0
  320. package/dist/assets/composables/use-bento-delayed-hover/use-bento-delayed-hover.stories.ts +1 -1
  321. package/dist/assets/composables/use-bento-delayed-hover/use-bento-delayed-hover.ts +1 -0
  322. package/dist/assets/composables/use-bento-theme/index.ts +1 -0
  323. package/dist/assets/composables/use-bento-theme/install.ts +1 -1
  324. package/dist/assets/composables/use-bento-theme/install.types.ts +1 -1
  325. package/dist/assets/composables/use-bento-theme/use-bento-theme.ts +1 -0
  326. package/dist/assets/composables/use-bento-toast-controller/index.ts +1 -0
  327. package/dist/assets/composables/use-bento-toast-controller/toast-stack.ts +1 -0
  328. package/dist/assets/composables/use-bento-toast-controller/use-bento-toast-controller.ts +1 -0
  329. package/dist/assets/composables/use-click-outside/index.ts +1 -0
  330. package/dist/assets/composables/use-click-outside/use-click-outside.stories.ts +1 -1
  331. package/dist/assets/composables/use-click-outside/use-click-outside.ts +1 -0
  332. package/dist/assets/composables/use-form-validate/index.ts +1 -0
  333. package/dist/assets/composables/use-form-validate/use-form.ts +1 -0
  334. package/dist/assets/composables/use-form-validate/use-form.types.ts +1 -1
  335. package/dist/assets/composables.json +9 -0
  336. package/dist/assets/deprecations.md +139 -243
  337. package/dist/assets/directives/highlight/highlight.stories.ts +1 -1
  338. package/dist/assets/directives/keyboard-navigation/keyboard-navigation.stories.ts +1 -1
  339. package/dist/assets/directives/tooltip/tooltip.internal.types.ts +1 -0
  340. package/dist/assets/directives/tooltip/tooltip.stories.ts +1 -1
  341. package/dist/assets/directives/tooltip/tooltip.types.ts +1 -1
  342. package/dist/assets/icons.json +1 -0
  343. package/dist/assets/index.ts +1 -1
  344. package/dist/assets/install.ts +1 -1
  345. package/dist/assets/tailwind/README.md +3 -3
  346. package/dist/assets/templates/dashboard-template/components/dashboard-section/dashboard-section.stories.ts +1 -1
  347. package/dist/assets/templates/dashboard-template/dashboard-template.stories.ts +1 -1
  348. package/dist/assets/templates/data-grid-template/data-grid-template.stories.ts +1 -1
  349. package/dist/assets/usage.json +81 -80
  350. package/dist/assets/variables.css +152 -140
  351. package/dist/main.js +228 -34
  352. package/package.json +2 -2
  353. package/dist/assets/components/internal/base-button/base-button.types.ts +0 -1
  354. package/dist/assets/components/internal/base-button/base-button.vue +0 -1
  355. package/dist/assets/components/internal/button-actions-with-menu/button-actions-with-menu.stories.ts +0 -1
  356. package/dist/assets/components/internal/button-actions-with-menu/button-actions-with-menu.types.ts +0 -1
  357. package/dist/assets/components/internal/button-actions-with-menu/button-actions-with-menu.vue +0 -1
  358. package/dist/assets/components/internal/calendar/calendar.stories.ts +0 -1
  359. package/dist/assets/components/internal/calendar/calendar.types.ts +0 -1
  360. package/dist/assets/components/internal/calendar/calendar.vue +0 -1
  361. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.stories.ts +0 -1
  362. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.types.ts +0 -1
  363. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.vue +0 -1
  364. package/dist/assets/components/internal/calendar/components/calendar-month/components/calendar-month-day/__test__/calendar-month-day-stub.vue +0 -1
  365. package/dist/assets/components/internal/calendar/components/calendar-month/components/calendar-month-day/calendar-month-day.stories.ts +0 -1
  366. package/dist/assets/components/internal/calendar/components/calendar-month/components/calendar-month-day/calendar-month-day.types.ts +0 -1
  367. package/dist/assets/components/internal/calendar/components/calendar-month/components/calendar-month-day/calendar-month-day.vue +0 -1
  368. package/dist/assets/components/internal/calendar/components/calendar-month/composables/granular-min-max-date.types.ts +0 -1
  369. package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.types.ts +0 -1
  370. package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.vue +0 -1
  371. package/dist/assets/components/internal/calendar/components/calendar-year/components/calendar-year-month/__test__/calendar-year-month-stub.vue +0 -1
  372. package/dist/assets/components/internal/calendar/components/calendar-year/components/calendar-year-month/calendar-year-month.types.ts +0 -1
  373. package/dist/assets/components/internal/calendar/components/calendar-year/components/calendar-year-month/calendar-year-month.vue +0 -1
  374. package/dist/assets/components/internal/calendar/components/calendar-year-selector/calendar-year-selector.types.ts +0 -1
  375. package/dist/assets/components/internal/calendar/components/calendar-year-selector/calendar-year-selector.vue +0 -1
  376. package/dist/assets/components/internal/calendar/composables/use-granularity-adjustments.types.ts +0 -1
  377. package/dist/assets/components/internal/calendar/composables/use-range-pane.types.ts +0 -1
  378. package/dist/assets/components/internal/checkbox-input/checkbox-input.stories.ts +0 -1
  379. package/dist/assets/components/internal/checkbox-input/checkbox-input.types.ts +0 -1
  380. package/dist/assets/components/internal/checkbox-input/checkbox-input.vue +0 -1
  381. package/dist/assets/components/internal/controls-group/controls-group.vue +0 -1
  382. package/dist/assets/components/internal/copy/copy.docs.mdx +0 -43
  383. package/dist/assets/components/internal/copy/copy.stories.ts +0 -1
  384. package/dist/assets/components/internal/copy/copy.types.ts +0 -1
  385. package/dist/assets/components/internal/copy/copy.vue +0 -1
  386. package/dist/assets/components/internal/decorative-checkbox/decorative-checkbox.vue +0 -1
  387. package/dist/assets/components/internal/dialog-page/dialog-page.types.ts +0 -1
  388. package/dist/assets/components/internal/dialog-page/dialog-page.vue +0 -1
  389. package/dist/assets/components/internal/error-message/error-message.vue +0 -1
  390. package/dist/assets/components/internal/field-label/field-label.vue +0 -1
  391. package/dist/assets/components/internal/fixed-scroller/fixed-scroller.docs.mdx +0 -21
  392. package/dist/assets/components/internal/fixed-scroller/fixed-scroller.stories.ts +0 -1
  393. package/dist/assets/components/internal/fixed-scroller/fixed-scroller.vue +0 -1
  394. package/dist/assets/components/internal/footer-actions/footer-actions.vue +0 -1
  395. package/dist/assets/components/internal/grid-layout/grid-layout.types.ts +0 -1
  396. package/dist/assets/components/internal/grid-layout/grid-layout.vue +0 -1
  397. package/dist/assets/components/internal/header-meta/header-meta.types.ts +0 -1
  398. package/dist/assets/components/internal/header-meta/header-meta.vue +0 -1
  399. package/dist/assets/components/internal/info-icon-with-popover/info-icon-with-popover.vue +0 -1
  400. package/dist/assets/components/internal/listbox/components/listbox-lazy-load/listbox-lazy-load.types.ts +0 -1
  401. package/dist/assets/components/internal/listbox/components/listbox-lazy-load/listbox-lazy-load.vue +0 -1
  402. package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-category/listbox-multi-select-category.types.ts +0 -1
  403. 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 +0 -1
  404. package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-nested-lazy-load/listbox-multi-select-nested-lazy-load.types.ts +0 -1
  405. package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-nested-lazy-load/listbox-multi-select-nested-lazy-load.vue +0 -1
  406. package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/listbox-multi-select-options.types.ts +0 -1
  407. package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/listbox-multi-select-options.vue +0 -1
  408. package/dist/assets/components/internal/listbox/components/listbox-multi-select/listbox-multi-select.types.ts +0 -1
  409. package/dist/assets/components/internal/listbox/components/listbox-multi-select/listbox-multi-select.vue +0 -1
  410. package/dist/assets/components/internal/listbox/components/listbox-option/listbox-option.vue +0 -1
  411. package/dist/assets/components/internal/listbox/components/listbox-option-tag/listbox-option-tag.vue +0 -1
  412. package/dist/assets/components/internal/listbox/components/listbox-single-select/components/listbox-single-select-category/listbox-single-select-category.vue +0 -1
  413. package/dist/assets/components/internal/listbox/components/listbox-single-select/listbox-single-select.vue +0 -1
  414. package/dist/assets/components/internal/listbox/components/listbox-single-select-option/listbox-single-select-option.vue +0 -1
  415. package/dist/assets/components/internal/listbox/listbox.stories.ts +0 -1
  416. package/dist/assets/components/internal/listbox/listbox.types.ts +0 -1
  417. package/dist/assets/components/internal/listbox/listbox.vue +0 -1
  418. package/dist/assets/components/internal/radio-input/radio-input.stories.ts +0 -1
  419. package/dist/assets/components/internal/radio-input/radio-input.types.ts +0 -1
  420. package/dist/assets/components/internal/radio-input/radio-input.vue +0 -1
  421. package/dist/assets/components/internal/teleport/teleport.types.ts +0 -1
  422. package/dist/assets/components/internal/teleport/teleport.vue +0 -1
  423. package/dist/assets/components/internal/tooltip/tooltip.test.vue +0 -1
  424. package/dist/assets/components/internal/tooltip/tooltip.types.ts +0 -1
  425. package/dist/assets/components/internal/tooltip/tooltip.vue +0 -1
  426. package/dist/assets/composables/use-form-validate-zod-4/use-form.types.ts +0 -1
  427. package/dist/assets/composables/use-numeric-input/use-numeric-input.types.ts +0 -1
  428. package/dist/assets/composables/use-section-navigation-keyboard/use-section-navigation-keyboard.types.ts +0 -1
  429. package/dist/assets/composables/use-virtual-list/use-virtual-list.types.ts +0 -1
@@ -1 +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 };
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, BentoFilterItemType } from '@/components/filter-bar/filter-bar.types'; import { type BentoFilterBarFiltersWithValues } 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: BentoFilterBarFiltersWithValues = [ { 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: [ { value: BentoCountryCode.NL, }, { 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<BentoFilterBarFiltersWithValues>(props.value); const updateFilters = (newFilters: BentoFilterBarFiltersWithValues) => { filtersSet.value = newFilters; action('input')(newFilters); }; return { // Values filtersSet, // Events updateFilters, }; }, template: ` <bento-all-filters-modal :value="filtersSet" @input="updateFilters" /> `, }), }; Default.args = { value: FILTERS };
@@ -1 +1 @@
1
- export enum AllFiltersModalEvent { INPUT = 'input', }
1
+ import type { BentoFilterBarFilterWithValue, BentoFilterModel } from '../../filter-bar.types'; export enum AllFiltersModalEvent { INPUT = 'input', } export interface BentoAllFiltersModalSelectedFilter extends BentoFilterBarFilterWithValue { labelId: string; } export type BentoAllFiltersModalGetFilterCount = (filter: BentoFilterModel) => string | number | null; export interface BentoAllFiltersModalButtonInstance { $el: HTMLElement; } export interface BentoAllFiltersModalFilterSelectionInstance { focusFilter: (label: string) => void; } export interface BentoAllFiltersModalSelectedFilterContentInstance { focusFirstInput: () => void; } export interface BentoAllFiltersModalSelectedFilterInstance { $el: HTMLElement; }
@@ -1 +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 :id="selectedFilter.labelId" 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" :aria-labelledby="selectedFilter.labelId" @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 setup lang="ts"> import { computed, onBeforeUpdate, onMounted, 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 { 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']; const props = withDefaults( defineProps<{ /** * 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?: BentoFilterBarModel; }>(), { value: () => [], } ); const emit = defineEmits([AllFiltersModalEvent.INPUT]); const filtersInModal = ref<BentoFilterBarModel>(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], labelId: generateUid(filtersInModal.value[selectedFilterIndex.value]?.field), })); 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(); }; 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(); }; </script> <script lang="ts"> import { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoToggleFilter, } from '../index'; /** * 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 { name: 'all-filters-modal', i18n: { messages }, components: { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoToggleFilter, }, }; </script> <style lang="scss" scoped src="./all-filters-modal.scss" />
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 v-if="!isSmallScreen" 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> <all-filters-modal-filter-selection ref="filterSelectionRef" :active-filters="activeFilters" :actions="modalActions" :active-filters-label="t('activeFilters')" :empty-search-message="isEmptyResultForSearchFilter ? t('noFiltersMatchThisSearch') : ''" :get-filter-count="getFilterCount" :other-filters="otherFilters" :other-filters-label="t('otherFilters')" :search-bar-aria-label="t('searchBar')" :search-term="searchTerm" :selected-filter-label="selectedFilter.label" :unique-id="allFiltersModalUniqueId" @clear-search="onSearchbarClear" @keydown-right="onKeydownRight" @select-filter="selectFilter" @update:search-term="searchTerm = $event" /> </template> <template #content-col-2> <all-filters-modal-selected-filter ref="selectedFilterContentRef" :actions="modalActions" :clear-button-label="t(isSelectedFilterPersistent ? 'reset' : 'clear')" :is-clear-button-disabled="isClearButtonDisabled" :selected-filter="selectedFilter" :should-show-filter-title="shouldShowFilterTitle" @clear="clearSelectedFilter" @input="updateFilter" @keydown-left="onKeydownLeft" /> </template> </bento-modal> <bento-modal-fullscreen v-else class="b-all-filters-modal__modal b-all-filters-modal__modal--small-screen" :active-page="smallScreenActivePage" :is-open="isModalOpen" @before-close="cancel" @update:active-page="smallScreenActivePage = $event" > <bento-modal-fullscreen-page :page-id="FILTER_SELECTION_PAGE_ID" :previous-page="null" :title="t('allFilters')" > <all-filters-modal-filter-selection :active-filters="activeFilters" :actions="modalActions" :active-filters-label="t('activeFilters')" :empty-search-message="isEmptyResultForSearchFilter ? t('noFiltersMatchThisSearch') : ''" :get-filter-count="getFilterCount" :other-filters="otherFilters" :other-filters-label="t('otherFilters')" :search-bar-aria-label="t('searchBar')" :search-term="searchTerm" :selected-filter-label="selectedFilter.label" :unique-id="allFiltersModalUniqueId" small-screen @clear-search="onSearchbarClear" @select-filter="selectFilter" @update:search-term="searchTerm = $event" /> </bento-modal-fullscreen-page> <bento-modal-fullscreen-page :page-id="FILTER_APPLICATION_PAGE_ID" :previous-page="FILTER_SELECTION_PAGE_ID" :title="t('allFilters')" > <all-filters-modal-selected-filter :actions="modalActions" :clear-button-label="t(isSelectedFilterPersistent ? 'reset' : 'clear')" :is-clear-button-disabled="isClearButtonDisabled" :selected-filter="selectedFilter" :should-show-filter-title="shouldShowFilterTitle" small-screen @input="updateFilter" /> </bento-modal-fullscreen-page> </bento-modal-fullscreen> </div> </template> <script setup lang="ts"> import { computed, ref, toRef, watch } from 'vue'; import { useMediaQuery } from '@vueuse/core'; import { type BentoDateRangePickerValue } from '@/components/date-range-picker'; import { BentoModalFullscreen, BentoModalFullscreenPage } from '@/components/modal-fullscreen'; import BentoModal from '@/components/modal/modal.vue'; import { useSearchBarFilter } from '@/components/search-bar'; import { generateUid } from '@/utils/ts/generate-uid'; import { useI18n } from '@/utils/ts/i18n'; import { type BentoFilterBarFiltersWithValues, type BentoFilterBarFilterWithValue, BentoFilterItemType, type BentoSelectFilterOptions, } from '@/components/filter-bar/filter-bar.types'; import { FilterBarButton } from '../filter-bar-button'; import { AllFiltersModalEvent, type BentoAllFiltersModalFilterSelectionInstance, type BentoAllFiltersModalSelectedFilterContentInstance, } from './all-filters-modal.types'; import AllFiltersModalFilterSelection from './components/all-filters-modal-filter-selection/all-filters-modal-filter-selection.vue'; import AllFiltersModalSelectedFilter from './components/all-filters-modal-selected-filter/all-filters-modal-selected-filter.vue'; import SlidersIcon from '@adyen/ui-assets-icons-16/vue/sliders-1'; import { type BentoCheckboxGroupFilterOptions } from '../checkbox-group-filter'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const FILTER_SELECTION_PAGE_ID = 'filter-selection'; const FILTER_APPLICATION_PAGE_ID = 'filter-application'; const props = withDefaults( defineProps<{ /** * 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?: BentoFilterBarFiltersWithValues; }>(), { value: () => [], } ); const emit = defineEmits([AllFiltersModalEvent.INPUT]); const filtersInModal = ref<BentoFilterBarFiltersWithValues>(props.value); const allFiltersModalUniqueId = generateUid('all-filters-modal'); const filterSelectionRef = ref<BentoAllFiltersModalFilterSelectionInstance | null>(null); const isModalOpen = ref(false); const selectedFilterIndex = ref(0); const selectedFilterContentRef = ref<BentoAllFiltersModalSelectedFilterContentInstance | null>(null); const searchTerm = ref<string>(''); const smallScreenActivePage = ref(FILTER_SELECTION_PAGE_ID); const { t, n } = useI18n<{ message: MessageSchema }>({ messages }); const isSmallScreen = useMediaQuery('(max-width: 599px)'); const filteredItems = useSearchBarFilter<BentoFilterBarFilterWithValue>(searchTerm, toRef(filtersInModal, '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: BentoFilterBarFiltersWithValues) => 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], labelId: generateUid(filtersInModal.value[selectedFilterIndex.value]?.field), })); 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; }; watch( () => isModalOpen.value, isOpen => { if (isOpen) { selectOpenedFilter(); } } ); const computeCounts = (filterValue: BentoFilterBarFiltersWithValues) => 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 selectedFilterValue = computed(() => selectedFilter.value.value); const hasValueSelected = computed(() => Array.isArray(selectedFilterValue.value) ? selectedFilterValue.value.length && selectedFilterValue.value.find(item => item !== '') : !!selectedFilterValue.value ); const hasPersistentFilters = computed(() => filtersInModal.value.some(({ defaultValue }) => defaultValue !== undefined) ); const isSelectedFilterPersistent = computed(() => selectedFilter.value.defaultValue !== undefined); /** * 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( () => selectedFilter.value.disabled || (isSelectedFilterPersistent.value ? JSON.stringify(selectedFilter.value.value) === JSON.stringify(selectedFilter.value.defaultValue) : !hasValueSelected.value) ); const filtersIncludedInActiveCount = computed(() => props.value.filter( filter => filter.defaultValue === undefined || (filter.visible === false && JSON.stringify(filter.value) !== JSON.stringify(filter.defaultValue)) ) ); const numberOfActiveFilters = computed( () => computeCounts(filtersIncludedInActiveCount.value)?.filter(({ count }) => !!count).length ); const onClickAllFiltersModal = () => { if (!isModalOpen.value) { smallScreenActivePage.value = FILTER_SELECTION_PAGE_ID; isModalOpen.value = true; } else { isModalOpen.value = false; } }; const filterCounts = computed( () => new Map(computeCounts(filtersInModal.value).map(({ field, count }) => [field, count])) ); const getFilterCount = (filter: BentoFilterBarFilterWithValue) => { const filterCount = filterCounts.value.get(filter.field); 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 ); if (isSmallScreen.value) { smallScreenActivePage.value = FILTER_APPLICATION_PAGE_ID; } }; const onSearchbarClear = () => { searchTerm.value = ''; }; const cancel = () => { isModalOpen.value = false; filtersInModal.value = props.value; onSearchbarClear(); }; const updateFilter = (updatedFilter: BentoFilterBarFilterWithValue) => { const updatedFilters = filtersInModal.value.map(({ value, ...otherFields }, index) => { if (index === selectedFilterIndex.value) { return { value: updatedFilter.value, ...otherFields }; } return { value, ...otherFields }; }); filtersInModal.value = updatedFilters; }; const clearSelectedFilter = () => { updateFilter({ field: selectedFilter.value.field, label: selectedFilter.value.label, type: selectedFilter.value.type, value: isSelectedFilterPersistent.value ? selectedFilter.value.defaultValue : undefined, }); }; const clearAll = () => { filtersInModal.value = props.value.map(filter => ({ ...filter, value: filter.defaultValue, })); }; const applyAll = () => { emit(AllFiltersModalEvent.INPUT, filtersInModal.value); isModalOpen.value = false; smallScreenActivePage.value = FILTER_SELECTION_PAGE_ID; }; const isApplyAllButtonDisabled = computed( () => JSON.stringify(filtersInModal.value) === JSON.stringify(props.value) ); const isClearAllButtonDisabled = computed(() => hasPersistentFilters.value ? filtersInModal.value.every(filter => JSON.stringify(filter.value) === JSON.stringify(filter.defaultValue)) : !numberOfActiveFilters.value ); const modalActions = computed(() => [ { title: t('applyAll'), disabled: isApplyAllButtonDisabled.value, event: applyAll, }, { title: t('cancel'), event: cancel, }, { title: t(hasPersistentFilters.value ? 'resetAll' : 'clearAll'), disabled: isClearAllButtonDisabled.value, event: clearAll, }, ]); const onKeydownRight = () => { selectedFilterContentRef.value?.focusFirstInput(); }; const onKeydownLeft = () => { filterSelectionRef.value?.focusFilter(selectedFilter.value.label); }; </script> <script lang="ts"> /** * 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 { name: 'all-filters-modal', i18n: { messages }, }; </script> <style lang="scss" scoped src="./all-filters-modal.scss" />
@@ -0,0 +1 @@
1
+ import { type BentoButtonActionsList } from '@/components/button'; import { type BentoFilterBarModel } from '@/components/filter-bar/filter-bar.types'; import { type BentoAllFiltersModalGetFilterCount } from '../../all-filters-modal.types'; export interface BentoAllFiltersModalFilterSelectionProps { /** Filters with an applied value. */ activeFilters: BentoFilterBarModel; /** Actions displayed below the filter list on small screens. */ actions?: BentoButtonActionsList; /** Label for the active filters section. */ activeFiltersLabel: string; /** Message displayed when a search has no matching filters. */ emptySearchMessage?: string; /** Returns the applied value count for a filter. */ getFilterCount: BentoAllFiltersModalGetFilterCount; /** Filters without an applied value. */ otherFilters: BentoFilterBarModel; /** Label for the other filters section. */ otherFiltersLabel: string; /** Label of the currently selected filter. */ selectedFilterLabel: string; /** Accessible label for the search input. */ searchBarAriaLabel: string; /** Current filter search term. */ searchTerm: string; /** Displays the small-screen variant. */ smallScreen?: boolean; /** Unique suffix used by section label IDs. */ uniqueId: string; } export interface BentoAllFiltersModalFilterSelectionEmits { /** * Emitted when the search term is cleared */ (event: 'clear-search'): void; /** * Emitted when the right arrow key is pressed */ (event: 'keydown-right'): void; /** * Emitted when a filter is selected */ (event: 'select-filter', label: string, field: string): void; /** * Emitted when the search term changes */ (event: 'update:search-term', value: string): void; }
@@ -0,0 +1 @@
1
+ <template> <div class="b-all-filters-modal-filter-selection" :class="conditionalClasses" @keydown.right="$emit('keydown-right')" > <bento-search-bar class="b-all-filters-modal-filter-selection__search-bar" :value="searchTerm" :input-field-aria-label="searchBarAriaLabel" @input="$emit('update:search-term', $event)" @clear="$emit('clear-search')" /> <div v-if="emptySearchMessage" class="b-all-filters-modal-filter-selection__empty-search-message"> <bento-typography el="span">{{ emptySearchMessage }}</bento-typography> </div> <div v-if="activeFilters.length" class="b-all-filters-modal-filter-selection__filters-section"> <bento-typography :id="`active-filters-${screenIdPrefix}${uniqueId}`" class="b-all-filters-modal-filter-selection__filters-section-title" el="span" variant="caption" > {{ activeFiltersLabel }} </bento-typography> <ul class="b-all-filters-modal-filter-selection__filters-list" :aria-labelledby="`active-filters-${screenIdPrefix}${uniqueId}`" > <li v-for="item in activeFilters" :key="`${item.label}-${item.field}`"> <all-filters-modal-button :ref="element => setFilterButtonRef(element, item.label)" :label="item.label" :applied-filters-count="getFilterCount(item)" :is-filter-open="!smallScreen && selectedFilterLabel === item.label" :disabled="item.disabled" @click="$emit('select-filter', item.label, item.field)" /> </li> </ul> </div> <div v-if="otherFilters.length" class="b-all-filters-modal-filter-selection__filters-section"> <bento-typography v-if="activeFilters.length" :id="`other-filters-${screenIdPrefix}${uniqueId}`" class="b-all-filters-modal-filter-selection__filters-section-title" el="span" variant="caption" > {{ otherFiltersLabel }} </bento-typography> <ul class="b-all-filters-modal-filter-selection__filters-list" :aria-labelledby="`other-filters-${screenIdPrefix}${uniqueId}`" > <li v-for="item in otherFilters" :key="`${item.label}-${item.field}`"> <all-filters-modal-button :ref="element => setFilterButtonRef(element, item.label)" :label="item.label" :disabled="item.disabled" :applied-filters-count="getFilterCount(item)" :is-filter-open="!smallScreen && selectedFilterLabel === item.label" @click="$emit('select-filter', item.label, item.field)" /> </li> </ul> </div> <bento-button-actions v-if="smallScreen" class="b-all-filters-modal-filter-selection__actions" :actions="actions" layout="vertical-stack" /> </div> </template> <script setup lang="ts"> import { computed, onBeforeUpdate, ref } from 'vue'; import { BentoButtonActions } from '@/components/button'; import { BentoTypography } from '@/components/typography'; import BentoSearchBar from '@/components/search-bar/search-bar.vue'; import { type BentoAllFiltersModalButtonInstance } from '../../all-filters-modal.types'; import AllFiltersModalButton from '../all-filters-modal-button/all-filters-modal-button.vue'; import { type BentoAllFiltersModalFilterSelectionEmits, type BentoAllFiltersModalFilterSelectionProps, } from './all-filters-modal-filter-selection.types'; const props = withDefaults(defineProps<BentoAllFiltersModalFilterSelectionProps>(), { actions: () => [], emptySearchMessage: '', smallScreen: false, }); defineEmits<BentoAllFiltersModalFilterSelectionEmits>(); const conditionalClasses = computed(() => ({ 'b-all-filters-modal-filter-selection--small-screen': props.smallScreen, })); const filterButtons = ref<Record<string, BentoAllFiltersModalButtonInstance>>({}); const screenIdPrefix = computed(() => (props.smallScreen ? 'small-screen-' : '')); onBeforeUpdate(() => { filterButtons.value = {}; }); const setFilterButtonRef = (element: BentoAllFiltersModalButtonInstance | null, label: string) => { if (element) { filterButtons.value[label] = element; } }; const focusFilter = (label: string) => { filterButtons.value[label]?.$el?.focus(); }; defineExpose({ focusFilter }); </script> <script lang="ts"> export default { name: 'all-filters-modal-filter-selection', }; </script> <style lang="scss" scoped src="./all-filters-modal-filter-selection.scss" />
@@ -0,0 +1 @@
1
+ <template> <div class="b-all-filters-modal-selected-filter" :class="conditionalClasses" @keydown.left="$emit('keydown-left')"> <div v-if="smallScreen || shouldShowFilterTitle" class="b-all-filters-modal-selected-filter__text"> <bento-typography :id="selectedFilter.labelId" 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" :aria-labelledby="selectedFilter.labelId" @input="$emit('input', $event)" /> </div> <bento-button-actions v-if="smallScreen" class="b-all-filters-modal-selected-filter__actions" :actions="actions" layout="vertical-stack" /> <div v-else class="b-all-filters-modal-selected-filter__clear-button"> <bento-button variant="secondary" :disabled="isClearButtonDisabled" @click="$emit('clear')"> {{ clearButtonLabel }} </bento-button> </div> </div> </template> <script setup lang="ts"> import { computed, ref } from 'vue'; import { BentoButton, BentoButtonActions, type BentoButtonActionsList } from '@/components/button'; import { BentoTypography } from '@/components/typography'; import { getNextFocusableElement } from '@/utils/ts/focus-trap.utils'; import { type BentoFilterModel } from '@/components/filter-bar/filter-bar.types'; import { type BentoAllFiltersModalSelectedFilter, type BentoAllFiltersModalSelectedFilterInstance, } from '../../all-filters-modal.types'; const props = withDefaults( defineProps<{ /** Actions displayed below the selected filter on small screens. */ actions?: BentoButtonActionsList; /** Label for the clear or reset button. */ clearButtonLabel: string; /** Disables the clear or reset button. */ isClearButtonDisabled: boolean; /** Currently selected filter. */ selectedFilter: BentoAllFiltersModalSelectedFilter; /** Displays the small-screen variant. */ smallScreen?: boolean; /** Controls whether the selected filter title is displayed. */ shouldShowFilterTitle: boolean; }>(), { actions: () => [], smallScreen: false, } ); defineEmits<{ /** * Emitted when the selected filter is cleared */ (event: 'clear'): void; /** * Emitted when the selected filter value changes */ (event: 'input', filter: BentoFilterModel): void; /** * Emitted when the left arrow key is pressed */ (event: 'keydown-left'): void; }>(); const conditionalClasses = computed(() => ({ 'b-all-filters-modal-selected-filter--small-screen': props.smallScreen, })); const selectedFilterRef = ref<BentoAllFiltersModalSelectedFilterInstance | null>(null); const focusFirstInput = () => { const selectedFilterElement = selectedFilterRef.value?.$el; if (!selectedFilterElement) { return; } getNextFocusableElement(1, selectedFilterElement).focus(); }; defineExpose({ focusFirstInput }); </script> <script lang="ts"> import { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoToggleFilter, } from '../../../index'; export default { name: 'all-filters-modal-selected-filter', components: { BentoBooleanFilter, BentoCheckboxGroupFilter, BentoDateFilter, BentoDateRangeFilter, BentoInputFilter, BentoInputWithDropdownFilter, BentoRadioGroupFilter, BentoRangeFilter, BentoSelectAvatarFilter, BentoSelectCountryFilter, BentoSelectCurrencyFilter, BentoSelectFilter, BentoSelectPaymentMethodFilter, BentoSelectTagFilter, BentoToggleFilter, }, }; </script> <style lang="scss" scoped src="./all-filters-modal-selected-filter.scss" />
@@ -0,0 +1 @@
1
+ export enum BaseFilterEvent { APPLY = 'apply', CLEAR = 'clear', OPEN = 'open', CLOSE = 'close', RESET = 'reset', CANCEL = 'cancel', }
@@ -1 +1 @@
1
- import { type BentoTooltipPosition } from '../../../../directives/tooltip/tooltip.types'; /** * Private */ export enum BaseFilterEvent { APPLY = 'apply', CLEAR = 'clear', OPEN = 'open', CLOSE = 'close', RESET = 'reset', CANCEL = 'cancel', } /** * Public */ export interface BentoBaseFilterProps<Value, Field> { /** * Total count of applied filters */ appliedFiltersCount?: string | number; /** * Overrides the label of the popover visibility toggle button text. */ buttonLabel?: string; /** * Sets the secondary label text of the popover visibility toggle button text. */ buttonSecondaryLabel?: string; /** * Disables the filter from being interacted with. */ disabled?: boolean; /** * Default value of the filter to reset to. */ defaultValue?: Value; /** * Renders the base filter's slot content only - leaving out the button and the popover. * Used for the allFiltersModal component. */ onlySlot?: boolean; /** * Allows to handle the controls (apply/clear) from outside * of the component */ controlled?: boolean; /** * Disable the "apply" button */ disableApplyButton?: boolean; /** * Disable the secondary button action. */ disableSecondaryButton?: boolean; /** * Filter name/key referencing the actual property name in the data set. * Should be unique per filter bar. */ field: Field; /** * Label text to be displayed */ label: string; /** * Sets the size of the container to fit its content */ fitContent?: boolean; /** * Attaches a `BentoTooltip` if present. Text showed in the tooltip on hover. */ tooltipText?: string; /** * Position the tooltip should take if space is available. */ tooltipPosition?: `${BentoTooltipPosition}`; /** * The current value of the filter, used for v-model. */ value?: Value; }
1
+ import { type BentoTooltipPosition } from '../../../../directives/tooltip'; export interface BentoBaseFilterProps<Value, Field> { /** * Total count of applied filters */ appliedFiltersCount?: string | number; /** * Overrides the label of the popover visibility toggle button text. */ buttonLabel?: string; /** * Sets the secondary label text of the popover visibility toggle button text. */ buttonSecondaryLabel?: string; /** * Disables the filter from being interacted with. */ disabled?: boolean; /** * Default value of the filter to reset to. */ defaultValue?: Value; /** * Renders the base filter's slot content only - leaving out the button and the popover. * Used for the allFiltersModal component. */ onlySlot?: boolean; /** * Allows to handle the controls (apply/clear) from outside * of the component */ controlled?: boolean; /** * Disable the "apply" button */ disableApplyButton?: boolean; /** * Disable the secondary button action. */ disableSecondaryButton?: boolean; /** * Filter name/key referencing the actual property name in the data set. * Should be unique per filter bar. */ field: Field; /** * Label text to be displayed */ label: string; /** * Sets the size of the container to fit its content */ fitContent?: boolean; /** * Attaches a `BentoTooltip` if present. Text showed in the tooltip on hover. */ tooltipText?: string; /** * Position the tooltip should take if space is available. */ tooltipPosition?: `${BentoTooltipPosition}`; /** * The current value of the filter, used for v-model. */ value?: Value; }
@@ -1 +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="filterButtonEl"> <bento-popover :id="popoverId" :ref="setPopoverRef" class="b-base-filter__popover" divider :aria-label="label" :open="isFilterOpen" :actions="controlled ? null : actions" actions-layout="space-between" :target-element="filterButtonEl" :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 filterButtonEl = computed(() => filterButtonRef.value?.$el ?? null); const setPopoverRef = (instance: unknown) => { popoverRef.value = instance ?? 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>
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="filterButtonEl"> <bento-popover :id="popoverId" :ref="setPopoverRef" class="b-base-filter__popover" divider :aria-label="label" :open="isFilterOpen" :actions="controlled ? null : actions" actions-layout="space-between" :target-element="filterButtonEl" :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 } from './base-filter.internal.types'; import { type BentoBaseFilterProps } from './base-filter.types'; import messages from './messages.json'; import { generateUid } from '@/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 filterButtonEl = computed(() => filterButtonRef.value?.$el ?? null); const setPopoverRef = (instance: unknown) => { popoverRef.value = instance ?? 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>
@@ -1 +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 Since v2.0.0. 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" />
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 Since v3.0.0. 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.`, '3.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" />
@@ -1 +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" :value="value" fit-content @apply="updateFilter" @clear="clearFilter" @open="openFilter" @reset="resetFilter" > <bento-date-picker v-if="onlySlot || isTouchDevice" :class="touchDeviceDatePickerConditionalClasses" :aria-labelledby="$attrs?.['aria-labelledby']" :value="internalFilterValue" :first-day-of-week="options?.firstDayOfWeek" :is-date-disabled="options?.isDateDisabled" :variant="options?.variant" :year-range="options?.yearRange" :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" :year-range="options?.yearRange" @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'; import { useTouchDevice } from '@/composables'; /** * 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 { isTouchDevice } = useTouchDevice(); 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(); }; const touchDeviceDatePickerConditionalClasses = computed(() => ({ 'b-date-filter--touch-device': isTouchDevice.value, })); return { // Values internalFilterValue, isApplyButtonDisabled, isSecondaryButtonDisabled, buttonLabel, buttonSecondaryLabel, isTouchDevice, touchDeviceDatePickerConditionalClasses, // Events clearFilter, onDateSelected, openFilter, updateFilter, resetFilter, onDateInputSelected, }; }, }); </script> <style lang="scss" scoped src="./date-filter.scss" />
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" :value="value" fit-content @apply="updateFilter" @clear="clearFilter" @open="openFilter" @reset="resetFilter" > <bento-date-picker v-if="onlySlot || isTouchDevice" :class="touchDeviceDatePickerConditionalClasses" :aria-labelledby="$attrs?.['aria-labelledby']" :model-value="internalFilterValue" :first-day-of-week="options?.firstDayOfWeek" :is-date-disabled="options?.isDateDisabled" :variant="options?.variant" :year-range="options?.yearRange" :disabled="disabled" @update:model-value="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" :year-range="options?.yearRange" @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'; import { useTouchDevice } from '@/composables'; /** * 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 { isTouchDevice } = useTouchDevice(); 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(); }; const touchDeviceDatePickerConditionalClasses = computed(() => ({ 'b-date-filter--touch-device': isTouchDevice.value, })); return { // Values internalFilterValue, isApplyButtonDisabled, isSecondaryButtonDisabled, buttonLabel, buttonSecondaryLabel, isTouchDevice, touchDeviceDatePickerConditionalClasses, // Events clearFilter, onDateSelected, openFilter, updateFilter, resetFilter, onDateInputSelected, }; }, }); </script> <style lang="scss" scoped src="./date-filter.scss" />
@@ -1 +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" :value="value" 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" :year-range="options?.yearRange" :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" :aria-labelledby="$attrs?.['aria-labelledby']" @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" :year-range="options?.yearRange" @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 { isSameDay } from 'date-fns/isSameDay'; import { format } from 'date-fns/format'; import { isEqual } from 'date-fns/isEqual'; import { isToday } from 'date-fns/isToday'; import { dateToTimeInputString } from '@/utils/ts/format-date'; import { useDateInputFormatter } from '@/composables/use-date-input-formatter/use-date-input-formatter'; 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 { dateToInputDateString } = useDateInputFormatter(); 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 baseFilter = ref(null); const isDateIncorrect = ref(false); const formData = reactive({ startDate: dateToInputDateString(props.value?.startDate), endDate: dateToInputDateString(props.value?.endDate), startTime: dateToTimeInputString(props.value?.startDate), endTime: dateToTimeInputString(props.value?.endDate), }); watch( () => props.value, () => { formData.startDate = dateToInputDateString(props.value?.startDate); formData.endDate = dateToInputDateString(props.value?.endDate); formData.startTime = dateToTimeInputString(props.value?.startDate); formData.endTime = dateToTimeInputString(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 = dateToInputDateString(props.value?.startDate); formData.endDate = dateToInputDateString(props.value?.endDate); formData.startTime = dateToTimeInputString(props.value?.startDate); formData.endTime = dateToTimeInputString(props.value?.endDate); }; const onDateSelected = (selectedDate: BentoDateRangePickerValue) => { isSelectingDate.value = false; internalFilterValue.value = selectedDate; formData.startDate = dateToInputDateString(selectedDate.startDate); formData.endDate = dateToInputDateString(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 = dateToInputDateString(quickSelectRanges.startDate); formData.endDate = dateToInputDateString(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" />
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" :value="value" 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" :year-range="options?.yearRange" :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" :time-interval="options?.timeInterval" :aria-labelledby="$attrs?.['aria-labelledby']" @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" :time-interval="options?.timeInterval" :variant="options?.variant" :year-range="options?.yearRange" @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 { isSameDay } from 'date-fns/isSameDay'; import { format } from 'date-fns/format'; import { isEqual } from 'date-fns/isEqual'; import { isToday } from 'date-fns/isToday'; import { dateToTimeInputString } from '@/utils/ts/format-date'; import { useDateInputFormatter, useDynamicDateFnsLocale } from '@/composables'; 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. * - timeInterval: Limits the selectable times to a fixed interval, in minutes. Requires allowTimeInput * - 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 dateLocale = useDynamicDateFnsLocale(locale); const { dateToInputDateString } = useDateInputFormatter(); 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 baseFilter = ref(null); const isDateIncorrect = ref(false); const formData = reactive({ startDate: dateToInputDateString(props.value?.startDate), endDate: dateToInputDateString(props.value?.endDate), startTime: dateToTimeInputString(props.value?.startDate), endTime: dateToTimeInputString(props.value?.endDate), }); watch( () => props.value, () => { formData.startDate = dateToInputDateString(props.value?.startDate); formData.endDate = dateToInputDateString(props.value?.endDate); formData.startTime = dateToTimeInputString(props.value?.startDate); formData.endTime = dateToTimeInputString(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: dateLocale.value, })} - ${format(currentValue.endDate, 'HH:mm:ss', { locale: dateLocale.value, })}` : `${format(currentValue.startDate, 'dd MMM, yyyy', { locale: dateLocale.value, })} - ${format(currentValue.endDate, 'dd MMM, yyyy', { locale: dateLocale.value, })}`; }; const buttonSecondaryLabel = computed(() => 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 = dateToInputDateString(props.value?.startDate); formData.endDate = dateToInputDateString(props.value?.endDate); formData.startTime = dateToTimeInputString(props.value?.startDate); formData.endTime = dateToTimeInputString(props.value?.endDate); }; const onDateSelected = (selectedDate: BentoDateRangePickerValue) => { isSelectingDate.value = false; internalFilterValue.value = selectedDate; formData.startDate = dateToInputDateString(selectedDate.startDate); formData.endDate = dateToInputDateString(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 = dateToInputDateString(quickSelectRanges.startDate); formData.endDate = dateToInputDateString(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; } }; 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" />
@@ -1 +1 @@
1
- import type { BentoInputFieldProps } from '../../../input-field/input-field.types'; import type { BentoTooltipPosition } from '@/directives/tooltip/tooltip.types'; /** * @deprecated Since v2.0.0. Use `BentoFilterItemType.INPUT_DROPDOWN` instead. */ export interface BentoInputFilterInputAndDropdownValues { inputValue: string | null; dropdownValue?: string; } export type BentoInputFilterValue = string | null | BentoInputFilterInputAndDropdownValues; export interface BentoInputFilterOptions { /** * @deprecated Since v2.0.0. Do not use. Use `BentoFilterItemType.INPUT_DROPDOWN` instead if `dropdown` is needed. */ dropdown?: any; /** * @deprecated Since v2.0.0. Do not use. Use `BentoFilterItemType.INPUT_DROPDOWN` instead if `dropdownPosition` is needed. */ dropdownPosition?: any; /** * Sets a placeholder in the input field */ placeholder?: BentoInputFieldProps['placeholder']; /** * @deprecated Since v2.0.0. Do not use. Use `BentoFilterItemType.INPUT_DROPDOWN` instead if `variant` is needed. */ variant?: 'dropdown'; /** * Attaches a `BentoTooltip` if present. Text showed in the tooltip on hover. */ tooltipText?: string; /** * Position the tooltip should take if space is available. */ tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoInputFilterProps { /** * Default value of the filter to reset to. */ defaultValue?: string; /** * Disables the filter from being interacted with. */ disabled?: boolean; /** * Filter name/key referencing the actual property name in the data set. * Should be unique per filter bar. */ field: string; /** * Label text to be displayed */ label: string; /** * Renders the base filter's slot content only - leaving out the button and the popover. * Used for the allFiltersModal component. */ onlySlot?: boolean; /** * List of options that are needed to setup the filter * Available options: * - placeholder: allows to set a placeholder to the search bar * - dropdown (deprecated): all the dropdown properties that can be set in the inner dropdown of the input field. @see BentoDropdown for more info. * - dropdownPosition (deprecated): allow the user to set the dropdown on the left or right side. Accepts the `start` or `end` values. * - variant (deprecated): can be `undefined` or set to `dropdown`. Enables dropdown input variant. * - tooltipPosition: The position of the tooltip attached to the filter button * - tooltipText: The content of the tooltip attached to the filter button */ options?: BentoInputFilterOptions; /** * Current value of the filter. * If the options.variant is equal to 'dropdown', the value should be of type `{ inputValue: string | null; dropdownValue?: string }`. */ value?: BentoInputFilterValue; }
1
+ import type { BentoInputFieldProps } from '../../../input-field/input-field.types'; import type { BentoTooltipPosition } from '@/directives/tooltip/tooltip.types'; export type BentoInputFilterValue = string | null; export interface BentoInputFilterOptions { /** * Sets a placeholder in the input field */ placeholder?: BentoInputFieldProps['placeholder']; /** * Attaches a `BentoTooltip` if present. Text showed in the tooltip on hover. */ tooltipText?: string; /** * Position the tooltip should take if space is available. */ tooltipPosition?: BentoTooltipPosition | `${BentoTooltipPosition}`; } export interface BentoInputFilterProps { /** * Default value of the filter to reset to. */ defaultValue?: string; /** * Disables the filter from being interacted with. */ disabled?: boolean; /** * Filter name/key referencing the actual property name in the data set. * Should be unique per filter bar. */ field: string; /** * Label text to be displayed */ label: string; /** * Renders the base filter's slot content only - leaving out the button and the popover. * Used for the allFiltersModal component. */ onlySlot?: boolean; /** * List of options that are needed to setup the filter * Available options: * - placeholder: allows to set a placeholder to the search bar * - tooltipPosition: The position of the tooltip attached to the filter button * - tooltipText: The content of the tooltip attached to the filter button */ options?: BentoInputFilterOptions; /** * Current value of the filter. */ value?: BentoInputFilterValue; }
@@ -1 +1 @@
1
- <template> <bento-base-filter v-bind="$props" :button-label="buttonLabel" :button-secondary-label="buttonSecondaryLabel" :disable-apply-button="isApplyButtonDisabled" :disable-secondary-button="isSecondaryButtonDisabled" :only-slot="onlySlot" :popover-overflow-visible="isDropdownVariant" :tooltip-position="options?.tooltipPosition" :tooltip-text="options?.tooltipText" @apply="updateFilter" @clear="clearFilter" @open="openFilter" @reset="resetFilter" > <div class="b-input-filter" :class="conditionalClasses" @keydown.enter="updateFilter" @input="onInput"> <bento-input-field ref="inputTextRef" :dropdown="dropdownProp" :dropdown-position="options?.dropdownPosition" :placeholder="options?.placeholder" :variant="inputVariant" :model-value="internalFilterValue" :disabled="disabled" @update:model-value="setInternalFilterValue" @dropdown-input="onDropdownInput" /> </div> </bento-base-filter> </template> <script setup lang="ts"> import { computed, onBeforeUpdate, provide, ref, toRef, watch } from 'vue'; import { BentoBaseFilter, useBaseFilter } from '../base-filter'; import { type BentoInputFilterInputAndDropdownValues, type BentoInputFilterValue, } from '@/components/filter-bar/filter-bar.types'; import { BentoInputField } from '@/components/input-field'; import { truncate } from '@/utils/ts/truncate'; import { type BentoListboxSelectedValue } from '@/types/listbox'; import { INPUT_FILTER_POPOVER_INJECTION_KEY } from '@/components/filter-bar/components/input-filter/input-filter.keys'; import { deprecate } from '@/utils/ts/deprecate'; import { type BentoInputFilterProps } from './input-filter.types'; const props = withDefaults(defineProps<BentoInputFilterProps>(), { defaultValue: null, disabled: false, onlySlot: false, options: undefined, value: null, }); const emit = defineEmits<{ /** * Fires upon the user interacting with the filter */ (e: 'input', props: BentoInputFilterProps): void; /** * Fires on the (new) filter values being applied */ (e: 'update', props: BentoInputFilterProps): void; }>(); const inputTextRef = ref(null); // Dropdown const dropdownValue = ref<BentoListboxSelectedValue>(props?.options?.dropdown?.value); const isDropdownVariant = computed( () => props?.options?.variant === 'dropdown' && props?.options?.dropdown !== undefined ); const dropdownProp = computed(() => isDropdownVariant.value ? { ...props?.options?.dropdown, value: dropdownValue.value } : undefined ); // For now we only allow the dropdown or the default variant for the input field. const inputVariant = computed(() => (props?.options?.variant === 'dropdown' ? 'dropdown' : 'default')); // Computed value as the input value location will be different if it is a dropdown variant const valueProp = computed(() => isDropdownVariant.value ? (props.value as BentoInputFilterInputAndDropdownValues)?.inputValue : (props?.value as string) ); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled } = useBaseFilter<string>( valueProp, toRef(props, 'defaultValue') ); const buttonLabel = computed(() => props.label); const buttonSecondaryLabel = computed(() => truncate(valueProp.value, 12)); const conditionalClasses = computed(() => ({ [`b-input-filter--only-slot`]: props.onlySlot, })); onBeforeUpdate(() => { // Set value to null when the string is empty if (internalFilterValue.value === '') { internalFilterValue.value = null; } }); watch(inputTextRef, () => { // Focus on the input when it's opened inputTextRef.value?.focusInput(); }); const openFilter = () => { internalFilterValue.value = valueProp.value; }; const updateFilter = () => { if (!internalFilterValue.value && props.defaultValue !== null) { // If value set to empty, reset back to default value internalFilterValue.value = props.defaultValue; } emit('update', { ...props, value: isDropdownVariant.value ? ({ inputValue: internalFilterValue.value, dropdownValue: dropdownValue.value, } as BentoInputFilterValue) : internalFilterValue.value, }); }; const resetFilter = () => emit('update', { ...props, value: props.defaultValue }); const clearFilter = () => { internalFilterValue.value = null; updateFilter(); }; const onInput = () => { emit('input', { ...props, value: internalFilterValue.value }); }; const setInternalFilterValue = (event: string) => { internalFilterValue.value = event; }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { dropdownValue.value = selectedValue; }; // We need to change the base filter's popover to have overflow visible so the dropdown options is // not hidden inside the filter when opened. provide(INPUT_FILTER_POPOVER_INJECTION_KEY, isDropdownVariant.value); // Verify that no unsopported attributes are passed to the component watch( () => props.options, () => { if (props.options?.dropdown || props.options?.dropdownPosition || props.options?.variant) { deprecate( 'BentoInputFilter "options" property', `BentoInputFilter "options" no longer supports "dropdown", "dropdownPosition" or "variant" attributes. Use the "BentoFilterItemType.INPUT_DROPDOWN" instead of "BentoFilterItemType.INPUT" <bento-filter-bar :config="[{ field: 'name', label: 'Name', type: BentoFilterItemType.INPUT_DROPDOWN, options: [...], }]" />`, '2.0.0' ); } }, { immediate: true } ); </script> <script lang="ts"> export default { name: 'bento-input-filter', }; </script> <style lang="scss" scoped src="./input-filter.scss" />
1
+ <template> <bento-base-filter v-bind="$props" :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-input-filter" :class="conditionalClasses" @keydown.enter="updateFilter" @input="onInput"> <bento-input-field ref="inputTextRef" :placeholder="options?.placeholder" :model-value="internalFilterValue" :disabled="disabled" @update:model-value="setInternalFilterValue" /> </div> </bento-base-filter> </template> <script setup lang="ts"> import { computed, onBeforeUpdate, ref, toRef, watch } from 'vue'; import { BentoBaseFilter, useBaseFilter } from '../base-filter'; import { BentoInputField } from '@/components/input-field'; import { truncate } from '@/utils/ts/truncate'; import { type BentoInputFilterProps } from './input-filter.types'; const props = withDefaults(defineProps<BentoInputFilterProps>(), { defaultValue: null, disabled: false, onlySlot: false, options: undefined, value: null, }); const emit = defineEmits<{ /** * Fires upon the user interacting with the filter */ (e: 'input', props: BentoInputFilterProps): void; /** * Fires on the (new) filter values being applied */ (e: 'update', props: BentoInputFilterProps): void; }>(); const inputTextRef = ref(null); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled } = useBaseFilter<string>( toRef(props, 'value'), toRef(props, 'defaultValue') ); const buttonLabel = computed(() => props.label); const buttonSecondaryLabel = computed(() => truncate(props.value, 12)); const conditionalClasses = computed(() => ({ [`b-input-filter--only-slot`]: props.onlySlot, })); onBeforeUpdate(() => { // Set value to null when the string is empty if (internalFilterValue.value === '') { internalFilterValue.value = null; } }); watch(inputTextRef, () => { // Focus on the input when it's opened inputTextRef.value?.focusInput(); }); const openFilter = () => { internalFilterValue.value = props.value; }; const updateFilter = () => { if (!internalFilterValue.value && props.defaultValue !== null) { // If value set to empty, reset back to default value internalFilterValue.value = props.defaultValue; } emit('update', { ...props, value: internalFilterValue.value, }); }; const resetFilter = () => emit('update', { ...props, value: props.defaultValue }); const clearFilter = () => { internalFilterValue.value = null; updateFilter(); }; const onInput = () => { emit('input', { ...props, value: internalFilterValue.value }); }; const setInternalFilterValue = (event: string) => { internalFilterValue.value = event; }; </script> <script lang="ts"> export default { name: 'bento-input-filter', }; </script> <style lang="scss" scoped src="./input-filter.scss" />
@@ -1 +1 @@
1
- import { action } from '@storybook/addon-actions'; import { type Meta, type StoryObj } from '@storybook/vue'; import { ref } from 'vue'; import { isVue2 } from 'vue-demi'; import BentoInputWithDropdownFilter from './input-with-dropdown-filter.vue'; import { type BentoInputWithDropdownFilterOptions } from '../../filter-bar.types'; export default { title: 'Filter bar/Input with dropdown filter', component: BentoInputWithDropdownFilter, 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 }, }, }, } satisfies Meta; type Story = StoryObj<typeof BentoInputWithDropdownFilter>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoInputWithDropdownFilter }, props: Object.keys(argTypes), template: ` <div style="padding-top: 24px"> <bento-input-with-dropdown-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, }; }, }), args: { label: 'Input with dropdown', field: 'input', options: { dropdown: { items: [ { label: 'EUR', value: 'EUR' }, { label: 'USD', value: 'USD' }, ], value: 'EUR', }, } satisfies BentoInputWithDropdownFilterOptions, }, };
1
+ import { action } from '@storybook/addon-actions'; import { type Meta, type StoryObj } from '@storybook/vue'; import { ref } from 'vue'; import { isVue2 } from 'vue-demi'; import BentoInputWithDropdownFilter from './input-with-dropdown-filter.vue'; import { type BentoInputWithDropdownFilterOptions } from '../../filter-bar.types'; export default { title: 'Filter bar/Input with dropdown filter', component: BentoInputWithDropdownFilter, 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 }, }, }, } satisfies Meta; type Story = StoryObj<typeof BentoInputWithDropdownFilter>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoInputWithDropdownFilter }, props: Object.keys(argTypes), template: ` <div style="padding-top: 24px"> <bento-input-with-dropdown-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, }; }, }), args: { label: 'Input with dropdown', field: 'input', options: { dropdown: { items: [ { label: 'EUR', value: 'EUR' }, { label: 'USD', value: 'USD' }, ], modelValue: 'EUR', }, } satisfies BentoInputWithDropdownFilterOptions, }, };
@@ -1 +1 @@
1
- <template> <bento-base-filter v-bind="$props" :applied-filters-count="selectedDropdownLabel" :button-label="buttonLabel" :button-secondary-label="buttonSecondaryLabel" :disable-apply-button="shouldDisableApply" :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-input-with-dropdown-filter" :class="conditionalClasses" @keydown.enter="updateFilter" @input="onInput" > <bento-input-field ref="inputTextRef" :dropdown="dropdownOptions" :dropdown-position="options?.dropdownPosition" variant="dropdown" :model-value="internalFilterValue?.inputValue" :disabled="disabled" @update:model-value="setInternalFilterValue" @dropdown-input="onDropdownInput" /> </div> </bento-base-filter> </template> <script setup lang="ts"> import { computed, onBeforeUpdate, provide, ref, toRef, watch } from 'vue'; import { BentoBaseFilter, useBaseFilter } from '../base-filter'; import { BentoFilterEvent, type BentoInputWithDropdownFilterOptions, type BentoInpuWithDropdowntFilterValue, } from '../../filter-bar.types'; import { type BentoInputDropdownProps, BentoInputField } from '@/components/input-field'; import { truncate } from '@/utils/ts/truncate'; import { type BentoListboxSelectedValue } from '@/types/listbox'; import { INPUT_FILTER_WITH_DROPDOWN_POPOVER_INJECTION_KEY } from '@/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.keys'; const props = withDefaults( defineProps<{ /** * Disables the filter from being interacted with. */ disabled?: boolean; /** * Default value of the filter to reset to. */ defaultValue?: BentoInpuWithDropdowntFilterValue; /** * Filter name/key referencing the actual property name in the data set. * Should be unique per filter bar. */ field: string; /** * Label text to be displayed */ label: string; /** * Renders the base filter's slot content only - leaving out the button and the popover. * Used for the allFiltersModal component. */ onlySlot?: boolean; /** * List of options that are needed to setup the filter * Available options: * - dropdown: all the dropdown properties that can be set in the inner dropdown of the input field. @see BentoDropdown for more info. * - dropdownPosition: allow the user to set the dropdown on the left or right side. Accepts the `start` or `end` values. * - tooltipPosition: The position of the tooltip attached to the filter button * - tooltipText: The content of the tooltip attached to the filter button */ options?: BentoInputWithDropdownFilterOptions; /** * Current value of the filter. * If the options.variant is equal to 'dropdown', the value should be of type should be `{ inputValue: string | null; dropdownValue?: string }`. */ value?: BentoInpuWithDropdowntFilterValue; }>(), { defaultValue: undefined, onlySlot: false, options: undefined, value: undefined, } ); const emit = defineEmits([BentoFilterEvent.INPUT, BentoFilterEvent.UPDATE]); const inputTextRef = ref(null); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled } = useBaseFilter<BentoInpuWithDropdowntFilterValue>(toRef(props, 'value'), toRef(props, 'defaultValue')); const selectedDropdownLabel = computed(() => { const selectedValue = props.value?.dropdownValue ?? props.options?.dropdown?.modelValue ?? props.options?.dropdown?.value; if ( !buttonSecondaryLabel.value || selectedValue === undefined || selectedValue === null || !props.options?.dropdown?.items ) { return null; } const label = props.options.dropdown.items.find(item => item.value === selectedValue)?.label; return label ? truncate(label, 12) : null; }); const buttonLabel = computed(() => props.label); const buttonSecondaryLabel = computed(() => props.value?.inputValue ? truncate(props.value.inputValue, 12) : undefined ); const dropdownSelection = computed( () => internalFilterValue.value?.dropdownValue ?? props.options?.dropdown?.modelValue ?? props.options?.dropdown?.value ); const dropdownOptions = computed<BentoInputDropdownProps>(() => ({ ...((props.options?.dropdown || {}) as BentoInputDropdownProps), modelValue: dropdownSelection.value, })); const conditionalClasses = computed(() => ({ [`b-input-with-dropdown-filter--only-slot`]: props.onlySlot, })); const shouldDisableApply = computed(() => { const allFieldsAreSet = !!internalFilterValue.value?.inputValue && !!dropdownSelection.value; return !allFieldsAreSet || isApplyButtonDisabled.value; }); onBeforeUpdate(() => { // Set value to null when the string is empty if (internalFilterValue.value?.inputValue === '') { internalFilterValue.value = { ...internalFilterValue.value, inputValue: undefined }; } }); watch(inputTextRef, () => { // Focus on the input when it's opened inputTextRef.value?.focusInput(); }); const openFilter = () => { internalFilterValue.value = props.value; }; const updateFilter = () => { if (!internalFilterValue.value?.inputValue && props.defaultValue !== null) { // If value set to empty, reset back to default value internalFilterValue.value = { ...props.defaultValue }; } emit(BentoFilterEvent.UPDATE, { ...props, value: internalFilterValue.value, }); }; const resetFilter = () => emit(BentoFilterEvent.UPDATE, { ...props, value: props.defaultValue }); const clearFilter = () => { internalFilterValue.value = null; updateFilter(); }; const onInput = () => { emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const setInternalFilterValue = (event: string) => { internalFilterValue.value = { dropdownValue: dropdownSelection.value, inputValue: event }; }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { internalFilterValue.value = { ...internalFilterValue.value, dropdownValue: selectedValue }; }; // We need to change the base filter's popover to have overflow visible so the dropdown options is // not hidden inside the filter when opened. provide(INPUT_FILTER_WITH_DROPDOWN_POPOVER_INJECTION_KEY, true); </script> <script lang="ts"> /** * */ export default { name: 'bento-input-with-dropdown-filter', }; </script> <style lang="scss" scoped src="./input-with-dropdown-filter.scss" />
1
+ <template> <bento-base-filter v-bind="$props" :applied-filters-count="selectedDropdownLabel" :button-label="buttonLabel" :button-secondary-label="buttonSecondaryLabel" :disable-apply-button="shouldDisableApply" :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-input-with-dropdown-filter" :class="conditionalClasses" @keydown.enter="updateFilter" @input="onInput" > <bento-input-field ref="inputTextRef" :dropdown="dropdownOptions" :dropdown-position="options?.dropdownPosition" variant="dropdown" :model-value="internalFilterValue?.inputValue" :disabled="disabled" @update:model-value="setInternalFilterValue" @dropdown-input="onDropdownInput" /> </div> </bento-base-filter> </template> <script setup lang="ts"> import { computed, onBeforeUpdate, provide, ref, toRef, watch } from 'vue'; import { BentoBaseFilter, useBaseFilter } from '../base-filter'; import { BentoFilterEvent, type BentoInputWithDropdownFilterOptions, type BentoInpuWithDropdowntFilterValue, } from '../../filter-bar.types'; import { type BentoInputDropdownProps, BentoInputField } from '@/components/input-field'; import { truncate } from '@/utils/ts/truncate'; import { type BentoListboxSelectedValue } from '@/types/listbox'; import { INPUT_FILTER_WITH_DROPDOWN_POPOVER_INJECTION_KEY } from '@/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.keys'; const props = withDefaults( defineProps<{ /** * Disables the filter from being interacted with. */ disabled?: boolean; /** * Default value of the filter to reset to. */ defaultValue?: BentoInpuWithDropdowntFilterValue; /** * Filter name/key referencing the actual property name in the data set. * Should be unique per filter bar. */ field: string; /** * Label text to be displayed */ label: string; /** * Renders the base filter's slot content only - leaving out the button and the popover. * Used for the allFiltersModal component. */ onlySlot?: boolean; /** * List of options that are needed to setup the filter * Available options: * - dropdown: all the dropdown properties that can be set in the inner dropdown of the input field. @see BentoDropdown for more info. * - dropdownPosition: allow the user to set the dropdown on the left or right side. Accepts the `start` or `end` values. * - tooltipPosition: The position of the tooltip attached to the filter button * - tooltipText: The content of the tooltip attached to the filter button */ options?: BentoInputWithDropdownFilterOptions; /** * Current value of the filter. * If the options.variant is equal to 'dropdown', the value should be of type should be `{ inputValue: string | null; dropdownValue?: string }`. */ value?: BentoInpuWithDropdowntFilterValue; }>(), { defaultValue: undefined, onlySlot: false, options: undefined, value: undefined, } ); const emit = defineEmits([BentoFilterEvent.INPUT, BentoFilterEvent.UPDATE]); const inputTextRef = ref(null); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled } = useBaseFilter<BentoInpuWithDropdowntFilterValue>(toRef(props, 'value'), toRef(props, 'defaultValue')); const selectedDropdownLabel = computed(() => { const selectedValue = props.value?.dropdownValue ?? props.options?.dropdown?.modelValue; if ( !buttonSecondaryLabel.value || selectedValue === undefined || selectedValue === null || !props.options?.dropdown?.items ) { return null; } const label = props.options.dropdown.items.find(item => item.value === selectedValue)?.label; return label ? truncate(label, 12) : null; }); const buttonLabel = computed(() => props.label); const buttonSecondaryLabel = computed(() => props.value?.inputValue ? truncate(props.value.inputValue, 12) : undefined ); const dropdownSelection = computed( () => internalFilterValue.value?.dropdownValue ?? props.options?.dropdown?.modelValue ); const dropdownOptions = computed<BentoInputDropdownProps>(() => ({ ...((props.options?.dropdown || {}) as BentoInputDropdownProps), modelValue: dropdownSelection.value, })); const conditionalClasses = computed(() => ({ [`b-input-with-dropdown-filter--only-slot`]: props.onlySlot, })); const shouldDisableApply = computed(() => { const allFieldsAreSet = !!internalFilterValue.value?.inputValue && !!dropdownSelection.value; return !allFieldsAreSet || isApplyButtonDisabled.value; }); onBeforeUpdate(() => { // Set value to null when the string is empty if (internalFilterValue.value?.inputValue === '') { internalFilterValue.value = { ...internalFilterValue.value, inputValue: undefined }; } }); watch(inputTextRef, () => { // Focus on the input when it's opened inputTextRef.value?.focusInput(); }); const openFilter = () => { internalFilterValue.value = props.value; }; const updateFilter = () => { if (!internalFilterValue.value?.inputValue && props.defaultValue !== null) { // If value set to empty, reset back to default value internalFilterValue.value = { ...props.defaultValue }; } emit(BentoFilterEvent.UPDATE, { ...props, value: internalFilterValue.value, }); }; const resetFilter = () => emit(BentoFilterEvent.UPDATE, { ...props, value: props.defaultValue }); const clearFilter = () => { internalFilterValue.value = null; updateFilter(); }; const onInput = () => { emit(BentoFilterEvent.INPUT, { ...props, value: internalFilterValue.value }); }; const setInternalFilterValue = (event: string) => { internalFilterValue.value = { dropdownValue: dropdownSelection.value, inputValue: event }; }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { internalFilterValue.value = { ...internalFilterValue.value, dropdownValue: selectedValue }; }; // We need to change the base filter's popover to have overflow visible so the dropdown options is // not hidden inside the filter when opened. provide(INPUT_FILTER_WITH_DROPDOWN_POPOVER_INJECTION_KEY, true); </script> <script lang="ts"> /** * */ export default { name: 'bento-input-with-dropdown-filter', }; </script> <style lang="scss" scoped src="./input-with-dropdown-filter.scss" />
@@ -1 +1 @@
1
- <template> <bento-base-filter :applied-filters-count="appliedFiltersCount" :button-label="buttonLabel" :button-secondary-label="buttonSecondaryLabel" :disabled="disabled" :disable-apply-button="isApplyButtonDisabled" :disable-secondary-button="isSecondaryButtonDisabled" :default-value="defaultValue" :label="label" :field="field" :only-slot="false" :tooltip-position="options?.tooltipPosition" :tooltip-text="options?.tooltipText" :value="value" @apply="updateFilter" @clear="clearFilter" @reset="resetFilter" @open="openFilter" @close="closeFilter" > <div class="b-select-filter-button__search-bar-wrapper"> <div v-if="options?.dynamicFiltering ?? true" class="b-select-filter-button__search-bar"> <bento-search-bar :debounce-time="options?.search?.debounceTime" :value="searchTerm" :input-field-aria-label="t('searchFilter', { filter: label.toLocaleLowerCase() })" @input="searchTerm = $event" /> </div> <div class="b-select-filter-button__divider"> <bento-divider /> </div> </div> <bento-listbox :id="listboxFilterId" :aria-label="label" class="b-select-filter-button__wrapper" :component-loading="options?.loading" :empty-state="options?.emptyState" :has-more-items="options?.lazy?.hasMoreItems" :items="filteredItems" :is-option-disabled="options?.isOptionDisabled" :lazy-load-type="options?.lazy?.lazyLoadType" :loading="options?.lazy?.loading" :multiple="options?.multiple" :static-categories="options?.staticCategories" :no-results-message="noResultsMessage" :searching="!!searchTerm" :selected-value="internalFilterValue" :virtual-scroll="options?.virtualScroll" @select="onOptionSelected" @space-pressed="onOptionSelected" @enter-pressed="onOptionSelected" @show-more="options?.lazy?.showMoreEvent()" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox> </bento-base-filter> </template> <script setup lang="ts"> import { computed, type PropType, ref, toRef, useSlots, watch } from 'vue'; // Components import BentoSearchBar from '@/components/search-bar/search-bar.vue'; import { BentoDivider } from '@/components/divider'; import { BentoListbox, useCachedSelectedValues } from '@/internal'; import { type BentoListboxMultiSelectValue, type BentoListboxOptionItem, type BentoListboxOptions, type BentoListboxSelectedValue, type BentoListboxSelectedValueLabelPair, type BentoListboxValue, } from '@/types/listbox'; import { BentoBaseFilter, useBaseFilter } from '../../../base-filter'; // Types import { BentoFilterEvent, type BentoSelectFilterOptions } from '../../../../filter-bar.types'; // Utilities import { useSearchBarFilter } from '@/components/search-bar'; import { generateUid } from '@/core/utils/ts'; import { truncate } from '@/utils/ts/truncate'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const slots = useSlots(); const emit = defineEmits<{ (e: 'input', value: any); (e: 'update', value: any); }>(); const props = defineProps({ /** * Disables the filter from being interacted with. */ disabled: { type: Boolean, default: false }, /** * Default value of the filter to reset to. * */ defaultValue: { type: [String, Number, Array] as PropType<BentoListboxSelectedValue>, 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: * - multiple: If "true", the user will be able to select multiple items. * - listboxItems: The option elements to populate the filter with. * * @default undefined */ options: { type: Object as PropType<BentoSelectFilterOptions>, default: undefined, }, /** * Current value of the filter * * @default undefined */ value: { type: [String, Number, Array, Object] as PropType<BentoListboxSelectedValue>, default: undefined, }, }); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const searchTerm = ref(''); const optionsRef = toRef(props, 'options'); const itemsRef = ref(props.options.listboxItems ?? []); const { cachedSelectedListboxOptions, setCachedValues, resetCache } = useCachedSelectedValues( itemsRef, toRef(props, 'value'), toRef(optionsRef.value, 'multiple') ); const { cachedSelectedListboxOptions: cachedDefaultSelectedListboxOptions } = useCachedSelectedValues( itemsRef, toRef(props, 'defaultValue'), toRef(optionsRef.value, 'multiple') ); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled } = useBaseFilter<BentoListboxOptions>(cachedSelectedListboxOptions, cachedDefaultSelectedListboxOptions); // We need to make sure to update the items if the consumer updates them manually watch(optionsRef, () => { itemsRef.value = optionsRef.value.listboxItems ?? []; }); const filteredItems = useSearchBarFilter<BentoListboxOptionItem>( searchTerm, itemsRef, props.options?.search?.searchEvent ); const noResultsMessage = computed(() => t('noFiltersMatchThisSearch') as string); const buttonLabel = computed(() => props.label); const buttonSecondaryLabel = computed(() => { const isSingleSelectAndOneItemSelected = !props?.options?.multiple && props.value != null; const isMultiSelectedAndOneItemSelected = props?.options?.multiple && Array.isArray(props.value) && props.value.length === 1; // When only 1 item is selected: Display the Label of the selected value or use value as Label if (isSingleSelectAndOneItemSelected || isMultiSelectedAndOneItemSelected) { // If enableValueLabelPair is enabled then "value" has the label, no need to search for it. if (props.options?.enableValueLabelPair) { let displayValue = (props.value as Array<BentoListboxSelectedValueLabelPair>)?.length > 0 ? (props.value[0] as BentoListboxSelectedValueLabelPair).label : (props.value as BentoListboxSelectedValueLabelPair).label; if (!displayValue) { displayValue = props.value as string; printDevelopmentWarning( 'BentoFilterBar Select filter (single & multi): The "value" provided is incorrect. The "value" must be of type "BentoListboxSelectedValueLabelPair" or "Array<BentoListboxSelectedValueLabelPair>" when "enableValueLabelPair" is set to true' ); } return truncate(displayValue as string, 12); } const flatListboxItems = props.options.listboxItems.flatMap(x => (x.items ? x.items : x)); // If the value is not in the list of Items set the value as secondary label. // This happens on external filtering let displayValue = flatListboxItems.find(({ value }) => Array.isArray(props.value) ? (props?.value as BentoListboxMultiSelectValue)?.includes(value as BentoListboxValue) : value === props.value )?.label; if (!displayValue && props.value) { displayValue = (props.value as BentoListboxMultiSelectValue)[0] as string; } return truncate(displayValue, 12); } return null; }); const listboxFilterId = computed(() => generateUid(`filter-listbox-${props.field}`)); const onOptionSelected = (selectedValue: BentoListboxOptions) => { internalFilterValue.value = selectedValue; const { onlySlot, ...cleanedProps } = props; // Return null if there are no selected values if (props.options?.multiple) { let value: BentoListboxSelectedValue; if (props.options?.enableValueLabelPair) { value = selectedValue.length ? internalFilterValue.value : null; } else { value = selectedValue.length ? internalFilterValue.value.map(({ value }) => value) : null; } emit(BentoFilterEvent.INPUT, { ...cleanedProps, value }); return; } let value: BentoListboxSelectedValue; if (props.options?.enableValueLabelPair) { value = selectedValue.length ? selectedValue[0] : null; } else { value = selectedValue.length ? selectedValue[0].value : null; } emit(BentoFilterEvent.INPUT, { ...cleanedProps, value }); }; const updateFilter = () => { // Omit the onlySlot prop as it is not needed const { onlySlot, ...cleanedProps } = props; if (props.options.multiple) { // If multi select filter and all items unchecked, // If persistent filter, reset back to default value, else reset back to undefined if (internalFilterValue.value && (internalFilterValue.value as Array<unknown>)?.length === 0) { internalFilterValue.value = props.defaultValue !== null ? cachedDefaultSelectedListboxOptions.value : undefined; } let value: BentoListboxSelectedValue; if (props.options?.enableValueLabelPair) { value = internalFilterValue.value?.length ? internalFilterValue.value : null; } else { value = internalFilterValue.value?.length ? internalFilterValue.value?.map(({ value }) => value) : null; } emit(BentoFilterEvent.UPDATE, { ...cleanedProps, value, }); return; } let value: BentoListboxSelectedValue; if (props.options?.enableValueLabelPair) { value = internalFilterValue.value.length ? internalFilterValue.value[0] : undefined; } else { value = internalFilterValue.value.length ? internalFilterValue.value[0].value : undefined; } emit(BentoFilterEvent.UPDATE, { ...cleanedProps, value }); }; const openFilter = () => { internalFilterValue.value = cachedSelectedListboxOptions.value; props.options?.onOpen?.(); }; const closeFilter = () => { // If the search term is set, reset it to "empty" if (searchTerm.value) { searchTerm.value = ''; } }; const resetFilter = () => { setCachedValues(cachedDefaultSelectedListboxOptions.value); internalFilterValue.value = cachedDefaultSelectedListboxOptions.value; updateFilter(); }; const clearFilter = () => { resetCache(); internalFilterValue.value = []; updateFilter(); }; const appliedFiltersCount = computed(() => { // Don't show if no filter applied or peristent filter or not multiple select if (props.value == null || !props.options.multiple) { return null; } /* * Show amount of items selceted (props.value) or 'All' if all items selected */ // If nested we need to get the correct count const allSelectableItemsCount = props.options.listboxItems?.reduce((acc, item) => { // eslint-disable-next-line no-param-reassign acc += item?.items?.length ? item.items.length : 1; return acc; }, 0); const selectedAmount = (props.value as Array<string | number>)?.length; if (selectedAmount === 1) { return null; } return selectedAmount === allSelectableItemsCount ? t('all') : selectedAmount; }); </script> <script lang="ts"> /** * The Listbox filter displays a listbox of items * passed in the "options.listboxItems" property. * * @example * import { BentoSelectFilter } from '@adyen/bento-vue2'; * * export default { * components: { BentoSelectFilter }, * template: ` * <bento-select-filter * :field="filter.field" * :label="filter.label" * :options="filter.options" * :value="filter.value" * @update="updateFilterHandler" * > * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./select-filter-button.scss" />
1
+ <template> <bento-base-filter :applied-filters-count="appliedFiltersCount" :button-label="buttonLabel" :button-secondary-label="buttonSecondaryLabel" :disabled="disabled" :disable-apply-button="isApplyButtonDisabled" :disable-secondary-button="isSecondaryButtonDisabled" :default-value="defaultValue" :label="label" :field="field" :only-slot="false" :tooltip-position="options?.tooltipPosition" :tooltip-text="options?.tooltipText" :value="value" @apply="updateFilter" @clear="clearFilter" @reset="resetFilter" @open="openFilter" @close="closeFilter" > <div class="b-select-filter-button__search-bar-wrapper"> <div v-if="options?.dynamicFiltering ?? true" class="b-select-filter-button__search-bar"> <bento-search-bar :debounce-time="options?.search?.debounceTime" :value="searchTerm" :input-field-aria-label="t('searchFilter', { filter: label.toLocaleLowerCase() })" @input="searchTerm = $event" /> </div> <div class="b-select-filter-button__divider"> <bento-divider /> </div> </div> <bento-listbox :id="listboxFilterId" :aria-label="label" class="b-select-filter-button__wrapper" :component-loading="options?.loading" :empty-state="options?.emptyState" :has-more-items="options?.lazy?.hasMoreItems" :items="filteredItems" :is-option-disabled="options?.isOptionDisabled" :lazy-load-type="options?.lazy?.lazyLoadType" :loading="options?.lazy?.loading" :multiple="options?.multiple" :static-categories="options?.staticCategories" :no-results-message="noResultsMessage" :searching="!!searchTerm" :selected-value="internalFilterValue" :virtual-scroll="options?.virtualScroll" @select="onOptionSelected" @space-pressed="onOptionSelected" @enter-pressed="onOptionSelected" @show-more="options?.lazy?.showMoreEvent()" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </bento-listbox> </bento-base-filter> </template> <script setup lang="ts"> import { computed, type PropType, ref, toRef, useSlots, watch } from 'vue'; // Components import BentoSearchBar from '@/components/search-bar/search-bar.vue'; import { BentoDivider } from '@/components/divider'; import { BentoListbox, useCachedSelectedValues } from '@/internal'; import { type BentoListboxMultiSelectValue, type BentoListboxOptionItem, type BentoListboxOptions, type BentoListboxSelectedValue, type BentoListboxSelectedValueLabelPair, type BentoListboxValue, } from '@/types/listbox'; import { BentoBaseFilter, useBaseFilter } from '../../../base-filter'; // Types import { BentoFilterEvent, type BentoSelectFilterOptions } from '../../../../filter-bar.types'; // Utilities import { useSearchBarFilter } from '@/components/search-bar'; import { generateUid } from '@/utils/ts/generate-uid'; import { truncate } from '@/utils/ts/truncate'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const slots = useSlots(); const emit = defineEmits<{ (e: 'input', value: any); (e: 'update', value: any); }>(); const props = defineProps({ /** * Disables the filter from being interacted with. */ disabled: { type: Boolean, default: false }, /** * Default value of the filter to reset to. * */ defaultValue: { type: [String, Number, Array] as PropType<BentoListboxSelectedValue>, 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: * - multiple: If "true", the user will be able to select multiple items. * - listboxItems: The option elements to populate the filter with. * * @default undefined */ options: { type: Object as PropType<BentoSelectFilterOptions>, default: undefined, }, /** * Current value of the filter * * @default undefined */ value: { type: [String, Number, Array, Object] as PropType<BentoListboxSelectedValue>, default: undefined, }, }); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const searchTerm = ref(''); const optionsRef = toRef(props, 'options'); const itemsRef = ref(props.options.listboxItems ?? []); const { cachedSelectedListboxOptions, setCachedValues, resetCache } = useCachedSelectedValues( itemsRef, toRef(props, 'value'), toRef(optionsRef.value, 'multiple') ); const { cachedSelectedListboxOptions: cachedDefaultSelectedListboxOptions } = useCachedSelectedValues( itemsRef, toRef(props, 'defaultValue'), toRef(optionsRef.value, 'multiple') ); const { internalFilterValue, isSecondaryButtonDisabled, isApplyButtonDisabled } = useBaseFilter<BentoListboxOptions>(cachedSelectedListboxOptions, cachedDefaultSelectedListboxOptions); // We need to make sure to update the items if the consumer updates them manually watch(optionsRef, () => { itemsRef.value = optionsRef.value.listboxItems ?? []; }); const filteredItems = useSearchBarFilter<BentoListboxOptionItem>( searchTerm, itemsRef, props.options?.search?.searchEvent ); const noResultsMessage = computed(() => t('noFiltersMatchThisSearch') as string); const buttonLabel = computed(() => props.label); const buttonSecondaryLabel = computed(() => { const isSingleSelectAndOneItemSelected = !props?.options?.multiple && props.value != null; const isMultiSelectedAndOneItemSelected = props?.options?.multiple && Array.isArray(props.value) && props.value.length === 1; // When only 1 item is selected: Display the Label of the selected value or use value as Label if (isSingleSelectAndOneItemSelected || isMultiSelectedAndOneItemSelected) { // If enableValueLabelPair is enabled then "value" has the label, no need to search for it. if (props.options?.enableValueLabelPair) { let displayValue = (props.value as Array<BentoListboxSelectedValueLabelPair>)?.length > 0 ? (props.value[0] as BentoListboxSelectedValueLabelPair).label : (props.value as BentoListboxSelectedValueLabelPair).label; if (!displayValue) { displayValue = props.value as string; printDevelopmentWarning( 'BentoFilterBar Select filter (single & multi): The "value" provided is incorrect. The "value" must be of type "BentoListboxSelectedValueLabelPair" or "Array<BentoListboxSelectedValueLabelPair>" when "enableValueLabelPair" is set to true' ); } return truncate(displayValue as string, 12); } const flatListboxItems = props.options.listboxItems.flatMap(x => (x.items ? x.items : x)); // If the value is not in the list of Items set the value as secondary label. // This happens on external filtering let displayValue = flatListboxItems.find(({ value }) => Array.isArray(props.value) ? (props?.value as BentoListboxMultiSelectValue)?.includes(value as BentoListboxValue) : value === props.value )?.label; if (!displayValue && props.value) { displayValue = (props.value as BentoListboxMultiSelectValue)[0] as string; } return truncate(displayValue, 12); } return null; }); const listboxFilterId = computed(() => generateUid(`filter-listbox-${props.field}`)); const onOptionSelected = (selectedValue: BentoListboxOptions) => { internalFilterValue.value = selectedValue; const { onlySlot, ...cleanedProps } = props; // Return null if there are no selected values if (props.options?.multiple) { let value: BentoListboxSelectedValue; if (props.options?.enableValueLabelPair) { value = selectedValue.length ? internalFilterValue.value : null; } else { value = selectedValue.length ? internalFilterValue.value.map(({ value }) => value) : null; } emit(BentoFilterEvent.INPUT, { ...cleanedProps, value }); return; } let value: BentoListboxSelectedValue; if (props.options?.enableValueLabelPair) { value = selectedValue.length ? selectedValue[0] : null; } else { value = selectedValue.length ? selectedValue[0].value : null; } emit(BentoFilterEvent.INPUT, { ...cleanedProps, value }); }; const updateFilter = () => { // Omit the onlySlot prop as it is not needed const { onlySlot, ...cleanedProps } = props; if (props.options.multiple) { // If multi select filter and all items unchecked, // If persistent filter, reset back to default value, else reset back to undefined if (internalFilterValue.value && (internalFilterValue.value as Array<unknown>)?.length === 0) { internalFilterValue.value = props.defaultValue !== null ? cachedDefaultSelectedListboxOptions.value : undefined; } let value: BentoListboxSelectedValue; if (props.options?.enableValueLabelPair) { value = internalFilterValue.value?.length ? internalFilterValue.value : null; } else { value = internalFilterValue.value?.length ? internalFilterValue.value?.map(({ value }) => value) : null; } emit(BentoFilterEvent.UPDATE, { ...cleanedProps, value, }); return; } let value: BentoListboxSelectedValue; if (props.options?.enableValueLabelPair) { value = internalFilterValue.value.length ? internalFilterValue.value[0] : undefined; } else { value = internalFilterValue.value.length ? internalFilterValue.value[0].value : undefined; } emit(BentoFilterEvent.UPDATE, { ...cleanedProps, value }); }; const openFilter = () => { internalFilterValue.value = cachedSelectedListboxOptions.value; props.options?.onOpen?.(); }; const closeFilter = () => { // If the search term is set, reset it to "empty" if (searchTerm.value) { searchTerm.value = ''; } }; const resetFilter = () => { setCachedValues(cachedDefaultSelectedListboxOptions.value); internalFilterValue.value = cachedDefaultSelectedListboxOptions.value; updateFilter(); }; const clearFilter = () => { resetCache(); internalFilterValue.value = []; updateFilter(); }; const appliedFiltersCount = computed(() => { // Don't show if no filter applied or peristent filter or not multiple select if (props.value == null || !props.options.multiple) { return null; } /* * Show amount of items selceted (props.value) or 'All' if all items selected */ // If nested we need to get the correct count const allSelectableItemsCount = props.options.listboxItems?.reduce((acc, item) => { // eslint-disable-next-line no-param-reassign acc += item?.items?.length ? item.items.length : 1; return acc; }, 0); const selectedAmount = (props.value as Array<string | number>)?.length; if (selectedAmount === 1) { return null; } return selectedAmount === allSelectableItemsCount ? t('all') : selectedAmount; }); </script> <script lang="ts"> /** * The Listbox filter displays a listbox of items * passed in the "options.listboxItems" property. * * @example * import { BentoSelectFilter } from '@adyen/bento-vue2'; * * export default { * components: { BentoSelectFilter }, * template: ` * <bento-select-filter * :field="filter.field" * :label="filter.label" * :options="filter.options" * :value="filter.value" * @update="updateFilterHandler" * > * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./select-filter-button.scss" />
@@ -1 +1 @@
1
- <template> <select-filter-dropdown v-if="onlySlot" :disabled="disabled" :default-value="defaultValue" :field="field" :label="label" :options="options" :value="value" @input="onInput" @update="onUpdate" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </select-filter-dropdown> <select-filter-button v-else :disabled="disabled" :default-value="defaultValue" :field="field" :label="label" :options="options" :value="value" @input="onInput" @update="onUpdate" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </select-filter-button> </template> <script setup lang="ts"> import { SelectFilterButton, SelectFilterDropdown } from './components'; import { onMounted, useSlots } from 'vue'; import { deprecate } from '@/utils/ts/deprecate'; import SelectFilterProps from '@/components/filter-bar/components/select-filter/select-filter.props'; const slots = useSlots(); const emit = defineEmits<{ (e: 'input', updatedProps: any): void; (e: 'update', updatedProps: any): void; }>(); const props = defineProps(SelectFilterProps); const onInput = (updatedProps: any) => emit('input', updatedProps); const onUpdate = (updatedProps: any) => emit('update', updatedProps); onMounted(() => { if (props?.options?.messages) { deprecate( 'BentoFilterBar filter of type "BentoFilterItemType.SELECT" options message property', `Please remove the "options.messages" property from this filter, it is not required any more for persistent filters. This will be the default behavior in the v2 and "options.messages" will be removed.`, '2.0.0' ); } }); </script> <script lang="ts"> /** * The Listbox filter displays a listbox of items * passed in the "options.listboxItems" property. * * @example * import { BentoSelectFilter } from '@adyen/bento-vue2'; * * export default { * components: { BentoSelectFilter }, * template: ` * <bento-select-filter * :field="filter.field" * :label="filter.label" * :options="filter.options" * :value="filter.value" * @update="updateFilterHandler" * > * ` * } */ export default {}; </script>
1
+ <template> <select-filter-dropdown v-if="onlySlot" :disabled="disabled" :default-value="defaultValue" :field="field" :label="label" :options="options" :value="value" @input="onInput" @update="onUpdate" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </select-filter-dropdown> <select-filter-button v-else :disabled="disabled" :default-value="defaultValue" :field="field" :label="label" :options="options" :value="value" @input="onInput" @update="onUpdate" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </select-filter-button> </template> <script setup lang="ts"> import { SelectFilterButton, SelectFilterDropdown } from './components'; import { useSlots } from 'vue'; import SelectFilterProps from '@/components/filter-bar/components/select-filter/select-filter.props'; const slots = useSlots(); const emit = defineEmits<{ (e: 'input', updatedProps: any): void; (e: 'update', updatedProps: any): void; }>(); defineProps(SelectFilterProps); const onInput = (updatedProps: any) => emit('input', updatedProps); const onUpdate = (updatedProps: any) => emit('update', updatedProps); </script> <script lang="ts"> /** * The Listbox filter displays a listbox of items * passed in the "options.listboxItems" property. * * @example * import { BentoSelectFilter } from '@adyen/bento-vue2'; * * export default { * components: { BentoSelectFilter }, * template: ` * <bento-select-filter * :field="filter.field" * :label="filter.label" * :options="filter.options" * :value="filter.value" * @update="updateFilterHandler" * > * ` * } */ export default {}; </script>
@@ -1 +1 @@
1
- import { ref } from 'vue'; import { action } from '@storybook/addon-actions'; import { type BentoSelectAvatarFilterOptions } from '../../../../filter-bar.types'; import { storybookDocsParameter } from '../../../../../../utils/ts/storybook'; import { BentoSelectAvatarFilter } from './index'; import { isVue2 } from 'vue-demi'; const OPTIONS: BentoSelectAvatarFilterOptions = { listboxItems: [ { label: 'Avatar 1', value: 'avatar-1', }, { label: 'Avatar 2', value: 'avatar-2', }, ], }; export default { title: 'Filter bar/Select avatar filter', component: BentoSelectAvatarFilter, argTypes: { // Slots field: { name: 'field', type: { name: 'string', required: false }, description: 'Filter name/key referencing the actual property name in the data set. Should be unique per filter bar.', control: 'text', table: { type: { summary: 'string' }, }, }, label: { name: 'label', type: { name: 'string', required: false }, description: 'Label text to be displayed', control: 'text', table: { type: { summary: 'string' }, }, }, value: { name: 'value', type: { name: 'string', required: true }, description: 'Current value of the filter', control: { disable: true }, table: { type: { summary: 'string, number, Array<string, number>' }, }, }, // Events eventName: { name: 'update', defaultValue: '', description: 'Description waht the event is for', table: { type: { summary: '() => void' } }, control: { disable: true }, }, }, }; export const Default = (_args, { argTypes }) => ({ components: { BentoSelectAvatarFilter }, props: Object.keys(argTypes), template: ` <bento-select-avatar-filter v-bind="args" :value="valueRef" @update="onSelected"/> `, setup(props) { const valueRef = ref(); const onSelected = selectedValue => { valueRef.value = selectedValue.value; action('update')(selectedValue); }; return { // Values args: isVue2 ? props : _args, valueRef, // Events onSelected, }; }, }); Default.args = { field: 'Avatar', label: 'Avatar', options: OPTIONS, }; Default.parameters = storybookDocsParameter(` <bento-select-avatar-filter v-model="selectedValue" :options="{ listboxOptions: [ { label: 'Avatar 1', value: 'avatar-id-1' }, { label: 'Avatar 2', value: 'avatar-id-2', src: 'image-source.jpeg' }, ]" }", label="Avatar" field="avatar" /> `);
1
+ import { ref } from 'vue'; import { action } from '@storybook/addon-actions'; import { type BentoSelectAvatarFilterOptions } from '../../../../filter-bar.types'; import { BentoSelectAvatarFilter } from './index'; import { isVue2 } from 'vue-demi'; const OPTIONS: BentoSelectAvatarFilterOptions = { listboxItems: [ { label: 'Avatar 1', value: 'avatar-1', }, { label: 'Avatar 2', value: 'avatar-2', }, ], }; export default { title: 'Filter bar/Select avatar filter', component: BentoSelectAvatarFilter, argTypes: { // Slots field: { name: 'field', type: { name: 'string', required: false }, description: 'Filter name/key referencing the actual property name in the data set. Should be unique per filter bar.', control: 'text', table: { type: { summary: 'string' }, }, }, label: { name: 'label', type: { name: 'string', required: false }, description: 'Label text to be displayed', control: 'text', table: { type: { summary: 'string' }, }, }, value: { name: 'value', type: { name: 'string', required: true }, description: 'Current value of the filter', control: { disable: true }, table: { type: { summary: 'string, number, Array<string, number>' }, }, }, // Events eventName: { name: 'update', defaultValue: '', description: 'Description waht the event is for', table: { type: { summary: '() => void' } }, control: { disable: true }, }, }, }; export const Default = (_args, { argTypes }) => ({ components: { BentoSelectAvatarFilter }, props: Object.keys(argTypes), template: ` <bento-select-avatar-filter v-bind="args" :value="valueRef" @update="onSelected"/> `, setup(props) { const valueRef = ref(); const onSelected = selectedValue => { valueRef.value = selectedValue.value; action('update')(selectedValue); }; return { // Values args: isVue2 ? props : _args, valueRef, // Events onSelected, }; }, }); Default.args = { field: 'Avatar', label: 'Avatar', options: OPTIONS, };
@@ -1 +1 @@
1
- import { ref } from 'vue'; import { action } from '@storybook/addon-actions'; import { BentoCountryCode } from '@/components/country/country.types'; import { type BentoSelectCountryFilterOptions } from '../../../../filter-bar.types'; import BentoSelectCountryFilter from './select-country-filter.vue'; import { isVue2 } from 'vue-demi'; const OPTIONS: BentoSelectCountryFilterOptions = { listboxItems: [ { label: 'Netherlands', value: BentoCountryCode.NL, }, { label: 'Belgium', value: BentoCountryCode.BE, }, ], variant: 'capital', }; export default { title: 'Filter bar/Select country filter', component: BentoSelectCountryFilter, argTypes: { // Slots field: { name: 'field', type: { name: 'string', required: false }, description: 'Filter name/key referencing the actual property name in the data set. Should be unique per filter bar.', control: 'text', table: { type: { summary: 'string' }, }, }, label: { name: 'label', type: { name: 'string', required: false }, description: 'Label text to be displayed', control: 'text', table: { type: { summary: 'string' }, }, }, value: { name: 'value', type: { name: 'string', required: true }, description: 'Current value of the filter', control: { disable: true }, table: { type: { summary: 'string, number, Array<string, number>' }, }, }, // Events eventName: { name: 'update', defaultValue: '', description: 'Description waht the event is for', table: { type: { summary: '() => void' } }, control: { disable: true }, }, }, }; const Template = (_args, { argTypes }) => ({ components: { BentoSelectCountryFilter }, props: Object.keys(argTypes), template: ` <bento-select-country-filter v-bind="args" :value="valueRef" @update="onSelected"/> `, setup(props) { const valueRef = ref(); const onSelected = selectedValue => { valueRef.value = selectedValue.value; action('update')(selectedValue); }; return { // Values args: isVue2 ? props : _args, valueRef, // Events onSelected, }; }, }); export const SingleSelect = Template.bind({}); SingleSelect.args = { field: 'country', label: 'Country/Region', options: OPTIONS, }; export const MultiSelect = Template.bind({}); MultiSelect.args = { field: 'country', label: 'Country/Region', multiple: true, options: { ...OPTIONS, multiple: true }, };
1
+ import { ref } from 'vue'; import { action } from '@storybook/addon-actions'; import { BentoCountryCode } from '@/components/country/country.types'; import { type BentoSelectCountryFilterOptions } from '../../../../filter-bar.types'; import BentoSelectCountryFilter from './select-country-filter.vue'; import { isVue2 } from 'vue-demi'; const OPTIONS: BentoSelectCountryFilterOptions = { listboxItems: [ { value: BentoCountryCode.NL, }, { value: BentoCountryCode.BE, }, ], variant: 'capital', }; export default { title: 'Filter bar/Select country filter', component: BentoSelectCountryFilter, argTypes: { // Slots field: { name: 'field', type: { name: 'string', required: false }, description: 'Filter name/key referencing the actual property name in the data set. Should be unique per filter bar.', control: 'text', table: { type: { summary: 'string' }, }, }, label: { name: 'label', type: { name: 'string', required: false }, description: 'Label text to be displayed', control: 'text', table: { type: { summary: 'string' }, }, }, value: { name: 'value', type: { name: 'string', required: true }, description: 'Current value of the filter', control: { disable: true }, table: { type: { summary: 'string, number, Array<string, number>' }, }, }, // Events eventName: { name: 'update', defaultValue: '', description: 'Description waht the event is for', table: { type: { summary: '() => void' } }, control: { disable: true }, }, }, }; const Template = (_args, { argTypes }) => ({ components: { BentoSelectCountryFilter }, props: Object.keys(argTypes), template: ` <bento-select-country-filter v-bind="args" :value="valueRef" @update="onSelected"/> `, setup(props) { const valueRef = ref(); const onSelected = selectedValue => { valueRef.value = selectedValue.value; action('update')(selectedValue); }; return { // Values args: isVue2 ? props : _args, valueRef, // Events onSelected, }; }, }); export const SingleSelect = Template.bind({}); SingleSelect.args = { field: 'country', label: 'Country/Region', options: OPTIONS, }; export const MultiSelect = Template.bind({}); MultiSelect.args = { field: 'country', label: 'Country/Region', multiple: true, options: { ...OPTIONS, multiple: true }, };
@@ -1 +1 @@
1
- <template> <bento-select-filter v-slot="{ label: slotLabel }" v-bind="props" :field="field" :label="label" :options="countryOptions" @update="onUpdate" @input="e => $emit(BentoFilterEvent.INPUT, e)" > <bento-typography el="span">{{ slotLabel }}</bento-typography> </bento-select-filter> </template> <script setup lang="ts"> import { computed, type PropType, ref, watch } from 'vue'; import { useCountryItems } from '@/components/dropdown/components/variants/dropdown-country/composables/use-country-items'; import { type BentoListboxSelectedValue } from '@/types/listbox'; import { BentoFilterEvent, type BentoSelectCountryFilterOptions } from '@/components/filter-bar/filter-bar.types'; import { deprecate } from '@/utils/ts/deprecate'; import { BentoTypography } from '@/components/typography'; import BentoSelectFilter from '../../select-filter.vue'; import SelectFilterProps from '@/components/filter-bar/components/select-filter/select-filter.props'; const props = defineProps({ ...SelectFilterProps, /** * List of options that are needed to setup the country filter * Available options: * - multiple: If "true", the user will be able to select multiple items. * - listboxItems: The option elements to populate the filter with {@see BentoSelectCountryFilterOptions} * - variant: Which label variant to display for the option item. */ options: { type: Object as PropType<BentoSelectCountryFilterOptions>, default: undefined, }, }); const emit = defineEmits([BentoFilterEvent.INPUT, BentoFilterEvent.UPDATE]); const variant = ref(props.options.variant); const listboxItems = ref(props.options.listboxItems); watch([() => props.options.variant, () => props.options.listboxItems], () => { variant.value = props.options.variant; listboxItems.value = props.options.listboxItems; }); const onUpdate = (selectedValue: BentoListboxSelectedValue) => emit(BentoFilterEvent.UPDATE, selectedValue); const countryItems = useCountryItems(variant, listboxItems); const countryOptions = computed(() => { return { ...(props?.options ? props.options : {}), listboxItems: countryItems.value, }; }); if (!variant.value || variant.value === 'custom') { deprecate( 'BentoSelectCountryFilter type with `options.variant==="custom"`', 'Use any of the other BentoSelectCountryFilter variants instead OR use BentoSelectFilter type for custom labels', '2.0.0' ); } </script> <script lang="ts"> /** * The Select Country filter displays a listbox of country items * passed in the "options.listboxItems" property. * * @example * import { BentoSelectCountryFilter } from '@adyen/bento-vue2'; * * export default { * components: { BentoSelectCountryFilter }, * template: ` * <bento-select-country-filter * :field="country" * :label="Country" * :options="{ * listboxItems: [{ * value: "NL", * }, { * value: "BE", * }], * variant: 'capital' * }" * :value="BE" * @update="updateFilterHandler" * > * ` * } */ export default { name: 'bento-select-country-filter', }; </script>
1
+ <template> <bento-select-filter v-slot="{ label: slotLabel }" v-bind="props" :field="field" :label="label" :options="countryOptions" @update="onUpdate" @input="e => $emit(BentoFilterEvent.INPUT, e)" > <bento-typography el="span">{{ slotLabel }}</bento-typography> </bento-select-filter> </template> <script setup lang="ts"> import { computed, type PropType, ref, watch } from 'vue'; import { useCountryItems } from '@/components/dropdown/components/variants/dropdown-country/composables/use-country-items'; import { type BentoListboxSelectedValue } from '@/types/listbox'; import { BentoFilterEvent, type BentoSelectCountryFilterOptions } from '@/components/filter-bar/filter-bar.types'; import { BentoCountryVariant } from '@/components/country'; import { BentoTypography } from '@/components/typography'; import BentoSelectFilter from '../../select-filter.vue'; import SelectFilterProps from '@/components/filter-bar/components/select-filter/select-filter.props'; const props = defineProps({ ...SelectFilterProps, /** * List of options that are needed to setup the country filter * Available options: * - multiple: If "true", the user will be able to select multiple items. * - listboxItems: The option elements to populate the filter with {@see BentoSelectCountryFilterOptions} * - variant: Which label variant to display for the option item. */ options: { type: Object as PropType<BentoSelectCountryFilterOptions>, default: undefined, }, }); const emit = defineEmits([BentoFilterEvent.INPUT, BentoFilterEvent.UPDATE]); const variant = ref(props.options?.variant ?? BentoCountryVariant.NAME); const listboxItems = ref(props.options?.listboxItems); watch([() => props.options?.variant, () => props.options?.listboxItems], () => { variant.value = props.options?.variant ?? BentoCountryVariant.NAME; listboxItems.value = props.options?.listboxItems; }); const onUpdate = (selectedValue: BentoListboxSelectedValue) => emit(BentoFilterEvent.UPDATE, selectedValue); const countryItems = useCountryItems(variant, listboxItems); const countryOptions = computed(() => { return { ...(props?.options ? props.options : {}), listboxItems: countryItems.value, }; }); </script> <script lang="ts"> /** * The Select Country filter displays a listbox of country items * passed in the "options.listboxItems" property. * * @example * import { BentoSelectCountryFilter } from '@adyen/bento-vue2'; * * export default { * components: { BentoSelectCountryFilter }, * template: ` * <bento-select-country-filter * :field="country" * :label="Country" * :options="{ * listboxItems: [{ * value: "NL", * }, { * value: "BE", * }], * variant: 'capital' * }" * :value="BE" * @update="updateFilterHandler" * > * ` * } */ export default { name: 'bento-select-country-filter', }; </script>