@adyen/bento-mcp 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (410) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +113 -0
  4. package/dist/assets/components/accordion/accordion.docs.mdx +77 -0
  5. package/dist/assets/components/accordion/accordion.stories.ts +1 -0
  6. package/dist/assets/components/accordion/accordion.vue +1 -0
  7. package/dist/assets/components/accordion/components/accordion-item.stories.ts +1 -0
  8. package/dist/assets/components/accordion/components/accordion-item.vue +1 -0
  9. package/dist/assets/components/action-bar/action-bar.docs.mdx +103 -0
  10. package/dist/assets/components/action-bar/action-bar.stories.ts +1 -0
  11. package/dist/assets/components/action-bar/action-bar.vue +1 -0
  12. package/dist/assets/components/alert/alert.docs.mdx +106 -0
  13. package/dist/assets/components/alert/alert.stories.ts +1 -0
  14. package/dist/assets/components/alert/alert.vue +1 -0
  15. package/dist/assets/components/avatar/avatar.docs.mdx +44 -0
  16. package/dist/assets/components/avatar/avatar.stories.ts +1 -0
  17. package/dist/assets/components/avatar/avatar.vue +1 -0
  18. package/dist/assets/components/avatar/components/avatar-image/avatar-image.vue +1 -0
  19. package/dist/assets/components/button/button.docs.mdx +137 -0
  20. package/dist/assets/components/button/button.stories.ts +1 -0
  21. package/dist/assets/components/button/button.vue +1 -0
  22. package/dist/assets/components/button/components/ai-button/ai-button.docs.mdx +40 -0
  23. package/dist/assets/components/button/components/ai-button/ai-button.stories.ts +1 -0
  24. package/dist/assets/components/button/components/ai-button/ai-button.vue +1 -0
  25. package/dist/assets/components/button/components/button-actions/button-actions.docs.mdx +169 -0
  26. package/dist/assets/components/button/components/button-actions/button-actions.stories.ts +1 -0
  27. package/dist/assets/components/button/components/button-actions/button-actions.vue +1 -0
  28. package/dist/assets/components/button/components/loading-button/loading-button.docs.mdx +106 -0
  29. package/dist/assets/components/button/components/loading-button/loading-button.stories.ts +1 -0
  30. package/dist/assets/components/button/components/loading-button/loading-button.vue +1 -0
  31. package/dist/assets/components/button/components/toggle-button/toggle-button.docs.mdx +81 -0
  32. package/dist/assets/components/button/components/toggle-button/toggle-button.stories.ts +1 -0
  33. package/dist/assets/components/button/components/toggle-button/toggle-button.vue +1 -0
  34. package/dist/assets/components/card/card.docs.mdx +166 -0
  35. package/dist/assets/components/card/card.stories.ts +1 -0
  36. package/dist/assets/components/card/card.vue +1 -0
  37. package/dist/assets/components/checkbox/checkbox.docs.mdx +116 -0
  38. package/dist/assets/components/checkbox/checkbox.stories.ts +1 -0
  39. package/dist/assets/components/checkbox/checkbox.vue +1 -0
  40. package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.docs.mdx +134 -0
  41. package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.stories.ts +1 -0
  42. package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.vue +1 -0
  43. package/dist/assets/components/chip/chip.docs.mdx +51 -0
  44. package/dist/assets/components/chip/chip.stories.ts +1 -0
  45. package/dist/assets/components/chip/chip.vue +1 -0
  46. package/dist/assets/components/click-outside/click-outside.docs.mdx +30 -0
  47. package/dist/assets/components/click-outside/click-outside.stories.ts +1 -0
  48. package/dist/assets/components/code-snippet/code-snippet.docs.mdx +73 -0
  49. package/dist/assets/components/code-snippet/code-snippet.stories.ts +1 -0
  50. package/dist/assets/components/code-snippet/code-snippet.vue +1 -0
  51. package/dist/assets/components/country/country.docs.mdx +61 -0
  52. package/dist/assets/components/country/country.stories.ts +1 -0
  53. package/dist/assets/components/country/country.vue +1 -0
  54. package/dist/assets/components/currency/currency.docs.mdx +38 -0
  55. package/dist/assets/components/currency/currency.stories.ts +1 -0
  56. package/dist/assets/components/currency/currency.vue +1 -0
  57. package/dist/assets/components/dashed-underline/dashed-underline.docs.mdx +57 -0
  58. package/dist/assets/components/dashed-underline/dashed-underline.stories.ts +1 -0
  59. package/dist/assets/components/dashed-underline/dashed-underline.vue +1 -0
  60. package/dist/assets/components/data-grid/components/data-grid-cell/data-grid-cell.vue +1 -0
  61. package/dist/assets/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.vue +1 -0
  62. package/dist/assets/components/data-grid/components/data-grid-columns/data-grid-columns.vue +1 -0
  63. package/dist/assets/components/data-grid/components/data-grid-config-settings/components/data-grid-config-settings-column-row/data-grid-config-settings-column-row.vue +1 -0
  64. package/dist/assets/components/data-grid/components/data-grid-config-settings/data-grid-config-settings.vue +1 -0
  65. package/dist/assets/components/data-grid/components/data-grid-lazy-load/data-grid-lazy-load.vue +1 -0
  66. package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.vue +1 -0
  67. package/dist/assets/components/data-grid/components/data-grid-static-icon/data-grid-static-icon.docs.mdx +61 -0
  68. package/dist/assets/components/data-grid/components/data-grid-static-icon/data-grid-static-icon.stories.ts +1 -0
  69. package/dist/assets/components/data-grid/components/data-grid-static-icon/data-grid-static-icon.vue +1 -0
  70. package/dist/assets/components/data-grid/components/data-grid-summary-cell/data-grid-summary-cell.docs.mdx +56 -0
  71. package/dist/assets/components/data-grid/components/data-grid-summary-cell/data-grid-summary-cell.stories.ts +1 -0
  72. package/dist/assets/components/data-grid/components/data-grid-summary-cell/data-grid-summary-cell.vue +1 -0
  73. package/dist/assets/components/data-grid/components/data-grid-trend-cell/data-grid-trend-cell.docs.mdx +48 -0
  74. package/dist/assets/components/data-grid/components/data-grid-trend-cell/data-grid-trend-cell.stories.ts +1 -0
  75. package/dist/assets/components/data-grid/components/data-grid-trend-cell/data-grid-trend-cell.vue +1 -0
  76. package/dist/assets/components/data-grid/data-grid.docs.mdx +951 -0
  77. package/dist/assets/components/data-grid/data-grid.stories.ts +1 -0
  78. package/dist/assets/components/data-grid/data-grid.vue +1 -0
  79. package/dist/assets/components/date/date.docs.mdx +49 -0
  80. package/dist/assets/components/date/date.stories.ts +1 -0
  81. package/dist/assets/components/date/date.vue +1 -0
  82. package/dist/assets/components/date-picker/date-picker.docs.mdx +69 -0
  83. package/dist/assets/components/date-picker/date-picker.stories.ts +1 -0
  84. package/dist/assets/components/date-picker/date-picker.vue +1 -0
  85. package/dist/assets/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.vue +1 -0
  86. package/dist/assets/components/date-range-picker/date-range-picker.docs.mdx +168 -0
  87. package/dist/assets/components/date-range-picker/date-range-picker.stories.ts +1 -0
  88. package/dist/assets/components/date-range-picker/date-range-picker.vue +1 -0
  89. package/dist/assets/components/divider/divider.docs.mdx +27 -0
  90. package/dist/assets/components/divider/divider.stories.ts +1 -0
  91. package/dist/assets/components/divider/divider.vue +1 -0
  92. package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue +1 -0
  93. package/dist/assets/components/dropdown/components/dropdown-default-textbox/dropdown-default-textbox.vue +1 -0
  94. package/dist/assets/components/dropdown/components/dropdown-options-container/dropdown-options-container.vue +1 -0
  95. package/dist/assets/components/dropdown/components/dropdown-small-textbox/dropdown-small-textbox.vue +1 -0
  96. package/dist/assets/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.stories.ts +1 -0
  97. package/dist/assets/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.vue +1 -0
  98. package/dist/assets/components/dropdown/components/variants/dropdown-country/dropdown-country.stories.ts +1 -0
  99. package/dist/assets/components/dropdown/components/variants/dropdown-country/dropdown-country.vue +1 -0
  100. package/dist/assets/components/dropdown/components/variants/dropdown-currency/dropdown-currency.stories.ts +1 -0
  101. package/dist/assets/components/dropdown/components/variants/dropdown-currency/dropdown-currency.vue +1 -0
  102. package/dist/assets/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.stories.ts +1 -0
  103. package/dist/assets/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.vue +1 -0
  104. package/dist/assets/components/dropdown/components/variants/dropdown-tag/dropdown-tag.stories.ts +1 -0
  105. package/dist/assets/components/dropdown/components/variants/dropdown-tag/dropdown-tag.vue +1 -0
  106. package/dist/assets/components/dropdown/dropdown.docs.mdx +420 -0
  107. package/dist/assets/components/dropdown/dropdown.stories.ts +1 -0
  108. package/dist/assets/components/dropdown/dropdown.vue +1 -0
  109. package/dist/assets/components/empty-state/empty-state.docs.mdx +115 -0
  110. package/dist/assets/components/empty-state/empty-state.stories.ts +1 -0
  111. package/dist/assets/components/empty-state/empty-state.vue +1 -0
  112. package/dist/assets/components/file-uploader/components/file-uploader-file-card/components/file-uploader-file-card-animation/file-uploader-file-card-animation.vue +1 -0
  113. package/dist/assets/components/file-uploader/components/file-uploader-file-card/file-uploader-file-card.stories.ts +1 -0
  114. package/dist/assets/components/file-uploader/components/file-uploader-file-card/file-uploader-file-card.vue +1 -0
  115. package/dist/assets/components/file-uploader/components/file-uploader-restrictions/file-uploader-restrictions.stories.ts +1 -0
  116. package/dist/assets/components/file-uploader/components/file-uploader-restrictions/file-uploader-restrictions.vue +1 -0
  117. package/dist/assets/components/file-uploader/file-uploader.docs.mdx +117 -0
  118. package/dist/assets/components/file-uploader/file-uploader.stories.ts +1 -0
  119. package/dist/assets/components/file-uploader/file-uploader.vue +1 -0
  120. package/dist/assets/components/filter-bar/components/all-filters-modal/all-filters-modal.stories.ts +1 -0
  121. package/dist/assets/components/filter-bar/components/all-filters-modal/all-filters-modal.vue +1 -0
  122. package/dist/assets/components/filter-bar/components/all-filters-modal/components/all-filters-modal-button/all-filters-modal-button.vue +1 -0
  123. package/dist/assets/components/filter-bar/components/base-filter/base-filter.docs.mdx +90 -0
  124. package/dist/assets/components/filter-bar/components/base-filter/base-filter.stories.ts +1 -0
  125. package/dist/assets/components/filter-bar/components/base-filter/base-filter.vue +1 -0
  126. package/dist/assets/components/filter-bar/components/base-filter-button/base-filter-button.vue +1 -0
  127. package/dist/assets/components/filter-bar/components/boolean-filter/boolean-filter.vue +1 -0
  128. package/dist/assets/components/filter-bar/components/checkbox-group-filter/checkbox-group-filter.stories.ts +1 -0
  129. package/dist/assets/components/filter-bar/components/checkbox-group-filter/checkbox-group-filter.vue +1 -0
  130. package/dist/assets/components/filter-bar/components/date-filter/date-filter.stories.ts +1 -0
  131. package/dist/assets/components/filter-bar/components/date-filter/date-filter.vue +1 -0
  132. package/dist/assets/components/filter-bar/components/date-range-filter/date-range-filter.stories.ts +1 -0
  133. package/dist/assets/components/filter-bar/components/date-range-filter/date-range-filter.vue +1 -0
  134. package/dist/assets/components/filter-bar/components/filter-bar-button/filter-bar-button.vue +1 -0
  135. package/dist/assets/components/filter-bar/components/filter-bar-button-group/filter-bar-button-group.vue +1 -0
  136. package/dist/assets/components/filter-bar/components/input-filter/input-filter.docs.mdx +18 -0
  137. package/dist/assets/components/filter-bar/components/input-filter/input-filter.stories.ts +1 -0
  138. package/dist/assets/components/filter-bar/components/input-filter/input-filter.vue +1 -0
  139. package/dist/assets/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.docs.mdx +18 -0
  140. package/dist/assets/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.stories.ts +1 -0
  141. package/dist/assets/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.vue +1 -0
  142. package/dist/assets/components/filter-bar/components/radio-group-filter/radio-group-filter.stories.ts +1 -0
  143. package/dist/assets/components/filter-bar/components/radio-group-filter/radio-group-filter.vue +1 -0
  144. package/dist/assets/components/filter-bar/components/range-filter/range-filter.vue +1 -0
  145. package/dist/assets/components/filter-bar/components/select-filter/components/select-filter-button/select-filter-button.vue +1 -0
  146. package/dist/assets/components/filter-bar/components/select-filter/components/select-filter-dropdown/select-filter-dropdown.vue +1 -0
  147. package/dist/assets/components/filter-bar/components/select-filter/select-filter.stories.ts +1 -0
  148. package/dist/assets/components/filter-bar/components/select-filter/select-filter.vue +1 -0
  149. package/dist/assets/components/filter-bar/components/select-filter/variants/select-avatar-filter/select-avatar-filter.stories.ts +1 -0
  150. package/dist/assets/components/filter-bar/components/select-filter/variants/select-avatar-filter/select-avatar-filter.vue +1 -0
  151. package/dist/assets/components/filter-bar/components/select-filter/variants/select-country-filter/select-country-filter.stories.ts +1 -0
  152. package/dist/assets/components/filter-bar/components/select-filter/variants/select-country-filter/select-country-filter.vue +1 -0
  153. package/dist/assets/components/filter-bar/components/select-filter/variants/select-currency-filter/select-currency-filter.stories.ts +1 -0
  154. package/dist/assets/components/filter-bar/components/select-filter/variants/select-currency-filter/select-currency-filter.vue +1 -0
  155. package/dist/assets/components/filter-bar/components/select-filter/variants/select-payment-method-filter/select-payment-method-filter.stories.ts +1 -0
  156. package/dist/assets/components/filter-bar/components/select-filter/variants/select-payment-method-filter/select-payment-method-filter.vue +1 -0
  157. package/dist/assets/components/filter-bar/components/select-filter/variants/select-tag-filter/select-tag-filter.stories.ts +1 -0
  158. package/dist/assets/components/filter-bar/components/select-filter/variants/select-tag-filter/select-tag-filter.vue +1 -0
  159. package/dist/assets/components/filter-bar/components/toggle-filter/toggle-filter.stories.ts +1 -0
  160. package/dist/assets/components/filter-bar/components/toggle-filter/toggle-filter.vue +1 -0
  161. package/dist/assets/components/filter-bar/filter-bar.docs.mdx +1005 -0
  162. package/dist/assets/components/filter-bar/filter-bar.stories.ts +1 -0
  163. package/dist/assets/components/filter-bar/filter-bar.vue +1 -0
  164. package/dist/assets/components/focus-trap/focus-trap.docs.mdx +10 -0
  165. package/dist/assets/components/focus-trap/focus-trap.stories.ts +1 -0
  166. package/dist/assets/components/focus-trap/focus-trap.vue +1 -0
  167. package/dist/assets/components/form-layout/components/form-layout-field-tip/form-layout-field-tip.vue +1 -0
  168. package/dist/assets/components/form-layout/components/form-layout-group/form-layout-group.vue +1 -0
  169. package/dist/assets/components/form-layout/components/form-layout-title/form-layout-title.vue +1 -0
  170. package/dist/assets/components/form-layout/form-layout.docs.mdx +452 -0
  171. package/dist/assets/components/form-layout/form-layout.stories.ts +1 -0
  172. package/dist/assets/components/form-layout/form-layout.vue +1 -0
  173. package/dist/assets/components/header/header.docs.mdx +85 -0
  174. package/dist/assets/components/header/header.stories.ts +1 -0
  175. package/dist/assets/components/header/header.vue +1 -0
  176. package/dist/assets/components/header-with-views/components/header-with-views-all-views/header-with-views-all-views.vue +1 -0
  177. package/dist/assets/components/header-with-views/components/header-with-views-item-edit-form/header-with-views-edit-form.vue +1 -0
  178. package/dist/assets/components/header-with-views/header-with-views.docs.mdx +103 -0
  179. package/dist/assets/components/header-with-views/header-with-views.stories.ts +1 -0
  180. package/dist/assets/components/header-with-views/header-with-views.vue +1 -0
  181. package/dist/assets/components/image/image.docs.mdx +34 -0
  182. package/dist/assets/components/image/image.stories.ts +1 -0
  183. package/dist/assets/components/image/image.vue +1 -0
  184. package/dist/assets/components/info-icon/info-icon.docs.mdx +57 -0
  185. package/dist/assets/components/info-icon/info-icon.stories.ts +1 -0
  186. package/dist/assets/components/info-icon/info-icon.vue +1 -0
  187. package/dist/assets/components/input-field/input-field.docs.mdx +141 -0
  188. package/dist/assets/components/input-field/input-field.stories.ts +1 -0
  189. package/dist/assets/components/input-field/input-field.vue +1 -0
  190. package/dist/assets/components/input-field-password/input-field-password.docs.mdx +292 -0
  191. package/dist/assets/components/input-field-password/input-field-password.stories.ts +1 -0
  192. package/dist/assets/components/input-field-password/input-field-password.vue +1 -0
  193. package/dist/assets/components/input-field-phone-number/input-field-phone-number.docs.mdx +72 -0
  194. package/dist/assets/components/input-field-phone-number/input-field-phone-number.stories.ts +1 -0
  195. package/dist/assets/components/input-field-phone-number/input-field-phone-number.vue +1 -0
  196. package/dist/assets/components/inspector/inspector.docs.mdx +118 -0
  197. package/dist/assets/components/inspector/inspector.stories.ts +1 -0
  198. package/dist/assets/components/inspector/inspector.vue +1 -0
  199. package/dist/assets/components/internal/base-button/base-button.vue +1 -0
  200. package/dist/assets/components/internal/button-actions-with-menu/button-actions-with-menu.stories.ts +1 -0
  201. package/dist/assets/components/internal/button-actions-with-menu/button-actions-with-menu.vue +1 -0
  202. package/dist/assets/components/internal/calendar/calendar.stories.ts +1 -0
  203. package/dist/assets/components/internal/calendar/calendar.vue +1 -0
  204. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.stories.ts +1 -0
  205. package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.vue +1 -0
  206. package/dist/assets/components/internal/calendar/components/calendar-month/components/calendar-month-day/__test__/calendar-month-day-stub.vue +1 -0
  207. package/dist/assets/components/internal/calendar/components/calendar-month/components/calendar-month-day/calendar-month-day.stories.ts +1 -0
  208. package/dist/assets/components/internal/calendar/components/calendar-month/components/calendar-month-day/calendar-month-day.vue +1 -0
  209. package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.vue +1 -0
  210. package/dist/assets/components/internal/calendar/components/calendar-year/components/calendar-year-month/__test__/calendar-year-month-stub.vue +1 -0
  211. package/dist/assets/components/internal/calendar/components/calendar-year/components/calendar-year-month/calendar-year-month.vue +1 -0
  212. package/dist/assets/components/internal/checkbox-input/checkbox-input.stories.ts +1 -0
  213. package/dist/assets/components/internal/checkbox-input/checkbox-input.vue +1 -0
  214. package/dist/assets/components/internal/controls-group/controls-group.vue +1 -0
  215. package/dist/assets/components/internal/copy/copy.docs.mdx +43 -0
  216. package/dist/assets/components/internal/copy/copy.stories.ts +1 -0
  217. package/dist/assets/components/internal/copy/copy.vue +1 -0
  218. package/dist/assets/components/internal/decorative-checkbox/decorative-checkbox.vue +1 -0
  219. package/dist/assets/components/internal/dialog-page/dialog-page.vue +1 -0
  220. package/dist/assets/components/internal/error-message/error-message.vue +1 -0
  221. package/dist/assets/components/internal/field-label/field-label.vue +1 -0
  222. package/dist/assets/components/internal/fixed-scroller/fixed-scroller.docs.mdx +21 -0
  223. package/dist/assets/components/internal/fixed-scroller/fixed-scroller.stories.ts +1 -0
  224. package/dist/assets/components/internal/fixed-scroller/fixed-scroller.vue +1 -0
  225. package/dist/assets/components/internal/footer-actions/footer-actions.vue +1 -0
  226. package/dist/assets/components/internal/grid-layout/grid-layout.vue +1 -0
  227. package/dist/assets/components/internal/header-meta/header-meta.vue +1 -0
  228. package/dist/assets/components/internal/info-icon-with-popover/info-icon-with-popover.vue +1 -0
  229. package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-category/listbox-multi-select-category.vue +1 -0
  230. package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/listbox-multi-select-options.vue +1 -0
  231. package/dist/assets/components/internal/listbox/components/listbox-multi-select/listbox-multi-select.vue +1 -0
  232. package/dist/assets/components/internal/listbox/components/listbox-option/listbox-option.vue +1 -0
  233. package/dist/assets/components/internal/listbox/components/listbox-option-tag/listbox-option-tag.vue +1 -0
  234. package/dist/assets/components/internal/listbox/components/listbox-single-select/components/listbox-single-select-category/listbox-single-select-category.vue +1 -0
  235. package/dist/assets/components/internal/listbox/components/listbox-single-select/listbox-single-select.vue +1 -0
  236. package/dist/assets/components/internal/listbox/components/listbox-single-select-option/listbox-single-select-option.vue +1 -0
  237. package/dist/assets/components/internal/listbox/listbox.stories.ts +1 -0
  238. package/dist/assets/components/internal/listbox/listbox.vue +1 -0
  239. package/dist/assets/components/internal/radio-input/radio-input.stories.ts +1 -0
  240. package/dist/assets/components/internal/radio-input/radio-input.vue +1 -0
  241. package/dist/assets/components/internal/teleport/teleport.vue +1 -0
  242. package/dist/assets/components/internal/tooltip/tooltip.test.vue +1 -0
  243. package/dist/assets/components/internal/tooltip/tooltip.vue +1 -0
  244. package/dist/assets/components/layout/components/layout-row/layout-row.stories.ts +1 -0
  245. package/dist/assets/components/layout/components/layout-row/layout-row.vue +1 -0
  246. package/dist/assets/components/layout/layout.docs.mdx +108 -0
  247. package/dist/assets/components/layout/layout.stories.ts +1 -0
  248. package/dist/assets/components/layout/layout.vue +1 -0
  249. package/dist/assets/components/link/link.docs.mdx +119 -0
  250. package/dist/assets/components/link/link.stories.ts +1 -0
  251. package/dist/assets/components/link/link.vue +1 -0
  252. package/dist/assets/components/loading-indicator/loading-indicator.docs.mdx +65 -0
  253. package/dist/assets/components/loading-indicator/loading-indicator.stories.ts +1 -0
  254. package/dist/assets/components/loading-indicator/loading-indicator.vue +1 -0
  255. package/dist/assets/components/menu/components/menu-item/menu-item.vue +1 -0
  256. package/dist/assets/components/menu/components/menu-item-list/menu-item-list.vue +1 -0
  257. package/dist/assets/components/menu/components/menu-list-container/menu-list-container.vue +1 -0
  258. package/dist/assets/components/menu/components/sub-menu-item-list/sub-menu-item-list.vue +1 -0
  259. package/dist/assets/components/menu/menu.docs.mdx +121 -0
  260. package/dist/assets/components/menu/menu.stories.ts +1 -0
  261. package/dist/assets/components/menu/menu.vue +1 -0
  262. package/dist/assets/components/modal/components/base-modal/base-modal.vue +1 -0
  263. package/dist/assets/components/modal/components/modal-default/modal-default.vue +1 -0
  264. package/dist/assets/components/modal/components/modal-dialog/modal-dialog.vue +1 -0
  265. package/dist/assets/components/modal/components/modal-page/modal-page.vue +1 -0
  266. package/dist/assets/components/modal/modal.docs.mdx +215 -0
  267. package/dist/assets/components/modal/modal.stories.ts +1 -0
  268. package/dist/assets/components/modal/modal.vue +1 -0
  269. package/dist/assets/components/modal-fullscreen/components/modal-fullscreen-page.vue +1 -0
  270. package/dist/assets/components/modal-fullscreen/modal-fullscreen.docs.mdx +217 -0
  271. package/dist/assets/components/modal-fullscreen/modal-fullscreen.stories.ts +1 -0
  272. package/dist/assets/components/modal-fullscreen/modal-fullscreen.vue +1 -0
  273. package/dist/assets/components/pagination/components/pagination-context/pagination-context.vue +1 -0
  274. package/dist/assets/components/pagination/components/pagination-controls/pagination-controls.vue +1 -0
  275. package/dist/assets/components/pagination/components/pagination-results-per-page/pagination-results-per-page.vue +1 -0
  276. package/dist/assets/components/pagination/pagination.docs.mdx +116 -0
  277. package/dist/assets/components/pagination/pagination.stories.ts +1 -0
  278. package/dist/assets/components/pagination/pagination.vue +1 -0
  279. package/dist/assets/components/payment-method/payment-method.docs.mdx +34 -0
  280. package/dist/assets/components/payment-method/payment-method.stories.ts +1 -0
  281. package/dist/assets/components/payment-method/payment-method.vue +1 -0
  282. package/dist/assets/components/popover/components/popover-dismiss-button/popover-dismiss-button.vue +1 -0
  283. package/dist/assets/components/popover/components/popover-image/popover-image.vue +1 -0
  284. package/dist/assets/components/popover/components/popover-title/popover-title.vue +1 -0
  285. package/dist/assets/components/popover/popover.docs.mdx +89 -0
  286. package/dist/assets/components/popover/popover.stories.ts +1 -0
  287. package/dist/assets/components/popover/popover.vue +1 -0
  288. package/dist/assets/components/popper-container/popper-container.docs.mdx +11 -0
  289. package/dist/assets/components/popper-container/popper-container.stories.ts +1 -0
  290. package/dist/assets/components/popper-container/popper-container.vue +1 -0
  291. package/dist/assets/components/promo-banner/promo-banner.docs.mdx +94 -0
  292. package/dist/assets/components/promo-banner/promo-banner.stories.ts +1 -0
  293. package/dist/assets/components/promo-banner/promo-banner.vue +1 -0
  294. package/dist/assets/components/radio-group/components/radio-button/radio-button.docs.mdx +76 -0
  295. package/dist/assets/components/radio-group/components/radio-button/radio-button.stories.ts +1 -0
  296. package/dist/assets/components/radio-group/components/radio-button/radio-button.vue +1 -0
  297. package/dist/assets/components/radio-group/radio-group.docs.mdx +154 -0
  298. package/dist/assets/components/radio-group/radio-group.stories.ts +1 -0
  299. package/dist/assets/components/radio-group/radio-group.vue +1 -0
  300. package/dist/assets/components/rich-text-editor/components/rich-text-editor-content-area/rich-text-editor-content-area.vue +1 -0
  301. package/dist/assets/components/rich-text-editor/components/rich-text-editor-renderer/components/rich-text-editor-code-block/rich-text-editor-code-block.vue +1 -0
  302. package/dist/assets/components/rich-text-editor/components/rich-text-editor-renderer/components/rich-text-editor-inline-code/rich-text-editor-inline-code.vue +1 -0
  303. package/dist/assets/components/rich-text-editor/components/rich-text-editor-renderer/components/rich-text-editor-link/rich-text-editor-link.vue +1 -0
  304. package/dist/assets/components/rich-text-editor/components/rich-text-editor-renderer/components/rich-text-editor-list-item/rich-text-editor-list-item.vue +1 -0
  305. package/dist/assets/components/rich-text-editor/components/rich-text-editor-renderer/components/rich-text-editor-table/rich-text-editor-table.vue +1 -0
  306. package/dist/assets/components/rich-text-editor/rich-text-editor.docs.mdx +52 -0
  307. package/dist/assets/components/rich-text-editor/rich-text-editor.stories.ts +1 -0
  308. package/dist/assets/components/rich-text-editor/rich-text-editor.vue +1 -0
  309. package/dist/assets/components/search-bar/search-bar.docs.mdx +117 -0
  310. package/dist/assets/components/search-bar/search-bar.stories.ts +1 -0
  311. package/dist/assets/components/search-bar/search-bar.vue +1 -0
  312. package/dist/assets/components/secondary-nav/components/secondary-nav-category/secondary-nav-category.vue +1 -0
  313. package/dist/assets/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue +1 -0
  314. package/dist/assets/components/secondary-nav/secondary-nav.docs.mdx +40 -0
  315. package/dist/assets/components/secondary-nav/secondary-nav.stories.ts +1 -0
  316. package/dist/assets/components/secondary-nav/secondary-nav.vue +1 -0
  317. package/dist/assets/components/segmented-control/components/segmented-control-button/segmented-control-button.vue +1 -0
  318. package/dist/assets/components/segmented-control/segmented-control.docs.mdx +90 -0
  319. package/dist/assets/components/segmented-control/segmented-control.stories.ts +1 -0
  320. package/dist/assets/components/segmented-control/segmented-control.vue +1 -0
  321. package/dist/assets/components/selection-card/components/selection-card-group/selection-card-group.docs.mdx +119 -0
  322. package/dist/assets/components/selection-card/components/selection-card-group/selection-card-group.stories.ts +1 -0
  323. package/dist/assets/components/selection-card/components/selection-card-group/selection-card-group.vue +1 -0
  324. package/dist/assets/components/selection-card/selection-card.docs.mdx +108 -0
  325. package/dist/assets/components/selection-card/selection-card.stories.ts +1 -0
  326. package/dist/assets/components/selection-card/selection-card.vue +1 -0
  327. package/dist/assets/components/sidepanel/components/sidepanel-page.stories.ts +1 -0
  328. package/dist/assets/components/sidepanel/components/sidepanel-page.vue +1 -0
  329. package/dist/assets/components/sidepanel/sidepanel.docs.mdx +129 -0
  330. package/dist/assets/components/sidepanel/sidepanel.stories.ts +1 -0
  331. package/dist/assets/components/sidepanel/sidepanel.vue +1 -0
  332. package/dist/assets/components/status/status.docs.mdx +62 -0
  333. package/dist/assets/components/status/status.stories.ts +1 -0
  334. package/dist/assets/components/status/status.vue +1 -0
  335. package/dist/assets/components/stepper/components/step/step.stories.ts +1 -0
  336. package/dist/assets/components/stepper/components/step/step.vue +1 -0
  337. package/dist/assets/components/stepper/stepper.docs.mdx +127 -0
  338. package/dist/assets/components/stepper/stepper.stories.ts +1 -0
  339. package/dist/assets/components/stepper/stepper.vue +1 -0
  340. package/dist/assets/components/structured-list/components/structered-list-item/structured-list-item.stories.ts +1 -0
  341. package/dist/assets/components/structured-list/components/structered-list-item/structured-list-item.vue +1 -0
  342. package/dist/assets/components/structured-list/structured-list.docs.mdx +68 -0
  343. package/dist/assets/components/structured-list/structured-list.stories.ts +1 -0
  344. package/dist/assets/components/structured-list/structured-list.vue +1 -0
  345. package/dist/assets/components/summary-grid/components/summary-grid-item/summary-grid-item.vue +1 -0
  346. package/dist/assets/components/summary-grid/components/summary-grid-item-amount/summary-grid-item-amount.stories.ts +1 -0
  347. package/dist/assets/components/summary-grid/components/summary-grid-item-amount/summary-grid-item-amount.vue +1 -0
  348. package/dist/assets/components/summary-grid/components/summary-grid-item-custom/summary-grid-item-custom.stories.ts +1 -0
  349. package/dist/assets/components/summary-grid/components/summary-grid-item-custom/summary-grid-item-custom.vue +1 -0
  350. package/dist/assets/components/summary-grid/components/summary-grid-item-numeric/summary-grid-item-numeric.stories.ts +1 -0
  351. package/dist/assets/components/summary-grid/components/summary-grid-item-numeric/summary-grid-item-numeric.vue +1 -0
  352. package/dist/assets/components/summary-grid/components/summary-grid-item-percentage/summary-grid-item-percentage.stories.ts +1 -0
  353. package/dist/assets/components/summary-grid/components/summary-grid-item-percentage/summary-grid-item-percentage.vue +1 -0
  354. package/dist/assets/components/summary-grid/components/summary-grid-item-text/summary-grid-item-text.stories.ts +1 -0
  355. package/dist/assets/components/summary-grid/components/summary-grid-item-text/summary-grid-item-text.vue +1 -0
  356. package/dist/assets/components/summary-grid/summary-grid.docs.mdx +152 -0
  357. package/dist/assets/components/summary-grid/summary-grid.stories.ts +1 -0
  358. package/dist/assets/components/summary-grid/summary-grid.vue +1 -0
  359. package/dist/assets/components/tabs/components/tab.vue +1 -0
  360. package/dist/assets/components/tabs/tabs.docs.mdx +98 -0
  361. package/dist/assets/components/tabs/tabs.stories.ts +1 -0
  362. package/dist/assets/components/tabs/tabs.vue +1 -0
  363. package/dist/assets/components/tag/tag.docs.mdx +137 -0
  364. package/dist/assets/components/tag/tag.stories.ts +1 -0
  365. package/dist/assets/components/tag/tag.vue +1 -0
  366. package/dist/assets/components/textarea/textarea.docs.mdx +53 -0
  367. package/dist/assets/components/textarea/textarea.stories.ts +1 -0
  368. package/dist/assets/components/textarea/textarea.vue +1 -0
  369. package/dist/assets/components/timeline/components/timeline-item/timeline-item.stories.ts +1 -0
  370. package/dist/assets/components/timeline/components/timeline-item/timeline-item.vue +1 -0
  371. package/dist/assets/components/timeline/timeline.docs.mdx +131 -0
  372. package/dist/assets/components/timeline/timeline.stories.ts +1 -0
  373. package/dist/assets/components/timeline/timeline.vue +1 -0
  374. package/dist/assets/components/toast/components/toast-item/toast-item.vue +1 -0
  375. package/dist/assets/components/toast/components/toast-shadow-stack/toast-shadow-stack.vue +1 -0
  376. package/dist/assets/components/toast/toast.docs.mdx +151 -0
  377. package/dist/assets/components/toast/toast.stories.ts +1 -0
  378. package/dist/assets/components/toast/toast.vue +1 -0
  379. package/dist/assets/components/toggle/toggle.docs.mdx +81 -0
  380. package/dist/assets/components/toggle/toggle.stories.ts +1 -0
  381. package/dist/assets/components/toggle/toggle.vue +1 -0
  382. package/dist/assets/components/tutorial/components/tutorial-popover/components/tutorial-indicator/tutorial-indicator.vue +1 -0
  383. package/dist/assets/components/tutorial/components/tutorial-popover/tutorial-popover.vue +1 -0
  384. package/dist/assets/components/tutorial/tutorial.docs.mdx +85 -0
  385. package/dist/assets/components/tutorial/tutorial.stories.ts +1 -0
  386. package/dist/assets/components/tutorial/tutorial.vue +1 -0
  387. package/dist/assets/components/typography/typography.docs.mdx +124 -0
  388. package/dist/assets/components/typography/typography.stories.ts +1 -0
  389. package/dist/assets/components/typography/typography.vue +1 -0
  390. package/dist/assets/composables/use-bento-currency/use-bento-currency.docs.mdx +28 -0
  391. package/dist/assets/composables/use-bento-currency/use-bento-currency.stories.ts +1 -0
  392. package/dist/assets/composables/use-click-outside/use-click-outside.docs.mdx +10 -0
  393. package/dist/assets/composables/use-click-outside/use-click-outside.stories.ts +1 -0
  394. package/dist/assets/directives/highlight/highlight.docs.mdx +64 -0
  395. package/dist/assets/directives/highlight/highlight.stories.ts +1 -0
  396. package/dist/assets/directives/keyboard-navigation/keyboard-navigation.docs.mdx +41 -0
  397. package/dist/assets/directives/keyboard-navigation/keyboard-navigation.stories.ts +1 -0
  398. package/dist/assets/directives/tooltip/tooltip.docs.mdx +86 -0
  399. package/dist/assets/directives/tooltip/tooltip.stories.ts +1 -0
  400. package/dist/assets/index.ts +1 -0
  401. package/dist/assets/templates/dashboard-template/components/dashboard-section/dashboard-section.stories.ts +1 -0
  402. package/dist/assets/templates/dashboard-template/components/dashboard-section/dashboard-section.vue +1 -0
  403. package/dist/assets/templates/dashboard-template/dashboard-template.docs.mdx +148 -0
  404. package/dist/assets/templates/dashboard-template/dashboard-template.stories.ts +1 -0
  405. package/dist/assets/templates/dashboard-template/dashboard-template.vue +1 -0
  406. package/dist/assets/templates/data-grid-template/data-grid-template.docs.mdx +117 -0
  407. package/dist/assets/templates/data-grid-template/data-grid-template.stories.ts +1 -0
  408. package/dist/assets/templates/data-grid-template/data-grid-template.vue +1 -0
  409. package/dist/main.js +30031 -0
  410. package/package.json +45 -3
@@ -0,0 +1 @@
1
+ <template> <bento-input-field v-bind="computedProps" ref="inputField" variant="dropdown" :dropdown="dropdown" class="b-input-field-phone-number" :model-value="displayValue" :placeholder="computedPlaceholder" type="tel" @blur="onBlur" @update:model-value="onUpdateModelValue" @keydown="onKeydown" @dropdown-input="onCountryChange" > <template v-if="label">{{ label }}</template> <template v-else><slot /></template> <template #description> <slot name="description">{{ description }}</slot> </template> </bento-input-field> </template> <script setup lang="ts"> import { computed, nextTick, provide, ref, watch } from 'vue'; import { type BentoInputDropdownProps, BentoInputField } from '@/components/input-field'; import { AsYouType, type CountryCode } from 'libphonenumber-js'; import { useI18n } from '@/utils/ts/i18n'; import { appendRegionCode, BentoInputFieldPhoneNumberCountries, getCountriesDropdownItems, getFormattedPhoneCode, getPhoneNumberPlaceholder, handleInputCursor, hasDefaultRegionCode, parsePhoneAndCountry, preventRegionCodeRepetition, stripRegionCode, } from './utils'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { deprecate } from '@/utils/ts/deprecate'; import type { BentoInputFieldPhoneNumberProps } from './input-field-phone-number.types'; import messages from './messages.json'; import { INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY } from '@/components/input-field/input-field.keys'; type MessageSchema = (typeof messages)['en-US']; const { t, locale } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoInputFieldPhoneNumberProps>(), { countries: () => BentoInputFieldPhoneNumberCountries, description: undefined, dynamicFiltering: true, }); /** * Avoid passing `value` to `bento-input-field`. * This component uses `displayValue` instead. */ const computedProps = computed(() => { const { value, ...allProps } = props; return allProps; }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the value changes. The value is the unformatted phone number in E.164 format. * @deprecated from version 2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', value: string): void; /** * Emitted when the value changes. The value is the unformatted phone number in E.164 format. */ (e: 'update:model-value', value: string): void; /** * Emitted when the selected country is updated. */ (e: 'update:selected-country', country: CountryCode): void; /** * Emitted when the phone number's validity changes. * The payload is a boolean indicating whether the number is valid. */ (e: 'input:valid', value: boolean): void; }>(); const inputField = ref(null); const displayValue = ref(''); const internalValue = ref(''); const insertedChar = ref(''); const { emitValue } = useFormFieldEmits<string>(emit); provide(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, 'bento-input-field-phone-number'); const computedPlaceholder = computed(() => { if (props.placeholder) { return props.placeholder; } if (props.selectedCountry) { return getPhoneNumberPlaceholder(props.selectedCountry); } return ''; }); const isDropdownReadonly = computed(() => props.dropdown?.readonly); const countryItems = computed(() => getCountriesDropdownItems(props.countries, locale.value)); const dropdown = computed<BentoInputDropdownProps>(() => ({ items: countryItems.value, modelValue: props.selectedCountry, readonly: isDropdownReadonly.value, dynamicFiltering: props.dynamicFiltering, 'aria-label': t('countrySelectorDropdown'), })); const onBlur = () => emit('blur'); const onUpdateModelValue = async (value: string) => { const inputEl = inputField.value.$refs.inputFieldElement; let formatter: AsYouType; // Check if the input value starts with '+' or '00', indicating it includes a country code. if (value.startsWith('+') || value.startsWith('00')) { // Reset displayValue to ensure changes are always reflected. // For example, if you paste '+1 264 123 4567' multiple times, the display value should always be '1234567'. // Without this, the displayValue will be '1234567' the first time but subsequently stay as '+1 264 123 4567'. displayValue.value = ''; // Normalize the value by replacing '00' with '+'. const normalizedValue = value.startsWith('00') ? `+${value.slice(2)}` : value; // Create a new AsYouType formatter to format the number as it's typed. formatter = new AsYouType(); formatter.input(normalizedValue); // Get the country code from the formatted number. const country = formatter.getCountry(); // Check if the extracted country code is valid and included in the allowed countries. const isValidCountry = country && props.countries.includes(country); // If the dropdown is readonly, determine whether to strip the region code based on the selected country. if (isDropdownReadonly.value) { displayValue.value = props.selectedCountry === country ? stripRegionCode(formatter.getNationalNumber(), country) : value; } else { // Emit an event to update the selected country, if valid. Otherwise, set it to null. emit('update:selected-country', isValidCountry ? country : null); // Strip the region code from the formatted number if the country is valid. displayValue.value = isValidCountry ? stripRegionCode(formatter.getNationalNumber(), country) : value; } const numberValue = formatter.getNumberValue() ?? ''; const formattedPhoneCode = getFormattedPhoneCode(country); const shouldClearInput = isValidCountry && numberValue.startsWith(formattedPhoneCode) && numberValue.length <= formattedPhoneCode.length; internalValue.value = shouldClearInput ? '' : numberValue; emitValue(internalValue.value); } else { // If the input doesn't start with a country code, format the number based on the selected country. formatter = new AsYouType(props.selectedCountry); const formatted = formatter.input(appendRegionCode(value, props.selectedCountry)); // Prevent repetition of the region code and get the unformatted number. const unformattedNumber = preventRegionCodeRepetition(value, formatter.getNumberValue(), props.selectedCountry) ?? ''; // Emit the unformatted number value and update the display value with the stripped region code. internalValue.value = unformattedNumber; emitValue(internalValue.value); displayValue.value = stripRegionCode(formatted, props.selectedCountry); } await nextTick(); // Re-parse the number to ensure correct formatting, e.g., adding a leading '0' for NL numbers if valid and `hasDefaultRegionCode` returns false. if (formatter.isValid() && props.selectedCountry && !hasDefaultRegionCode(props.selectedCountry)) { const { displayNumber, isValid } = parsePhoneAndCountry( displayValue.value?.toString(), props.selectedCountry, props.countries ); displayValue.value = displayNumber; emitValidation(isValid); } else { emitValidation(formatter.isValid()); } handleInputCursor(inputEl, displayValue.value.toString(), insertedChar.value); }; const emitValidation = (isValid: boolean) => { // Numbers shorter than 5 digits are never valid phone numbers. // `libphonenumber-js` rejects numbers under 5 digits (`TOO_SHORT`). // We don’t call that check directly here, but use the same value for consistency. const MIN_PHONE_NUMBER_LENGTH_FOR_VALIDATION = 5; const numberLength = displayValue.value?.toString().replace(/\D/g, '')?.length ?? 0; const isValidNumberLength = numberLength >= MIN_PHONE_NUMBER_LENGTH_FOR_VALIDATION; emit('input:valid', isValidNumberLength && isValid); }; const onKeydown = event => { if (/^[a-zA-Z]$/.test(event.key) && !event.metaKey && !event.ctrlKey) { event.preventDefault(); return; } insertedChar.value = event.key; }; const onCountryChange = (country: CountryCode) => { emit('update:selected-country', country); const isUnformatted = displayValue.value?.toString().startsWith('+'); if (hasDefaultRegionCode(country) && !isUnformatted) { let formatter = new AsYouType(country); const formatted = formatter.input(appendRegionCode(displayValue.value?.toString(), country)); const unformattedNumber = preventRegionCodeRepetition(displayValue.value?.toString(), formatter.getNumberValue(), country) ?? ''; internalValue.value = unformattedNumber; emitValue(internalValue.value); displayValue.value = stripRegionCode(formatted, country); emitValidation(formatter.isValid()); } else { const { displayNumber, unformattedNumber, isValid } = parsePhoneAndCountry( displayValue.value?.toString(), country, props.countries ); internalValue.value = unformattedNumber ?? ''; emitValue(internalValue.value); displayValue.value = displayNumber; emitValidation(isValid); } }; watch( () => [props.value, props.modelValue], () => { const newValue = props.modelValue ?? props.value; if (newValue === internalValue.value) { return; } const { displayNumber, country, isValid } = parsePhoneAndCountry( newValue?.toString(), props.selectedCountry, props.countries ); if (country && country !== props.selectedCountry && !isDropdownReadonly.value) { emit('update:selected-country', country); } displayValue.value = stripRegionCode(displayNumber, country); emitValidation(isValid); }, { immediate: true } ); if (props.value) { deprecate( 'BentoInputFieldPhoneNumber "value" property', `The use of "value" prop in "BentoInputFieldPhoneNumber" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field phone number can be used to display phone numbers that come with a country code. * * @example * import { BentoInputFieldPhoneNumber } from '@adyen/bento-vue2'; * * export default { * components: { BentoInputFieldPhoneNumber }, * template: ` * <bento-input-field-phone-number * label="Phone number" * :countries="['NL', 'BE']" * model-value="0612345678" * selected-country="NL" * /> * ` * } */ export default { i18n: { messages }, model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./input-field-phone-number.scss" />
@@ -0,0 +1,118 @@
1
+ import { Canvas, Meta } from '@storybook/blocks';
2
+ import * as InspectorStories from './inspector.stories';
3
+
4
+ <Meta of={InspectorStories} />
5
+
6
+ # Inspector
7
+
8
+ Inspector allows users to access additional information or details of an object/item. Typically, the Inspector opens
9
+ adjacent to the main content, pushing it aside to maintain visibility and accessibility to both the main content and
10
+ inspector.
11
+
12
+ <Canvas of={InspectorStories.Default} />
13
+
14
+ ## Use Cases
15
+
16
+ Use a Inspector if:
17
+
18
+ - If your users need to view additional data related to an item without navigating away from the main content.
19
+ - When users need persistent interaction between both the main content and the Inspector.
20
+
21
+ ### Do not use
22
+
23
+ - Don't use an inspector when the user has a focussed task and does not need to interact with the main content at the
24
+ same time.
25
+
26
+ ## Best practices
27
+
28
+ - Keep the content within the inspector concise and relevant to the selected object.
29
+ - Avoid Primary actions in the inspector if there is a primary action in the main content!
30
+ - Avoid main footer actions. By nature, the inspector holds detailed information that can have multiple actions related
31
+ to specific content.
32
+
33
+ ### Inspector vs Sidepanel
34
+
35
+ We use Inspectors to displays detailed, contextual information about a specific item or object without navigating away
36
+ from the main content.
37
+
38
+ We use Sidepanel for a focused view to perform actions, change settings or edit features that support the main content
39
+ but do not need the context of the main content.
40
+
41
+ ## Variations
42
+
43
+ ### Size
44
+
45
+ Use `small` (420px) inspectors:
46
+
47
+ - In compact layouts where screen space is limited.
48
+ - To display essential details in a condensed format.
49
+
50
+ Use `large` (595px) inspectors:
51
+
52
+ - To provide a more expansive layout for detailed information.
53
+ - To accommodate rich content such as images and charts.
54
+
55
+ ## Modifiers
56
+
57
+ ### Title
58
+
59
+ The Inspector must have a title. This can be passed using the `title` prop.
60
+
61
+ ### Loading
62
+
63
+ The Inspector should remain open when a user navigates between different items in the main content. The content in the
64
+ inspector will dynamically update based on the selected item. Quick loading is dependent on the backend on the teams
65
+ implementing the inpector.
66
+
67
+ In the event that content takes too long to fetch, we use a spinner to indicate that the content is still loading, which
68
+ can be enabled with the `loading` prop.
69
+
70
+ ### Closing
71
+
72
+ The Inspector can only be collapsed by using the close button.
73
+
74
+ It's important to keep the `is-open` prop in sync, by updating it when `update:is-open` is fired.
75
+
76
+ ### Active page
77
+
78
+ Set an ID for the current active page in the Inspector, by passing `active-page` prop. This could be the same as the
79
+ title for the page, though it needs to be unique per page.
80
+
81
+ `active-page` needs to be updated every time the Inspector window changes and must be kept in sync with
82
+ `update:active-page`.
83
+
84
+ **Important:** this feature is necessary for the correct accessibility behaviour and will be a required prop in
85
+ `Bento 2.0`
86
+
87
+ ## Accessibility
88
+
89
+ Inspector uses the native HTML `<aside>` element, which identifies supplementary content. Aside is labeled by the
90
+ section name and the title of the page, ('Inspector, page title').
91
+
92
+ ### Implementation requirements
93
+
94
+ There are some implementation requirements to make sure that the component's accessibility works:
95
+
96
+ - `active-page` prop must be set and kept in sync with `update:active-page`.
97
+ - Trigger button that opens the inspector must be a stable element on the page that does not disappear while the
98
+ Inspector is open
99
+ - Trigger button must have text or an `aria-label` that states 'Open inspector', and once the Inspector is opened the
100
+ copy should change to 'Move to inspector'
101
+
102
+ Check the inspector's code example for a guideline on how to implement this.
103
+
104
+ ### Keyboard interaction
105
+
106
+ - User can use `Tab` key to move focus to the trigger and use `Spacebar`/`Enter` to open the inspector.
107
+ - Focus moves to the Inspector first interactive element when Inspector is opened.
108
+ - Inspector first interactive element is `Return to main content` button, which allows the user to immediately leave the
109
+ Inspector and return to the main page.
110
+ - The `Tab` should change focus between interactive elements. This behaviour should only exist when the Inspector is
111
+ open.
112
+ - User can use `Ctrl + Shift + S` to move between main page and inspector.
113
+ - When user tabs through all elements of the Inspector, focus returns to the trigger.
114
+ - When user closes the Inspector, focus should return to the trigger and return focus to the trigger.
115
+
116
+ ## Resources
117
+
118
+ - [Figma link](https://www.figma.com/design/uLabwF3243jdMDsNSP7U9I/Bento---Components?node-id=28692-143589&node-type=instance&m=dev)
@@ -0,0 +1 @@
1
+ import { isVue2 } from 'vue-demi'; import { computed, ref, watch } from 'vue'; import { BentoButton } from '@/components/button'; import { BentoDataGrid } from '@/components/data-grid'; import { BentoStructuredList, BentoStructuredListItem } from '@/components/structured-list'; import { BentoTypography } from '@/components/typography'; import { BentoHeader } from '@/components/header'; import PlusIcon from '@adyen/ui-assets-icons-16/vue/plus'; import SearchIcon from '@adyen/ui-assets-icons-16/vue/search'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoInspector from './inspector.vue'; import type { Meta, StoryObj } from '@storybook/vue'; import { BentoInspectorSizeOptions } from './inspector.types'; import './inspector.stories.scss?module'; const meta: Meta = { title: 'Inspector', component: BentoInspector, parameters: { layout: 'fullscreen', }, argTypes: { size: { options: BentoInspectorSizeOptions, control: { type: 'select', }, }, relativeLayout: { options: ['push-content', 'overlay-content'], control: { type: 'select', }, }, }, }; export default meta; type Story = StoryObj<typeof BentoInspector>; const DEFAULT_PROPS = { title: 'Title', size: 'large', isOpen: false, activePage: '', relativeLayout: 'push-content', }; const defaultCode = ` <template> <bento-inspector :is-open="isOpen" @update:is-open="isOpen = $event" :active-page="activePage" @update:active-page="activePage = $event" title="Inspector title" expandable > <template #page> // Your page content <bento-button :aria-expanded="isOpen" @click="openInspector">{{ inspectorButtonTitle }}</bento-button> </template> <template #content> // Your inspector content </template> </bento-inspector> </template> <script setup lang="ts"> import { ref } from 'vue'; import { BentoButton, BentoInspector } from '@adyen/bento-vue2'; const isOpen = ref(false); const activePage = ref('') const inspectorButtonTitle = computed(() => (isOpen.value ? 'Move to inspector' : 'Open inspector')); const openInspector = () => { isOpen.value = true; activePage.value = 'PageId1234'; }; </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoDataGrid, BentoHeader, BentoInspector, BentoStructuredList, BentoStructuredListItem, BentoTypography, SearchIcon, }, props: Object.keys(argTypes), template: ` <bento-inspector v-bind="args" :is-open="isOpen" :title="title" :active-page="activePage" @update:is-open="isOpen = $event" @update:active-page="activePage = $event" expandable > <template #page> <div style="padding:16px"> <bento-header v-bind="headerProps"></bento-header> <bento-data-grid v-bind="gridArgs"> <template #item-paymentMethods="{item}"> <div> {{ item.paymentMethods }} <bento-button variant="tertiary" @click="openInspector(item)" style="float:right" > <template #iconLeft> <search-icon :svg-title="inspectorButtonTitle" /> </template> </bento-button> </div> </template> </bento-data-grid> </div> </template> <template #content> <div style="display: flex; flex-direction: column; height: 100%"> <bento-structured-list style="flex-grow: 1"> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> </bento-structured-list> <div> <bento-button style="float: right">Action</bento-button> </div> </div> </template> </bento-inspector> `, setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(args.isOpen); const title = ref(args.title); const activePage = ref(''); const inspectorButtonTitle = computed(() => (isOpen.value ? 'Move to inspector' : 'Open inspector')); const gridData = [ { id: 1, paymentMethods: 'Visa', amount: '1,234 USD', numeric: '54,213' }, { id: 2, paymentMethods: 'MasterCard', amount: '1,234 USD', numeric: '54,213' }, { id: 3, paymentMethods: 'iDeal', amount: '1,234 USD', numeric: '54,213' }, { id: 4, paymentMethods: 'Klarna', amount: '1,234 USD', numeric: '54,213' }, { id: 5, paymentMethods: 'Apple Pay', amount: '1,234 USD', numeric: '54,213' }, ]; const openInspector = value => { title.value = value.paymentMethods; isOpen.value = true; activePage.value = value.paymentMethods; }; const gridColumns = [ { field: 'paymentMethods', label: 'Payment methods', minWidth: 200, mandatory: true, }, { field: 'amount', label: 'Amount', numeric: true, minWidth: 300 }, { field: 'numeric', label: 'Numeric', numeric: true, minWidth: 300 }, ]; const gridArgs = { data: gridData, columns: gridColumns, pagination: {}, }; const headerProps = { title: 'Payment methods list', actions: [ { title: 'Primary action', event: () => undefined }, { title: 'Secondary action', icon: PlusIcon, event: () => undefined }, { title: 'Tertiary action', icon: PlusIcon, event: () => undefined }, { title: 'Another action', event: () => undefined }, ], lastUpdated: new Date(), }; watch( () => args.isOpen, () => { isOpen.value = args.isOpen; } ); watch( () => args.title, () => { title.value = args.title; } ); watch( () => args.activePage, () => { activePage.value = args.activePage; } ); return { args, isOpen, gridArgs, title, headerProps, activePage, openInspector, inspectorButtonTitle, }; }, }), args: DEFAULT_PROPS, parameters: storybookDocsParameter(defaultCode), };
@@ -0,0 +1 @@
1
+ <template> <div ref="wrapper" class="b-inspector" :class="conditionalClass" @keydown="handleSectionFocus"> <div ref="page" class="b-inspector__page-wrapper"> <div class="b-inspector__page"> <slot name="page" /> </div> </div> <Transition name="b-inspector__animation" mode="out-in" @before-enter="setFocus" @after-leave="setFocus"> <aside v-if="isOpen" ref="inspector" class="b-inspector__container-wrapper" :aria-label="inspectorAriaLabel" @keydown.tab="handleTabNavigation" > <bento-focus-trap :disabled="!isCompact"> <div class="b-inspector__container"> <div class="b-inspector__header"> <template v-if="!isCompact"> <bento-button class="b-inspector__return-content" variant="secondary" condensed :aria-describedby="returnToMainContentDescriptionId" @click="moveFocusBetweenSections" > {{ t('returnToMainContent') }} </bento-button> <span :id="returnToMainContentDescriptionId" class="b-inspector__return-content-description" >{{ t('returnToMainContentDescription') }}</span > </template> <bento-typography el="h2" variant="title" class="b-inspector__title"> {{ title }} </bento-typography> <div class="b-inspector__icons"> <bento-button v-if="expandable" variant="tertiary" @click="emit('expand')"> <template #iconLeft><expand-icon :svg-title="t('expand')"></expand-icon></template> </bento-button> <bento-button variant="tertiary" @click="emit('update:is-open', false)"> <template #iconLeft><cross-icon :svg-title="t('close')"></cross-icon></template> </bento-button> </div> </div> <div class="b-inspector__content"> <bento-loading-indicator v-if="loading" large class="b-inspector__loading-indicator" /> <slot v-else name="content" /> </div> </div> </bento-focus-trap> </aside> </Transition> </div> </template> <script setup lang="ts"> import { computed, nextTick, ref, watch } from 'vue'; import { BentoTypography } from '@/components/typography'; import { BentoButton } from '@/components/button'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoFocusTrap } from '@/components/focus-trap'; import ExpandIcon from '@adyen/ui-assets-icons-16/vue/expand'; import CrossIcon from '@adyen/ui-assets-icons-16/vue/cross'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { deprecate } from '@/utils/ts/deprecate'; import { getNextFocusableElement } from '@/utils/ts/focus-trap.utils'; import { useElementSize } from '@vueuse/core'; import moveFocusInside, { focusInside, getActiveElement } from 'focus-lock'; import { type BentoInspectorProps } from './inspector.types'; import { useFocusHistory } from './composables/use-focus-history/use-focus-history'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoInspectorProps>(), { activePage: '', expandable: false, isOpen: false, loading: false, size: 'small', relativeLayout: 'push-content', pageFocusAnchor: null, }); const emit = defineEmits<{ /** * Emits update to keep isOpen prop in sync */ (e: 'update:is-open', value: boolean): void; /** * Emits update to keep activePage prop in sync */ (e: 'update:active-page', value: string): void; /** * Emits event when user click expand button */ (e: 'expand'): void; }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const returnToMainContentDescriptionId = generateUid('returnToMainContentDescription'); const wrapper = ref<HTMLElement>(null); const inspector = ref<HTMLElement>(null); const page = ref<HTMLElement>(null); const inspectorLastFocusedElement = ref<HTMLElement>(null); const { width: wrapperWidth } = useElementSize(wrapper); const isCompact = computed(() => { const COMPACT_THRESHOLD_SMALL = 740; const COMPACT_THRESHOLD_LARGE = 915; // small inspector to occupy full screen if < 740px // large inspector to occupy full screen if < 915px const threshold = props.size === 'small' ? COMPACT_THRESHOLD_SMALL : COMPACT_THRESHOLD_LARGE; return wrapperWidth.value > 0 && wrapperWidth.value < threshold; }); const computedPageLastFocusedElement = computed(() => props.pageFocusAnchor); const conditionalClass = computed(() => ({ [`b-inspector--${props.size}`]: true, [`b-inspector--${props.relativeLayout}`]: true, [`b-inspector--is-compact`]: isCompact.value, })); const inspectorAriaLabel = computed(() => `${t('inspector')}, ${props.title}`); const { findValidFocusFallback } = useFocusHistory(page); const setFocus = async () => { if (props.isOpen) { await nextTick(); moveFocusInside(inspector.value, computedPageLastFocusedElement.value); } else { // This doesn't work with data grid cell actions, as the button gets destroyed once is not being hovered. // To be reviewed after LevelAccess revision focusElement(computedPageLastFocusedElement.value ?? findValidFocusFallback()); } }; const handleTabNavigation = event => { if (isCompact.value) { return; } const direction = event.shiftKey ? -1 : 1; const focusableEl = getNextFocusableElement(direction, inspector.value); // If the nextfocusableEl is the same as the activeElement that means it's the last focusable element // inside the inspector, so the focus is moved back to the last focused element on the page if (getActiveElement() === focusableEl) { event.preventDefault(); inspectorLastFocusedElement.value = document.activeElement as HTMLElement; focusElement(computedPageLastFocusedElement.value ?? findValidFocusFallback()); // Set active-page as undefined when focus is not on page emit('update:active-page', undefined); } }; const handleSectionFocus = event => { const sectionShortcut = event.shiftKey && event.ctrlKey && event.key === 'S'; if (props.isOpen && !isCompact.value && sectionShortcut) { moveFocusBetweenSections(); } }; const moveFocusBetweenSections = () => { if (focusInside(inspector.value)) { inspectorLastFocusedElement.value = document.activeElement as HTMLElement; focusElement(computedPageLastFocusedElement.value ?? findValidFocusFallback()); // Set active-page as undefined when focus is not on page emit('update:active-page', undefined); } else { focusElement(inspectorLastFocusedElement.value); } }; const focusElement = (element: HTMLElement) => { if (!element) { return; } element?.focus(); if (document.activeElement !== element) { focusElement(element.parentElement); } }; watch( () => props.activePage, () => { if (props.activePage) { setFocus(); } } ); if (!props.activePage) { deprecate( 'BentoInspector "active-page" as optional', `Make sure to set the "active-page" prop and keep it in sync with "update:active-page". "active-page" prop will be required in the next version`, '2.0.0' ); } </script> <script lang="ts"> /** * Inspector allows users to access additional information or details of an object/item. The inspector opens adjacent to the main content, pushing it aside to maintain visibility and accessibility to both the main content and inspector. * * @example * import { BentoInspector } from '@adyen/bento-vue2'; * * export default { * components: { BentoInspector }, * template: ` * <bento-inspector * title="Inspector" * size="large" * :is-open="isOpen" * :active-page="activePage" * expandable * @update:is-open="isOpen = $event" * @update:active-page="activePage = $event" * > * <template #page>Content of the page</template> * <template #content>Content of the inspector</template> * </bento-inspector> * ` * } */ export default { i18n: { messages } }; </script> <style lang="scss" scoped src="./inspector.scss" />
@@ -0,0 +1 @@
1
+ <template> <button ref="bentoBaseButtonRef" v-bind="$attrs" class="b-base-button" :class="conditionalClasses" :disabled="disabled" :type="type" :aria-expanded="ariaAttributes['aria-expanded']" @click="onClickButton" @focus="onFocusButton" @blur="onBlurButton" > <span v-if="hasSlot('iconLeft')" class="b-base-button__icon-left" data-testid="icon-button-left"> <slot name="iconLeft" /> </span> <bento-typography v-if="hasSlot('default')" el="span" variant="body" stronger class="b-base-button__label"> <slot /> </bento-typography> <span v-if="hasSlot('iconRight')" class="b-base-button__icon-right" data-testid="icon-button-right"> <slot name="iconRight" /> </span> </button> </template> <script lang="ts"> import { computed, defineComponent, type PropType, ref } from 'vue'; import { BentoBaseButtonEvent, BentoBaseButtonPropClass } from './base-button.types'; import type { ButtonHTMLAttributes } from 'vue/types/jsx.d.ts'; import { BentoTypography } from '@/components/typography'; import { useHasSlot } from '@/composables'; import { type Booleanish } from '@/types/prop-types'; /** * Base button component to use as a base for all the various types of bento buttons. For internal use only. * * * @example * import { BaseButton, BaseButtonEvent } from '@/components/internal/base-button'; * import YourImportedIcon from @adyen/ui-assets-icons-16/example-icon'; * * export default { * components: { BaseButton, YourImportedIcon }, * props: { * text: * }, * template: ` * <base-button class="your-custom-button-styles" @click="onClick"> * <template #iconLeft><your-imported-icon></template> * Button text * <template #iconRight><your-imported-icon></template> * </base-button> * `, * emits: [BentoButtonEvent.CLICK], * setup: (prps, { emit }) => { * const onClick = value => emit(BaseButtonEvent.CLICK, value); * * return { * onClick, * }; * } * } */ export default defineComponent({ name: 'base-button', components: { BentoTypography }, props: { /** * Disables button functionality */ disabled: { type: Boolean, default: false, }, /** * Displays button as critical */ critical: { type: Boolean, default: false, }, /** * The default behavior of the button. * Possible values are: 'button', 'reset', 'submit' */ type: { type: String as PropType<ButtonHTMLAttributes['type']>, default: 'button', }, }, emits: [ BentoBaseButtonEvent.CLICK, BentoBaseButtonEvent.FOCUS, BentoBaseButtonEvent.BLUR, BentoBaseButtonEvent.KEYDOWN_ESC, ], setup(props, { slots, emit, attrs }) { const ariaAttributes = computed(() => ({ 'aria-expanded': attrs['aria-expanded']?.toString() as Booleanish, })); const bentoBaseButtonRef = ref(null); const conditionalClasses = computed(() => ({ [`b-base-button--${BentoBaseButtonPropClass.DESTRUCTIVE}`]: props.critical, [`b-base-button--${BentoBaseButtonPropClass.ICON_ONLY}`]: !slots.default, })); const onClickButton = (e: MouseEvent) => { if (!props.disabled) { emit(BentoBaseButtonEvent.CLICK, e); } else { e.preventDefault(); e.stopPropagation(); } }; const onFocusButton = (e: FocusEvent) => { emit(BentoBaseButtonEvent.FOCUS, e); }; const onBlurButton = (e: FocusEvent) => { emit(BentoBaseButtonEvent.BLUR, e); }; return { // Values ariaAttributes, conditionalClasses, // Methods onClickButton, onFocusButton, onBlurButton, hasSlot: useHasSlot(slots), // Refs bentoBaseButtonRef, }; }, }); </script> <style lang="scss" scoped src="./base-button.scss" />
@@ -0,0 +1 @@
1
+ import { BentoButtonActionsLayout } from '@/components/button'; import { isVue2 } from 'vue-demi'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { type Meta, type StoryObj } from '@storybook/vue'; import ButtonActionsWithMenu from './button-actions-with-menu.vue'; import ButtonActionsWithMenuDefault from './__tests__/button-actions-with-menu-default.vue?raw'; import MachineLearningIcon from '@adyen/ui-assets-icons-16/vue/machine-learning'; export default { title: 'Button Actions with Menu', component: ButtonActionsWithMenu, argTypes: { layout: { options: Object.values(BentoButtonActionsLayout), mapping: BentoButtonActionsLayout, control: { type: 'select', }, }, }, } as Meta; type Story = StoryObj<typeof ButtonActionsWithMenu>; const DEFAULT_ACTIONS = [ { title: 'Primary', event: () => true, icon: MachineLearningIcon, }, { title: 'Secondary', icon: MachineLearningIcon, data: [ { text: 'Item 1', secondaryText: 'Text', handler: () => true, critical: true, icon: MachineLearningIcon, }, { text: 'Item 2', secondaryText: 'Text', handler: () => true, }, ], }, { title: 'Tertiary', event: () => true, data: [ { text: 'Item 1', secondaryText: 'Text', handler: () => true, critical: true, }, { text: 'Item 2', secondaryText: 'Text', handler: () => true, }, ], }, { title: 'Quarternary', event: () => true, }, ]; const DEFAULT_PROPS = { actions: DEFAULT_ACTIONS, displayedActions: 2, }; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { ButtonActionsWithMenu, MachineLearningIcon }, props: Object.keys(argTypes), template: ` <button-actions-with-menu v-bind="args" /> `, setup(props) { return { args: isVue2 ? props : _args, }; }, }), args: { ...DEFAULT_PROPS }, parameters: storybookDocsParameter(ButtonActionsWithMenuDefault), };
@@ -0,0 +1 @@
1
+ <template> <div class="b-button-actions-with-menu"> <div ref="buttonActionsWithMenu" role="group" class="b-button-actions-with-menu__container-wrapper" :class="conditionalClasses" > <bento-button-actions :actions="computedActions" :inverse="inverse" :layout="actionsLayout" /> <bento-menu v-if="computedMenuActions.length" ref="menu" :data="computedMenuActions" :menu-position="menuPosition" /> </div> </div> </template> <script setup lang="ts"> import { BentoButtonActions } from '@/components/button'; import { type BentoMenuItem } from '@/components/menu/components/menu-item/menu-item.types'; import { MENU_INVERSE_INJECTION_KEY } from '@/components/menu/menu.keys'; import BentoMenu from '@/components/menu/menu.vue'; import { computed, provide, ref, toRef } from 'vue'; import { BUTTON_ACTIONS_WITH_MENU_CONTAINER_INJECTION_KEY } from './button-actions-with-menu.keys'; import { type ButtonActionsWithMenuProps } from './button-actions-with-menu.types'; import { useResponsiveActions } from './composables/use-responsive-actions'; const props = withDefaults(defineProps<ButtonActionsWithMenuProps>(), { actionsLayout: 'buttons-end', disableResponsiveBehavior: false, displayedActions: undefined, inverse: false, hideDisplayedActionsBreakpoint: 0, menuPosition: 'bottom-start', }); const inverseRef = toRef(props, 'inverse'); const buttonActionsWithMenu = ref<HTMLDivElement>(null); const menu = ref(null); const stackButtons = ref(false); const computedActions = computed(() => { const displayedActionsLimit = props.hideDisplayedActionsBreakpoint ? displayedActionsCount.value : props.displayedActions; return props.actions.slice(0, displayedActionsLimit); }); const computedMenuRef = computed(() => menu?.value?.$el); const { displayedActionsCount } = useResponsiveActions({ breakpoint: props.hideDisplayedActionsBreakpoint, container: buttonActionsWithMenu, defaultActionsCount: toRef(props, 'displayedActions'), offset: computedMenuRef, }); provide(MENU_INVERSE_INJECTION_KEY, inverseRef); if (!props.disableResponsiveBehavior) { provide(BUTTON_ACTIONS_WITH_MENU_CONTAINER_INJECTION_KEY, { buttonActionsWithMenu, menu, stackButtons }); } const conditionalClasses = computed(() => ({ [`b-button-actions-with-menu__container-wrapper--buttons-end`]: props.actionsLayout === 'buttons-end', [`b-button-actions-with-menu__container-wrapper--buttons-start`]: props.actionsLayout === 'buttons-start', [`b-button-actions-with-menu__container-wrapper--stacked-buttons`]: stackButtons.value, })); const computedMenuActions = computed(() => { if (displayedActionsCount.value === undefined || displayedActionsCount.value >= props.actions.length) { return []; } const actionsToProcess = props.actions.slice(displayedActionsCount.value, props.actions.length); return actionsToProcess.flatMap<BentoMenuItem>(action => { if (!action.data) { return { text: action.title, handler: () => action.event?.({} as Event), icon: action.icon, critical: action.critical, disabled: action.disabled, }; } return action.data; }); }); </script> <script lang="ts"> /** * An internal button actions component with the option to display some of the actions in a menu. * * @example * <button-actions-with-menu * :actions="[ * { * title: 'Primary', * event: () => { ... }, * icon: FourPeopleIcon, * link: { * variant: 'default', * external: true, * to: 'https://www.adyen.com/' * }, * }, * { * title: 'Secondary', * event: () => { ... } * disabled: true, * variant: BentoButtonVariant.PRIMARY, * critical: true * }]" * :displayedActions="2" * /> */ export default {}; </script> <style lang="scss" scoped src="./button-actions-with-menu.scss" />
@@ -0,0 +1 @@
1
+ import { type Meta, type StoryObj } from '@storybook/vue'; import { action } from '@storybook/addon-actions'; import { computed, reactive, ref } from 'vue'; import { CalendarEvent } from './calendar.types'; import { isVue2 } from 'vue-demi'; import Calendar from './calendar.vue'; import { subDays } from 'date-fns/subDays'; import { subMonths } from 'date-fns/subMonths'; import { addMonths } from 'date-fns/addMonths'; const meta: Meta = { title: 'Calendar', component: Calendar, argTypes: { // Props month: { name: 'month', defaultValue: undefined, description: 'Month of the year to be displayed in the calendar', control: 'date', }, value: { name: 'value', defaultValue: undefined, description: 'Date selected', control: { disable: true, }, }, // Funcs isDateDisabled: { name: 'isDateDisabled', defaultValue: '', description: 'Disables the dates that meet the criteria defined in the function.', table: { type: { summary: '(currentDate: Date) => boolean' } }, control: { disable: true }, }, // Events input: { name: 'input', defaultValue: '', description: 'Triggered when a date is selected', table: { type: { summary: '(selectedDate: Date) => void' } }, control: { disable: true }, }, }, }; export default meta; type Story = StoryObj<typeof Calendar>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { Calendar }, props: Object.keys(argTypes), template: ` <calendar v-bind="args" :value="currentValue" @input="onInputEvent" /> `, setup(props) { const args = isVue2 ? props : _args; const currentMonth = computed(() => new Date(args.month)); const currentValue = ref(new Date(args.value)); const onInputEvent = value => { action('input')(value); currentValue.value = value; }; return { // Values args, currentMonth, currentValue, // Events onInputEvent, }; }, }), args: { isRange: false, month: new Date(), value: (() => { const date = new Date(); date.setDate(date.getDate() + 1); return date; })(), numberOfMonths: 1, }, }; export const MonthVariant: Story = { render: (_args, { argTypes }) => ({ components: { Calendar }, props: Object.keys(argTypes), template: ` <calendar v-bind="args" variant="month" :value="currentValue" @input="onInputEvent" /> `, setup(props) { const args = isVue2 ? props : _args; const currentValue = ref(new Date(args.value)); const onInputEvent = value => { action('input')(value); currentValue.value = value; }; return { // Values args, currentValue, // Events onInputEvent, }; }, }), args: { isRange: false, year: new Date(), value: subMonths(new Date(), 1), max: addMonths(new Date(), 1), numberOfMonths: 1, }, }; export const Range: Story = { render: (_args, { argTypes }) => ({ components: { Calendar }, props: Object.keys(argTypes), template: ` <calendar v-bind="args" :value="currentValue" @input="onInputEvent" /> `, setup(props) { const args = isVue2 ? props : _args; const currentMonth = computed(() => new Date(args.month)); const currentValue = reactive({ startDate: null, endDate: null }); const onInputEvent = value => { action(CalendarEvent.INPUT)(value); currentValue.startDate = value.startDate; currentValue.endDate = value.endDate; }; return { // Values args, currentMonth, currentValue, // Events onInputEvent, }; }, }), args: { isRange: true, month: new Date(), min: subDays(new Date(), 8), max: new Date(), value: { startDate: undefined, endDate: undefined, }, }, };
@@ -0,0 +1 @@
1
+ <template> <div class="b-calendar" :class="conditionalClasses"> <component :is="calendarComponent" v-for="(_, index) in numberOfMonths" :ref="numberOfMonths === 1 ? 'calendarRef' : null" :key="`calendar-${index}`" :first-day-of-week="firstDayOfWeek" :is-date-disabled="isDateDisabled" :month="calculateOffsetMonth(index)" :year="calculateOffsetYear(index)" :position="calendarPosition(index)" :min="min" :max="max" :is-range="isRange" :start-date="internalValue.startDate" :end-date="internalValue.endDate" :granularity="granularity" @input="onDateInput" @mouseover="onMouseOver" @changeDay="onMouseOver" @nextMonth="calculateNextMonths" @previousMonth="calculatePreviousMonths" /> </div> </template> <script lang="ts"> import { computed, defineComponent, type PropType, ref, toRef, watch } from 'vue'; import CalendarMonth from './components/calendar-month/calendar-month.vue'; import CalendarYear from './components/calendar-year/calendar-year.vue'; import { CalendarMonthFirstDayOfWeek } from './components/calendar-month'; import type { CalendarMonthIsDateDisabled } from './components/calendar-month'; import { CalendarEvent, type CalendarGranularityConfig, CalendarPosition, type CalendarRangeDateValue, type CalendarValue, CalendarVariant, } from './calendar.types'; import { useSinglePane } from './composables/use-single-pane'; import { useRangePane } from './composables/use-range-pane'; import { addMonths } from 'date-fns/addMonths'; import { subMonths } from 'date-fns/subMonths'; import { addYears } from 'date-fns/addYears'; import { subYears } from 'date-fns/subYears'; /** * Handles the calendar logic to change visible months for Single calendar * and Range calendar, as well as the selection logic for range. * All the processing necessary for the calendar to select dates * * @example * import { Calendar } from '@/internal/bento-vue2'; * * export default { * components: { Calendar }, * template: ` * <calendar * :value="value" * :first-day-of-week="CalendarFirstDayOfWeek.Monday" * :is-date-disabled="(day: Date) => boolean" * :number-of-months="2" * is-range * @input="(selectedDate) => void" * @start-date-selected="(startDate: Date) => void" * @end-date-selected="(endDate: Date) => void" * /> * ` * } */ export default defineComponent({ name: 'calendar', components: { CalendarMonth, CalendarYear }, props: { /** * Allows you to set the first day of the week. 0 is sunday, 1 is monday, 6 is saturday */ firstDayOfWeek: { type: Number as PropType<CalendarMonthFirstDayOfWeek>, default: CalendarMonthFirstDayOfWeek.MONDAY, validator: (value: CalendarMonthFirstDayOfWeek) => Object.values(CalendarMonthFirstDayOfWeek).includes(value), }, /** * The selected granularity configuration */ granularity: { type: Object as PropType<CalendarGranularityConfig>, default: null, }, /** * Indicate if a date should be disabled or not */ isDateDisabled: { type: Function as PropType<CalendarMonthIsDateDisabled>, default: undefined }, /** * Enables the Date Range calendar */ isRange: { type: Boolean, default: false }, /** * Set a maximum number of dates to be selectable by the range. */ maxRange: { type: Number, default: null, validator: (value: number) => !value || value > 0 }, /** * Sets a minimum limit for selectable dates - dates before `min` will not be selectable */ min: { type: Date, default: null }, /** * Sets a maximum limit for selectable dates - dates after `max` will not be selectable */ max: { type: Date, default: null }, /** * Number of months rendered on pane */ numberOfMonths: { type: Number, default: 3, validator: (n: number) => n >= 1, }, /** * Upon opening date range picker, end date's month will be pre-selected. */ showEndDateOnOpen: { type: Boolean, default: false }, /** * Selected date. Can be a single Date or a CalendarRangeDate * instance that contains startDate and endDate attributes. */ value: { type: [Date, Object] as PropType<CalendarValue>, default: undefined }, /** * Default start date for the calendar pane */ defaultDisplayMonth: { type: String, default: null, validator: (value: string) => { // Checks for YYYY-MM-DD format const dateRegex = /^\d{4}-(0?[1-9]|1[012])(-(0?[1-9]|[12][0-9]|3[01]))?$/; const isValidDate = dateRegex.test(value); return isValidDate; }, }, /** * The type of calendar to display. Defaults to showing days. */ variant: { type: String as PropType<`${CalendarVariant}`>, default: 'day', validator: (value: string) => Object.values(CalendarVariant).includes(value as CalendarVariant), }, }, emit: [CalendarEvent.INPUT, CalendarEvent.START_DATE_SELECTED, CalendarEvent.END_DATE_SELECTED], setup(props, { emit }) { // Single date calendar ref const calendarRef = ref(null); // Use a duplicate of "month" to move back and // forth with the arrows. Maintained in sync with the prop "value" const baseDate = ref(); watch( () => props.value, () => { if (props.isRange) { baseDate.value = props?.showEndDateOnOpen && (props.value as CalendarRangeDateValue)?.endDate ? (props.value as CalendarRangeDateValue)?.endDate : (props.value as CalendarRangeDateValue)?.startDate || new Date(); } else { const defaultDate = props.defaultDisplayMonth && new Date(props.defaultDisplayMonth).getTime() ? new Date(props.defaultDisplayMonth) : new Date(); baseDate.value = (props.value as Date) || defaultDate; } }, { immediate: true, deep: true } ); const calendarComponent = computed(() => props.variant === CalendarVariant.MONTH ? CalendarYear : CalendarMonth ); const conditionalClasses = computed(() => ({ 'b-calendar__range': props.isRange, })); const { internalValue, onDateInput, onMouseOver } = props.isRange ? useRangePane( { selectedDate: toRef(props, 'value'), isDateDisabled: props.isDateDisabled as (date: Date) => boolean, maxRange: toRef(props, 'maxRange'), granularity: toRef(props, 'granularity'), firstDayOfWeek: props.firstDayOfWeek, min: toRef(props, 'min'), max: toRef(props, 'max'), }, emit ) : useSinglePane( { selectedDate: toRef(props, 'value'), firstDayOfWeek: props.firstDayOfWeek, }, emit ); /** * Calculate the current month calendar. * This is used to render/hide the Previous & Next buttons on each calendar. * @param {number} calendarIndex - Number to offset the calendar position */ const calendarPosition = (calendarIndex: number): `${CalendarPosition}` => { if (props.numberOfMonths === 1) { return CalendarPosition.UNIQUE; } if (calendarIndex === 0) { return CalendarPosition.FIRST; } return calendarIndex === props.numberOfMonths - 1 ? CalendarPosition.LAST : CalendarPosition.MIDDLE; }; /** * Calculates in relation to the amount of months defined in "numberOfMonths" * which calendar month should be render starting from the first (leftmost) * rendered calendar. * If showEndDateOnOpen is set to true, the last pane should be the baseDate month i.e. the end date * @param offsetMonth - Amount of months to add after the first month was rendered */ const calculateOffsetMonth = computed(() => (offsetMonth: number) => { const monthOffset = props?.showEndDateOnOpen && props.isRange ? subMonths(baseDate.value, props.numberOfMonths - 1) : baseDate.value; return addMonths(monthOffset, offsetMonth); }); const calculateOffsetYear = computed(() => (offsetYear: number) => { const yearOffset = props?.showEndDateOnOpen ? subYears(baseDate.value, props.numberOfMonths - 1) : baseDate.value; return addYears(yearOffset, offsetYear); }); /** * By default shows the following month * Can skip multiple months by passing an additional param * Used for keyboard nav to be able to skip a year */ const calculateNextMonths = (amountOfMonths = 1) => { baseDate.value = addMonths(baseDate.value, amountOfMonths); }; /** * By default shows the previous month * Can skip multiple months by passing an additional param * Used for keyboard nav to be able to go back a year */ const calculatePreviousMonths = (amountOfMonths = 1) => { baseDate.value = subMonths(baseDate.value, amountOfMonths); }; return { // Refs calendarRef, // Values baseDate, calendarComponent, conditionalClasses, internalValue, // Methods calculateOffsetMonth, calculateOffsetYear, calendarPosition, // Events onDateInput, onMouseOver, calculateNextMonths, calculatePreviousMonths, }; }, }); </script> <style lang="scss" scoped src="./calendar.scss" />
@@ -0,0 +1 @@
1
+ import { type Meta, type StoryObj } from '@storybook/vue'; import { action } from '@storybook/addon-actions'; import { computed, ref } from 'vue'; import { CalendarMonthFirstDayOfWeek } from './calendar-month.types'; import CalendarMonth from './calendar-month.vue'; import { isVue2 } from 'vue-demi'; const meta: Meta = { title: 'Calendar/Month', component: CalendarMonth, argTypes: { // Props month: { name: 'month', defaultValue: undefined, description: 'Month of the year to be displayed in the calendar', control: 'date', }, value: { name: 'value', defaultValue: undefined, description: 'Date selected', control: { disable: true, }, }, // Funcs isDateDisabled: { name: 'isDateDisabled', defaultValue: '', description: 'Disables the dates that meet the criteria defined in the function.', table: { type: { summary: '(currentDate: Date) => boolean' } }, control: { disable: true }, }, // Enum props firstDayOfWeek: { options: Object.keys(CalendarMonthFirstDayOfWeek) .map(key => CalendarMonthFirstDayOfWeek[key]) .filter(value => typeof value !== 'string') as Array<number>, control: { type: 'select', }, }, // Events input: { name: 'input', defaultValue: '', description: 'Triggered when a date is selected', table: { type: { summary: '(selectedDate: Date) => void' } }, control: { disable: true }, }, }, }; export default meta; type Story = StoryObj<typeof CalendarMonth>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { CalendarMonth }, props: Object.keys(argTypes), template: ` <calendar-month v-bind="args" /> `, setup(props) { const currentMonth = computed(() => new Date(props.month)); const currentValue = ref(new Date(props.value)); const onInputEvent = value => { action('input')(value); currentValue.value = value; }; return { args: isVue2 ? props : _args, // Values currentMonth, currentValue, // Events onInputEvent, }; }, }), args: { isRange: true, month: new Date(), startDate: (() => { const date = new Date(); date.setDate(date.getDate() - 6); return date; })(), endDate: (() => { const date = new Date(); date.setDate(date.getDate()); return date; })(), }, };
@@ -0,0 +1 @@
1
+ <template> <div v-bento-keyboard-navigation-directive class="b-calendar-month" v-on="listeners"> <div class="b-calendar-month__month-selector"> <!-- go to previous month --> <div class="b-calendar-month__previous"> <bento-button v-if="shouldShowPreviousMonthButton" variant="tertiary" @click="moveToPreviousMonth($event, true)" > <template #iconLeft> <chevron-left-icon :svg-title="t('previousMonth')" /> </template> </bento-button> </div> <!-- month's name --> <bento-typography :id="monthLabelId" el="h2" variant="title" aria-live="polite"> {{ formattedMonthDate }} </bento-typography> <!-- go to next month --> <div class="b-calendar-month__next"> <bento-button v-if="shouldShowNextMonthButton" variant="tertiary" @click="moveToNextMonth($event, true)" > <template #iconLeft> <chevron-right-icon :svg-title="t('nextMonth')" /> </template> </bento-button> </div> </div> <table class="b-calendar-month__month-container" role="grid" :aria-labelledby="monthLabelId"> <thead> <tr class="b-calendar-month__week-header"> <th v-for="(dayOfWeekFormatted, index) in daysOfWeek" :key="index" class="b-calendar-month__day-of-week" scope="col" > <bento-typography class="b-calendar-month__day-of-week-font" el="span" variant="caption"> {{ dayOfWeekFormatted }} </bento-typography> </th> </tr> </thead> <tbody> <tr v-for="(week, index) in weeksOfMonth" :key="index"> <td v-if="index === 0 && week.length < 7" :colspan="7 - week.length"> </td> <calendar-month-day v-for="(day, dayIndex) in week" :ref="addDayRef(day)" :key="`calendar-day-${dayIndex}`" :day="day" :disabled="computedIsDateDisabled(day)" :selected="isSelectedDate(day)" :variant="dayVariant(day)" @click="onDayClicked" @mouseover="$emit(CalendarMonthEvent.MOUSE_OVER, day)" @keyup.up.native="$emit(CalendarMonthEvent.CHANGE_DAY, day)" @keyup.down.native="$emit(CalendarMonthEvent.CHANGE_DAY, day)" @keyup.left.native="$emit(CalendarMonthEvent.CHANGE_DAY, day)" @keyup.right.native="$emit(CalendarMonthEvent.CHANGE_DAY, day)" /> </tr> <template v-if="weeksOfMonth?.length < 6"> <tr v-for="n in 6 - weeksOfMonth?.length" :key="`calendar month filler row ${n}`"> <td aria-hidden="true" class="b-calendar-month__empty-cell-filler" :colspan="7"></td> </tr> </template> </tbody> </table> </div> </template> <script lang="ts"> import { computed, defineComponent, type PropType, toRef } from 'vue'; import BentoButton from '@/components/button/button.vue'; import { CalendarMonthDay, CalendarMonthDayVariant } from './components/calendar-month-day'; import { BentoTypography } from '@/components/typography'; import { useI18n } from '@/utils/ts/i18n'; import { CalendarMonthEvent, CalendarMonthFirstDayOfWeek, type CalendarMonthIsDateDisabled, } from './calendar-month.types'; import { CalendarPosition } from '@/internal/calendar/calendar.types'; import { generateUid } from '@/core/utils/ts'; import { BentoKeyboardNavigationDirective } from '@/directives'; import { useMoveCalendarDate } from './composables/move-calendar-date'; import ChevronLeftIcon from '@adyen/ui-assets-icons-16/vue/chevron-left'; import ChevronRightIcon from '@adyen/ui-assets-icons-16/vue/chevron-right'; import { setDay } from 'date-fns/setDay'; import { startOfMonth } from 'date-fns/startOfMonth'; import { startOfDay } from 'date-fns/startOfDay'; import { isSameDay } from 'date-fns/isSameDay'; import { isWithinInterval } from 'date-fns/isWithinInterval'; import { getDateTimeFormatter } from '@/utils/ts/format-date'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; /** * CalendarMonth that displays all the days of a given month and allows * to move back and forth the visible month with the Next and Previous buttons * * @example * import { CalendarMonthMonth, CalendarMonthFirstDayOfWeek, CalendarPosition } from '@/internal/calendar'; * * export default { * components: { CalendarMonthMonth }, * template: ` * <calendar * :first-day-of-week="CalendarMonthFirstDayOfWeek.SUNDAY" * :is-date-disabled="(date: Date) => boolean" * :month="new Date()" * :position="CalendarPosition.LAST" * is-range * :start-date="new Date()" * :end-date="new Date()" * @input="(selectedDate: Date) => void" * @mouseover="(event: Event) => void" * @nextMonth="() => void" * @previousMonth="() => void" * /> * ` * } */ export default defineComponent({ i18n: { messages }, name: 'calendar-month', components: { BentoButton, BentoTypography, CalendarMonthDay, ChevronLeftIcon, ChevronRightIcon, }, directives: { BentoKeyboardNavigationDirective }, props: { /** * end of the selected range */ endDate: { type: Date, default: null }, /** * Allows you to set the first day of the week. 0 is sunday, 1 is monday, 6 is saturday */ firstDayOfWeek: { type: Number as PropType<CalendarMonthFirstDayOfWeek>, default: CalendarMonthFirstDayOfWeek.MONDAY, validator: (value: CalendarMonthFirstDayOfWeek) => Object.values(CalendarMonthFirstDayOfWeek).includes(value), }, /** * Indicate if a date should be disabled or not */ isDateDisabled: { type: Function as PropType<CalendarMonthIsDateDisabled>, default: undefined }, /** * Indicates if the calendar is part of a range date picker or a sigle date picker */ isRange: { type: Boolean, default: false }, /** * Sets a minimum limit for selectable dates - dates before `min` will not be selectable */ min: { type: Date, default: null }, /** * Sets a maximum limit for selectable dates - dates after `max` will not be selectable */ max: { type: Date, default: null }, /** * Month of the year to be displayed in the calendar */ month: { type: Date, required: true }, /** * When rendering multiple calendars, this property allows to render the * Next and Previous calendar buttons to the right, left or not at all * depending on the position: Unique (only 1), First, Middle or Last */ position: { type: String as PropType<`${CalendarPosition}`>, default: 'unique', validator: (value: CalendarPosition) => Object.values(CalendarPosition).includes(value), }, /** * start of the selected range */ startDate: { type: Date, default: null }, // can be undefined, eslint complains without default }, emits: [ CalendarMonthEvent.CHANGE_DAY, CalendarMonthEvent.INPUT, CalendarMonthEvent.NEXT_MONTH, CalendarMonthEvent.PREVIOUS_MONTH, CalendarMonthEvent.MOUSE_OVER, ], setup(props, { emit }) { // Date formatter locale options const { locale, t } = useI18n<{ message: MessageSchema }>({ messages }); const monthLabelId = generateUid('bento-date-picker-month-label'); const computedIsDateDisabled = computed( () => (date: Date) => (props.min && date < props.min) || (props.max && date > props.max) || props.isDateDisabled?.(date) ); const { internalDate, baseDate, // Calendar refs addDayRef, // Month navigation showNextMonth, showPreviousMonth, // Day navigation moveToNextDay, moveToPreviousDay, moveToPreviousWeek, moveToNextWeek, moveToFirstDayOfWeek, moveToLastDayOfWeek, moveToPreviousMonth, moveToPreviousYear, moveToNextMonth, moveToNextYear, focusOnDate, } = useMoveCalendarDate( { currentSelectedDate: toRef(props, 'startDate'), month: toRef(props, 'month'), isDateDisabled: computedIsDateDisabled.value, firstDayOfWeek: props.firstDayOfWeek, }, emit ); /** * Default to Monday if the value passed in is not part of the defined Enum */ const fallbackFirstDayOfWeek = computed(() => Object.values(CalendarMonthFirstDayOfWeek).includes(props.firstDayOfWeek) ? props.firstDayOfWeek : CalendarMonthFirstDayOfWeek.MONDAY ); /** * Renders the previous month button if it's Unique or First calendar */ const shouldShowPreviousMonthButton = computed(() => ['first', 'unique'].includes(props.position)); /** * Renders the next month button if it's Unique or Last calendar */ const shouldShowNextMonthButton = computed(() => ['last', 'unique'].includes(props.position)); /** * Formats the month date to the name of the month and the year */ const formattedMonthDate = computed(() => getDateTimeFormatter(locale.value, { month: 'long', year: 'numeric' }).format(baseDate.value) ); /** * Formats all the days of the week to their day name */ const daysOfWeek = computed(() => new Array(7).fill(new Date()).map((date, index) => { const dayOfWeekDate = setDay(date, fallbackFirstDayOfWeek.value + index); return getDateTimeFormatter(locale.value, { weekday: 'short' }).format(dayOfWeekDate); }) ); /** * Calculates all the weeks of the current "props.month" * and adds the respective day number to each column and week */ const weeksOfMonth = computed(() => { const weeks = []; const date = startOfMonth(props.month); let week = weeks[weeks.push([]) - 1]; while (date.getMonth() === props.month.getMonth()) { if (date.getDay() - fallbackFirstDayOfWeek.value === 0 && week.length > 0) { week = weeks[weeks.push([]) - 1]; } week.push(startOfDay(date)); date.setDate(date.getDate() + 1); } return weeks; }); const onDayClicked = (selectedDate: Date) => { emit(CalendarMonthEvent.INPUT, selectedDate); }; /** * Calculates the variation of the days selected. * For single select it's always SINGLE * For ranges: * - Start & End is the same: SINGLE * - Start: BEGINNING * - End: END * - Within the range: MIDDLE * @param {Date} day Current day */ // eslint-disable-next-line consistent-return const dayVariant = (day: Date) => { if (!props.isRange || isSameDay(props.startDate, props.endDate)) { return CalendarMonthDayVariant.SINGLE; } if (props.startDate && isSameDay(props.startDate, day)) { return CalendarMonthDayVariant.BEGINNING; } if (props.endDate && isSameDay(props.endDate, day)) { return CalendarMonthDayVariant.END; } if ( props.startDate && props.endDate && isWithinInterval(day, { start: props.startDate, end: props.endDate }) ) { return CalendarMonthDayVariant.MIDDLE; } }; /** * Checks every date in the calendar to see if it's selected. * @param day Current day in the calendar */ const isSelectedDate = (day: Date) => { // Single selection if (internalDate.value && isSameDay(internalDate.value, day) && !props.endDate) { return true; } // Range return ( props.startDate && props.endDate && isWithinInterval(day, { start: props.startDate, end: props.endDate }) ); }; const confirmNavigationDate = () => { emit(CalendarMonthEvent.INPUT, internalDate); }; const listeners = { 'keydown:arrow-right': moveToNextDay, 'keydown:arrow-left': moveToPreviousDay, 'keydown:arrow-up': moveToPreviousWeek, 'keydown:arrow-down': moveToNextWeek, 'keydown:home': moveToFirstDayOfWeek, 'keydown:end': moveToLastDayOfWeek, 'keydown:page-up': moveToPreviousMonth, 'keydown:shift-page-up': moveToPreviousYear, 'keydown:page-down': moveToNextMonth, 'keydown:shift-page-down': moveToNextYear, }; return { // Refs addDayRef, // Values computedIsDateDisabled, daysOfWeek, weeksOfMonth, formattedMonthDate, shouldShowNextMonthButton, shouldShowPreviousMonthButton, monthLabelId, // Enums CalendarMonthEvent, // Functions isSelectedDate, showNextMonth, showPreviousMonth, dayVariant, confirmNavigationDate, // Events onDayClicked, listeners, // Exposed methods focusOnDate, moveToNextDay, moveToPreviousDay, moveToPreviousWeek, moveToNextWeek, moveToFirstDayOfWeek, moveToLastDayOfWeek, moveToPreviousMonth, moveToPreviousYear, moveToNextMonth, moveToNextYear, // Translations t, }; }, }); </script> <style lang="scss" scoped src="./calendar-month.scss" />
@@ -0,0 +1 @@
1
+ <template> <table role="grid"> <tbody> <tr> <calendar-month-day v-bind="$props" :day="day" @click="day => $emit(CalendarMonthDayEvent.CLICK, day)" /> </tr> </tbody> </table> </template> <script lang="ts"> import { defineComponent } from 'vue'; import { CalendarMonthDayEvent } from '../calendar-month-day.types'; import CalendarMonthDay from '../calendar-month-day.vue'; import { CalendarMonthDateProps } from '../calendar-month-day.props'; export default defineComponent({ components: { CalendarMonthDay }, props: CalendarMonthDateProps, setup() { return { // Enums CalendarMonthDayEvent, }; }, }); </script>
@@ -0,0 +1 @@
1
+ import { action } from '@storybook/addon-actions'; import { CalendarMonthDayVariant } from './calendar-month-day.types'; import CalendarMonthDay from './calendar-month-day.vue'; import type { Meta, StoryObj } from '@storybook/vue'; import { isVue2 } from 'vue-demi'; const meta: Meta = { title: 'Calendar/Month/Day', component: CalendarMonthDay, argTypes: { // Funcs isDateDisabled: { name: 'isDateDisabled', defaultValue: '', description: 'Disables the dates that meet the criteria defined in the function.', table: { type: { summary: '(currentDate: Date) => boolean' } }, control: { disable: true }, }, // Events click: { name: 'click', defaultValue: '', description: 'Triggered when the day is clicked, allowing to detect what day was chosen', table: { type: { summary: '(selectedDay: Date) => void' } }, control: { disable: true }, }, // Enum props variant: { options: Object.values(CalendarMonthDayVariant), mapping: CalendarMonthDayVariant, control: { type: 'select', }, }, }, }; export default meta; type Story = StoryObj<typeof CalendarMonthDay>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { CalendarMonthDay }, props: Object.keys(argTypes), template: ` <table> <tbody> <tr> <calendar-month-day v-bind="args" @click="clickedAction"/> </tr> </tbody> </table> `, setup(props) { const clickedAction = value => action('click')(value); return { args: isVue2 ? props : _args, // Events clickedAction, }; }, }), args: { selected: false, day: (() => { const date = new Date(); date.setDate(date.getDate() - 1); return date; })(), }, }; export const CurrentDay: Story = { ...Default, args: { day: new Date(), selected: false, }, };
@@ -0,0 +1 @@
1
+ <template> <td class="b-calendar-month-day" :class="conditionalClasses" :tabindex="selected ? '0' : '-1'" :aria-selected="selected ? selected : undefined" :aria-disabled="disabled" :aria-current="isCurrentDay ? 'date' : undefined" :data-date="day.toDateString()" @click="e => onClick(e, day)" @mouseover="$emit(CalendarMonthDayEvent.MOUSE_OVER)" @keydown.space.enter="e => onClick(e, day)" > <div class="b-calendar-month-day__wrapper"> <bento-typography class="b-calendar-month-day__label" el="span" variant="body" strong> {{ formattedDate }} </bento-typography> </div> </td> </template> <script lang="ts"> import { computed, defineComponent } from 'vue'; import { BentoTypography } from '../../../../../../typography'; import { CalendarMonthDayClass, CalendarMonthDayEvent, CalendarMonthDayVariant } from './calendar-month-day.types'; import { CalendarMonthDateProps } from './calendar-month-day.props'; import { getDateTimeFormatter } from '@/utils/ts/format-date'; import { useI18n } from '@/utils/ts/i18n'; /** * Handles the styles and states of the individual dates of the calendar. * * @example * import { CalendarMonthDay, CalendarMonthDayVariant } from './calendar-day'; * * export default { * components: { CalendarMonthDay }, * template: ` * <table> * <tbody> * <tr> * <calendar-day * :day="new Date()" * disabled * selected * :variant="CalendarMonthDayVariant.MIDDLE" * /> * </tr> * </tbody> * </table> * ` * } */ export default defineComponent({ name: 'calendar-month-day', components: { BentoTypography }, inheritAttrs: true, props: CalendarMonthDateProps, emits: [CalendarMonthDayEvent.CLICK, CalendarMonthDayEvent.MOUSE_OVER], setup(props, { emit }) { const { locale } = useI18n(undefined, true); const isCurrentDay = computed(() => { const today = new Date(); return ( props.day.getDate() === today.getDate() && props.day.getMonth() === today.getMonth() && props.day.getFullYear() === today.getFullYear() ); }); /** * Only get translated day part of the date */ const formattedDate = computed(() => { const formatter = getDateTimeFormatter(locale.value, { day: 'numeric' }); const parts = formatter.formatToParts(props.day); const dayPart = parts.find(part => part.type === 'day'); return dayPart?.value ?? ''; }); const conditionalClasses = computed(() => ({ // Variants [`b-calendar-month-day--${CalendarMonthDayClass.BEGINNING}`]: props.variant === CalendarMonthDayVariant.BEGINNING, [`b-calendar-month-day--${CalendarMonthDayClass.MIDDLE}`]: props.variant === CalendarMonthDayVariant.MIDDLE, [`b-calendar-month-day--${CalendarMonthDayClass.END}`]: props.variant === CalendarMonthDayVariant.END, [`b-calendar-month-day--${CalendarMonthDayClass.SINGLE}`]: props.variant === CalendarMonthDayVariant.SINGLE, // Selected [`b-calendar-month-day--${CalendarMonthDayClass.UNSELECTED}`]: !props.selected, [`b-calendar-month-day--${CalendarMonthDayClass.SELECTED}`]: props.selected, // Current Day [`b-calendar-month-day--${CalendarMonthDayClass.CURRENT_DAY}`]: isCurrentDay.value, // Disabled [`b-calendar-month-day--${CalendarMonthDayClass.DISABLED}`]: props.disabled, })); const onClick = (e: MouseEvent | KeyboardEvent, selectedDay: Date) => { if (!props.disabled) { emit(CalendarMonthDayEvent.CLICK, selectedDay, props.day); } else { e.preventDefault(); e.stopPropagation(); } }; return { // Values conditionalClasses, isCurrentDay, formattedDate, // Enums CalendarMonthDayEvent, // Events onClick, }; }, }); </script> <style lang="scss" scoped src="./calendar-month-day.scss" />
@@ -0,0 +1 @@
1
+ <template> <div v-bento-keyboard-navigation-directive class="b-calendar-year" v-on="listeners"> <div class="b-calendar-year__year-selector"> <!-- go to previous year --> <div class="b-calendar-year__previous"> <bento-button v-if="shouldShowPreviousYearButton" variant="tertiary" @click="moveToPreviousYear($event, true)" > <template #iconLeft> <chevron-left-icon :svg-title="t('previousYear')" /> </template> </bento-button> </div> <!-- year --> <bento-typography :id="yearLabelId" el="h2" variant="title" aria-live="polite"> {{ year.getFullYear() }} </bento-typography> <!-- go to next year --> <div class="b-calendar-year__next"> <bento-button v-if="shouldShowNextYearButton" variant="tertiary" @click="moveToNextYear($event, true)"> <template #iconLeft> <chevron-right-icon :svg-title="t('nextYear')" /> </template> </bento-button> </div> </div> <table class="b-calendar-year__year-container" role="grid" :aria-labelledby="yearLabelId"> <tbody> <tr v-for="(row, index) in monthRows" :key="index"> <calendar-year-month v-for="(month, monthIndex) in row" :ref="addDayRef(month)" :key="`calendar-month-${monthIndex}`" :month="month" :disabled="isDateDisabled(month)" :selected="isSelectedDate(month)" :variant="monthVariant(month)" @click="onMonthClicked" @mouseover="emit('mouseover', month)" /> </tr> </tbody> </table> </div> </template> <script setup lang="ts"> import { computed, toRef } from 'vue'; import { isSameMonth } from 'date-fns/isSameMonth'; import { isWithinInterval } from 'date-fns/isWithinInterval'; import BentoButton from '@/components/button/button.vue'; import { BentoTypography } from '@/components/typography'; import CalendarYearMonth from './components/calendar-year-month/calendar-year-month.vue'; import ChevronLeftIcon from '@adyen/ui-assets-icons-16/vue/chevron-left'; import ChevronRightIcon from '@adyen/ui-assets-icons-16/vue/chevron-right'; import { BentoKeyboardNavigationDirective as vBentoKeyboardNavigationDirective } from '@/directives'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { useMoveCalendarDate } from '@/internal/calendar/components/calendar-month/composables/move-calendar-date'; import { type CalendarYearProps } from './calendar-year.types'; import { CalendarYearMonthVariant } from './components/calendar-year-month/calendar-year-month.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const yearLabelId = generateUid('bento-date-picker-year-label'); const props = withDefaults(defineProps<CalendarYearProps>(), { endDate: null, isDateDisabled: undefined, isRange: false, min: null, max: null, position: 'unique', startDate: null, }); const emit = defineEmits<{ (e: 'input', d: Date); (e: 'mouseover', d: Date); (e: 'nextMonth' | 'previousMonth', a: number); }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const isDateDisabled = (date: Date) => (props.min && date < props.min) || (props.max && date > props.max) || props.isDateDisabled?.(date); /** * Renders the previous year button if it's Unique or First calendar */ const shouldShowPreviousYearButton = computed(() => ['first', 'unique'].includes(props.position)); /** * Renders the next year button if it's Unique or Last calendar */ const shouldShowNextYearButton = computed(() => ['last', 'unique'].includes(props.position)); const { internalDate, // Calendar refs addDayRef, // Day navigation moveToPreviousMonth, moveToPreviousYear, moveToNextMonth, moveToNextYear, moveToFirstMonthOfYear, moveToLastMonthOfYear, focusOnDate, } = useMoveCalendarDate( { currentSelectedDate: toRef(props, 'startDate'), month: toRef(props, 'year'), isDateDisabled, }, emit ); defineExpose({ focusOnDate }); /** * Calculates all the months of the current "props.month" * and adds the respective day number to each column and week */ const monthRows = computed(() => { const rows = []; let count = 0; for (let i = 0; i < 3; i++) { const innerArray = []; for (let j = 0; j < 4; j++) { innerArray.push(new Date(props.year.getFullYear(), count++, 1)); } rows.push(innerArray); } return rows; }); /** * Calculates the variation of the days selected. * For single select it's always SINGLE * For ranges: * - Start & End is the same: SINGLE * - Start: BEGINNING * - End: END * - Within the range: MIDDLE * @param {Date} day Current day */ const monthVariant = (day: Date) => { if (!props.isRange || isSameMonth(props.startDate, props.endDate)) { return CalendarYearMonthVariant.SINGLE; } if (props.startDate && isSameMonth(props.startDate, day)) { return CalendarYearMonthVariant.BEGINNING; } if (props.endDate && isSameMonth(props.endDate, day)) { return CalendarYearMonthVariant.END; } if (props.startDate && props.endDate && isWithinInterval(day, { start: props.startDate, end: props.endDate })) { return CalendarYearMonthVariant.MIDDLE; } return null; }; /** * Checks every date in the calendar to see if it's selected. * @param date Current date in the calendar */ const isSelectedDate = (date: Date) => { // Single selection if (internalDate.value && isSameMonth(internalDate.value, date)) { return true; } // Range return ( props.startDate && props.endDate && isWithinInterval(date, { start: props.startDate, end: props.endDate }) ); }; const onMonthClicked = (selectedDate: Date) => { emit('input', selectedDate); }; const listeners = !props.isRange ? { 'keydown:arrow-right': moveToNextMonth, 'keydown:arrow-left': moveToPreviousMonth, 'keydown:arrow-up': (ev: Event) => moveToPreviousMonth(ev, false, 4), 'keydown:arrow-down': (ev: Event) => moveToNextMonth(ev, false, 4), 'keydown:home': moveToFirstMonthOfYear, 'keydown:end': moveToLastMonthOfYear, 'keydown:page-up': moveToPreviousYear, 'keydown:page-down': moveToNextYear, } : {}; </script> <script lang="ts"> /** * CalendarYear that displays all the months of a given year and allows * to move back and forth the visible year with the Next and Previous buttons * * @example * import { CalendarYear } from '@/internal/calendar'; * * export default { * components: { CalendarYear }, * template: ` * <calendar-year * :is-date-disabled="(date: Date) => boolean" * :year="new Date()" * is-range * :start-date="new Date()" * :end-date="new Date()" * @input="(selectedDate: Date) => void" * @mouseover="(event: Event) => void" * @nextMonth="() => void" * @previousMonth="() => void" * /> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./calendar-year.scss" />
@@ -0,0 +1 @@
1
+ <template> <table> <tbody> <tr> <calendar-year-month :month="month" :disabled="disabled" :selected="selected" :variant="variant" @click="date => $emit(CalendarYearMonthEvent.CLICK, date)" /> </tr> </tbody> </table> </template> <script setup lang="ts"> import { CalendarYearMonthEvent, type CalendarYearMonthProps } from '../calendar-year-month.types'; import CalendarYearMonth from '../calendar-year-month.vue'; defineProps<CalendarYearMonthProps>(); </script>
@@ -0,0 +1 @@
1
+ <template> <td class="b-calendar-year-month" :class="conditionalClasses" :tabindex="selected ? '0' : '-1'" :aria-selected="selected ? selected : undefined" @click="e => onClick(e, month)" @mouseover="emit(CalendarYearMonthEvent.MOUSE_OVER)" @keydown.space.enter="e => onClick(e, month)" > <div class="b-calendar-year-month__wrapper"> <bento-date class="b-calendar-year-month__label" :date="month" :format="BentoDateFormat.SHORT_MONTH" /> </div> </td> </template> <script setup lang="ts"> import { computed } from 'vue'; import { BentoDate, BentoDateFormat } from '@/components/date'; import { CalendarYearMonthClass, CalendarYearMonthEvent, type CalendarYearMonthProps, CalendarYearMonthVariant, } from './calendar-year-month.types'; const props = withDefaults(defineProps<CalendarYearMonthProps>(), { disabled: false, selected: false, variant: CalendarYearMonthVariant.SINGLE, }); const emit = defineEmits<{ (e: 'click', s: Date, d: Date); (e: 'mouseover'); }>(); const isCurrentMonth = computed(() => { const today = new Date(); return props.month.getMonth() === today.getMonth() && props.month.getFullYear() === today.getFullYear(); }); const conditionalClasses = computed(() => ({ // Variants [`b-calendar-year-month--${CalendarYearMonthClass.BEGINNING}`]: props.variant === CalendarYearMonthVariant.BEGINNING, [`b-calendar-year-month--${CalendarYearMonthClass.MIDDLE}`]: props.variant === CalendarYearMonthVariant.MIDDLE, [`b-calendar-year-month--${CalendarYearMonthClass.END}`]: props.variant === CalendarYearMonthVariant.END, [`b-calendar-year-month--${CalendarYearMonthClass.SINGLE}`]: props.variant === CalendarYearMonthVariant.SINGLE, // Selected [`b-calendar-year-month--${CalendarYearMonthClass.UNSELECTED}`]: !props.selected, [`b-calendar-year-month--${CalendarYearMonthClass.SELECTED}`]: props.selected, // Current month [`b-calendar-year-month--${CalendarYearMonthClass.CURRENT_MONTH}`]: isCurrentMonth.value, // Disabled [`b-calendar-year-month--${CalendarYearMonthClass.DISABLED}`]: props.disabled, })); const onClick = (e: MouseEvent | KeyboardEvent, selectedDay: Date) => { if (!props.disabled) { emit(CalendarYearMonthEvent.CLICK, selectedDay, props.month); } else { e.preventDefault(); e.stopPropagation(); } }; </script> <script lang="ts"> /** * Handles the styles and states of the individual months of the calendar. * * @example * import { CalendarYearMonth, CalendarYearMonthVariant } from './calendar-year-month'; * * export default { * components: { CalendarYearMonth }, * template: ` * <table> * <tbody> * <tr> * <calendar-year-month * :month="new Date()" * disabled * selected * :variant="CalendarYearMonthVariant.MIDDLE" * /> * </tr> * </tbody> * </table> * ` * } */ export default {}; </script> <style lang="scss" scoped src="./calendar-year-month.scss" />
@@ -0,0 +1 @@
1
+ import { ref } from 'vue'; import { isVue2 } from 'vue-demi'; import BentoCheckboxInput from './checkbox-input.vue'; import type { Meta, StoryObj } from '@storybook/vue'; const meta: Meta = { title: 'Checkbox input', component: BentoCheckboxInput, }; export default meta; type Story = StoryObj<typeof BentoCheckboxInput>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoCheckboxInput }, props: Object.keys(argTypes), template: ` <bento-checkbox-input v-bind="args" :model-value.sync="selectedValue"/> `, setup(props) { const selectedValue = ref(undefined); return { // Values args: isVue2 ? props : _args, selectedValue, }; }, }), args: { value: 'checkbox-input-value', }, };
@@ -0,0 +1 @@
1
+ <template> <div class="b-checkbox-input" :class="conditionalClasses"> <!-- Don't change order because it matters for CSS. Icons first --> <minus-small-icon v-if="indeterminate" class="b-checkbox-input__icon" svg-title="" aria-hidden="true" /> <checkmark-small-icon v-else-if="isChecked" class="b-checkbox-input__icon" svg-title="" aria-hidden="true" /> <input :id="id" ref="inputRef" v-bind="computedAttrs" type="checkbox" class="b-checkbox-input__outline" :class="outlineConditionalClasses" :aria-label="ariaLabel" :aria-checked="ariaChecked" :aria-describedby="ariaDescribedby" :aria-invalid="ariaInvalid" :checked="isChecked" :disabled="disabled" :indeterminate.prop="indeterminate" :value="value" :aria-readonly="readonly" @click="onClick" @change="onChange" @keydown.space.prevent="inputRef?.click()" @keydown.enter.prevent="inputRef?.click()" /> </div> </template> <script setup lang="ts"> import { computed, type HTMLAttributes, type PropType, ref, toRefs, useAttrs } from 'vue'; import { CheckboxInputEvent, type CheckboxInputGroupItems, CheckboxInputState, type CheckboxInputType, type MultiCheckboxInputType, } from '@/internal/checkbox-input/checkbox-input.types'; import CheckmarkSmallIcon from '@adyen/ui-assets-icons-16/vue/checkmark-small'; import MinusSmallIcon from '@adyen/ui-assets-icons-16/vue/minus-small'; import { isVue3 } from 'vue-demi'; const attrs = useAttrs(); const props = defineProps({ /** * The aria label associated to the input checkbox. */ ariaLabel: { type: String, default: '' }, /** * An additional string that serves as description for the input checkbox (useful if the component shows a description). */ ariaDescribedby: { type: String, default: '' }, /** * Setting this property to true will disable the checkbox in your form. */ disabled: { type: Boolean, default: false }, /** * Setting this property to true will show the checkbox in error state. */ hasError: { type: Boolean, default: false }, /** * Sets the id of the checkbox. Required for accessibility purposes */ id: { type: String, default: null }, /** * The :indeterminate CSS pseudo-class represents any form element * whose state is indeterminate, such as checkboxes which have their HTML indeterminate attribute * set to true, radio buttons which are members of a group in which all radio buttons are unchecked, * and indeterminate <progress> elements. https://developer.mozilla.org/en-US/docs/Web/CSS/:indeterminate * This property lets you control the input.indeterminate native property. */ indeterminate: { type: Boolean, default: false }, /** * When handling a single checkbox with a singular state, use a boolean. * * When handling multiple checkboxes, use an array of keys. * These keys exist as a set "value" prop for each checkbox. */ modelValue: { type: [Boolean, Array] as PropType<CheckboxInputType | CheckboxInputGroupItems>, default: false, }, /** * Setting this property to true will set the checkbox to read only state. */ readonly: { type: Boolean, default: false }, /** * The value of the component. The DOM API casts this to a string. * The browser uses "on" as the default value. * * In the case of multiple checkboxes this value is used as a unique key. */ value: { type: [String, Number], default: null }, }); const emit = defineEmits<{ /** * Emit that emits the input value */ (e: 'update:model-value', value: boolean | Array<string | number>): void; }>(); const inputRef = ref<HTMLInputElement | null>(null); // filter out style classes because in vue3 they are also part of attrs const computedAttrs = computed(() => { if (isVue3) { const { class: _class, style: _style, ...rest } = attrs; return rest; } else { return attrs; } }); const { disabled, indeterminate, modelValue, readonly, value } = toRefs(props); const conditionalClasses = computed(() => { // Append classes set from parent to first element const inheritClasses = (attrs.class as string)?.split(' ').reduce((acc, className) => { acc[className] = true; return acc; }, {}); return { [`b-checkbox-input--${CheckboxInputState.DISABLED}`]: props.disabled, [`b-checkbox-input--${CheckboxInputState.READONLY}`]: props.readonly, ...inheritClasses, }; }); const outlineConditionalClasses = computed(() => ({ [`b-checkbox-input__outline--${CheckboxInputState.UNCHECKED}`]: !isChecked.value && !props.indeterminate, [`b-checkbox-input__outline--${CheckboxInputState.ERROR}`]: props.hasError, [`b-checkbox-input__outline--${CheckboxInputState.READONLY}`]: props.readonly, })); const isCheckboxGroup = computed(() => Array.isArray(modelValue.value)); const isChecked = computed(() => { if (!isCheckboxGroup.value) { return modelValue.value as boolean; } return (modelValue.value as MultiCheckboxInputType).some(item => item === value.value); }); // ARIA fields const ariaChecked = computed<HTMLAttributes['aria-checked']>(() => props.indeterminate ? 'mixed' : `${!!isChecked.value}` ); const ariaInvalid = computed<HTMLAttributes['aria-invalid']>( () => props.hasError && !props.disabled && !props.readonly ); // readonly attr doesn't work with checkbox, so we need to manually ignore the click instead const onClick = (event: Event) => { if (readonly.value) { event.preventDefault(); return; } }; const onChange = (event: Event) => { if (disabled.value) { return; } const newValue: boolean = (event.target as HTMLInputElement).checked; if (isCheckboxGroup.value) { const newCheckedList = [...(modelValue.value as MultiCheckboxInputType)]; const index = newCheckedList.indexOf(value.value); if (index === -1) { newCheckedList.push(value.value); } else { newCheckedList.splice(index, 1); } emit(CheckboxInputEvent.UPDATE, newCheckedList); } else { emit(CheckboxInputEvent.UPDATE, newValue); } }; </script> <script lang="ts"> /** * Internal component that renders a checkbox input (without label) * * @example * import { BentoCheckboxInput } from '@adyen/bento-vue2'; * * export default { * components: { BentoCheckboxInput }, * template: ` * <label for="checkbox-id">Your label</label> * <span id="description-id">Description</span> * <bento-checkbox-input id="checkbox-id" aria=label="Your label" aria-describedby="description-id" /> * ` * } */ export default { name: 'checkbox-input', inheritAttrs: false, }; </script> <style lang="scss" scoped src="./checkbox-input.scss" />
@@ -0,0 +1 @@
1
+ <template> <fieldset class="b-controls-group" :class="conditionalClasses" :form="form" :disabled="disabled" :aria-describedby="ariaAttributes['aria-describedby']" > <legend class="b-controls-group__label-container" :class="legendConditionalClasses"> <field-label v-if="label" class="b-controls-group__label" :label="label" :tooltip-text="tooltipText" :optional="optional" :required="required" /> <bento-typography v-if="description || hasSlot('description')" class="b-controls-group__description" el="span" > <slot name="description"> {{ description }} </slot> </bento-typography> </legend> <slot /> </fieldset> </template> <script setup lang="ts"> import { computed, useAttrs, useSlots } from 'vue'; import { BentoTypography } from '@/components/typography'; import { FieldLabel } from '@/components/internal/field-label'; import { deprecate } from '@/utils/ts/deprecate'; import { useHasSlot } from '@/composables'; const attrs = useAttrs(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const props = defineProps({ /** * Descriptive supporting text visbile underneath the label. */ description: { type: String, default: null }, /** * The name of the component to cite when throwing a depreacte error. */ componentName: { type: String, required: true }, /** * Sets all child controls of the <fieldset> as disabled. */ disabled: { type: Boolean, default: false }, /** * The value of the id attribute of a <form> element the <fieldset> is to part of, even if it is not inside the form itself. */ form: { type: String, default: null }, /** * Shows/hides the group label. */ hideLabel: { type: Boolean, default: null }, /** * The name associated with the group. Required for accessibility purposes. * The label is shown when `hideLabel` is set to `true` or if `description` is passed */ label: { type: String, required: true }, /** * Indicates the field is optional. */ optional: { type: Boolean, default: false }, /** * Indicates the field is required. */ required: { type: Boolean, default: false }, /** * Tooltip message. */ tooltipText: { type: String, default: null }, }); const ariaAttributes = computed(() => ({ 'aria-describedby': attrs['aria-describedby']?.toString() as string, })); const conditionalClasses = computed(() => ({ 'b-controls-group--disabled': props.disabled, })); const legendConditionalClasses = computed(() => { const isLabelAndDescriptionPresent = props.label && (props.description || hasSlot('description')); const shouldShowLabel = isLabelAndDescriptionPresent || props.hideLabel === false; return { 'b-controls-group__label-container--hidden': !shouldShowLabel, }; }); // eslint-disable-next-line eqeqeq -- Needs to show deprecate message when hideLabel is not set and no description available const hasNoPropsDefined = !(props.description || hasSlot('description')) && props.hideLabel == undefined; if (hasNoPropsDefined) { deprecate( `${props.componentName}: The property \`label\` will always be rendered by default.`, `After version 2.0.0, the label will be shown by default. To continue hiding the label set \`:hide-label="true"\`. Up until version 2.0.0, to display the label set \`hide-label="false"\`. \`label\` will always be rendered when \`description\` is passed alongside with \`label\``, `2.0.0` ); } </script> <script lang="ts"> /** * A controls group components should be used whenever it is necessary to group a set of controls (i.e. radio buttons or checkboxes) * This component conforms to the official WAI-ARIA guidelines for grouping controls: https://www.w3.org/WAI/tutorials/forms/grouping/ * * NOTE: This component should not be used by itself but more implemented by other exported components * @example * import { ControlsGroup, BentoCheckbox, BentoRadioButton } from '@adyen/bento-vue2/'; * * export default { * components: { ControlsGroup, BentoRadioButton }, * template: ` * <controls-group * label="Your group label" * description="Description" * > * <bento-radio-button * disabled * value="inputRadio" * > * Label text * </bento-radio-button> * <bento-radio-button * value="inputRadio" * > * Another label text * </bento-radio-button> * </controls-group> * ` * }; */ export default {}; </script> <style lang="scss" scoped src="./controls-group.scss" />
@@ -0,0 +1,43 @@
1
+ import { Canvas, Meta, Story } from '@storybook/blocks';
2
+ import * as CopyStories from './copy.stories';
3
+
4
+ <Meta of={CopyStories} />
5
+
6
+ # Copy
7
+
8
+ `Copy` should be used to help the user more easily copy relevant information to the clipboard. When the user clicks the
9
+ component, text should automatically be copied to their clipboard.
10
+
11
+ <Canvas withSource="closed">
12
+ <Story of={CopyStories.Default} />
13
+ </Canvas>
14
+
15
+ ## Variations
16
+
17
+ The component will be rendered by default as a copy icon, but a slot can also be passed to render something else that
18
+ when the user clicks on will copy the content to the clipboard. Useful, for example, with inline copy examples.
19
+
20
+ {/* TODO: change the slot example to inline code when that component is available */}
21
+
22
+ <Canvas withSource="closed">
23
+ <Story of={CopyStories.WithSlot} />
24
+ </Canvas>
25
+
26
+ ## Button customisation
27
+
28
+ The copy button variation can be customised using the `buttonText` and `button` properties.
29
+
30
+ <Canvas withSource="closed">
31
+ <Story of={CopyStories.WithCustomButton} />
32
+ </Canvas>
33
+
34
+ ## Accessibility
35
+
36
+ Assistive technology should notify the user once a text has been copied to the clipboard (i.e. once the content of the
37
+ tooltip changes)
38
+
39
+ ### Keyboard interaction
40
+
41
+ The button/content to be copied should be reached via keyboard navigation and the screen reader should announce the
42
+ content of the tooltip (copy to clipboard) The content should be copied when the button is pressed (enter/space key is
43
+ pressed)
@@ -0,0 +1 @@
1
+ import Copy from './copy.vue'; import { BentoTypography } from '@/components/typography'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import type { Meta, StoryObj } from '@storybook/vue'; import './copy.stories.scss?modules'; import { isVue2 } from 'vue-demi'; const meta: Meta = { title: 'Copy', component: Copy, }; const DEFAULT_PROPS = { text: 'Copy this', }; export default meta; type Story = StoryObj<typeof Copy>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { Copy }, props: Object.keys(argTypes), setup: props => ({ args: isVue2 ? props : _args }), template: ` <copy v-bind="args" /> `, }), args: { ...DEFAULT_PROPS, }, }; export const WithSlot: Story = { render: (_args, { argTypes }) => ({ components: { Copy, BentoTypography }, props: Object.keys(argTypes), setup: props => ({ args: isVue2 ? props : _args }), template: ` <copy v-bind="args"> <bento-typography el="span">Click me to copy</bento-typography> </copy> `, }), args: { ...DEFAULT_PROPS, }, }; const WithCustomButtonCode = ` <copy buttonText="Copy text" :button="{condensed: true, variant: 'secondary' }" /> `; export const WithCustomButton: Story = { render: (_args, { argTypes }) => ({ components: { Copy, BentoTypography }, props: Object.keys(argTypes), setup: props => ({ args: isVue2 ? props : _args }), template: ` <copy v-bind="args" /> `, }), args: { ...DEFAULT_PROPS, buttonText: 'Copy text', button: { condensed: true, variant: 'secondary' }, }, parameters: storybookDocsParameter(WithCustomButtonCode), };
@@ -0,0 +1 @@
1
+ <template> <div class="b-copy" @mouseleave="resetText" @focusout="resetText"> <span v-if="$slots.default" v-bento-tooltip-directive:[tooltipPosition]="copyText" @click="onClickCopy"> <slot /> </span> <bento-button v-else v-bento-tooltip-directive:[tooltipPosition]="copyText" v-bind="button" @click="onClickCopy" > <template #iconLeft> <copy-icon :svg-title="svgTitle" /> </template> <template v-if="buttonText" #default> {{ buttonText }} </template> </bento-button> <div v-if="showScreenReaderAlert" role="alert" class="b-copy__screen-reader-alert">{{ t('copied') }}</div> </div> </template> <script setup lang="ts"> import { computed, ref } from 'vue'; import { BentoButton } from '@/components/button'; import { PopperContainerPosition } from '@/components/popper-container'; import { BentoTooltipDirective as vBentoTooltipDirective } from '@/directives'; import { useI18n } from '@/utils/ts/i18n'; import { type BentoCopyButtonProps, type BentoCopyProps, CopyTooltipState } from './copy.types'; import CopyIcon from '@adyen/ui-assets-icons-16/vue/copy'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const emit = defineEmits<{ /** * Emitted when the copy button is clicked. */ (e: 'click', event: MouseEvent): void; }>(); const props = withDefaults(defineProps<BentoCopyProps>(), { button: () => ({ variant: 'tertiary' }) as BentoCopyButtonProps, buttonText: undefined, tooltipPosition: PopperContainerPosition.TOP, }); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const copyState = ref<CopyTooltipState>(CopyTooltipState.DEFAULT); const showScreenReaderAlert = ref(false); const onClickCopy = async (event: MouseEvent) => { try { await navigator.clipboard.writeText(props.text); copyState.value = CopyTooltipState.COPYING; showScreenReaderAlert.value = true; } catch { copyState.value = CopyTooltipState.ERROR; } emit('click', event); }; const resetText = () => { copyState.value = CopyTooltipState.DEFAULT; showScreenReaderAlert.value = false; }; const copyText = computed(() => { switch (copyState.value) { case CopyTooltipState.COPYING: return t('copied'); case CopyTooltipState.ERROR: return t('copyFailed'); default: return t('copy'); } }); const svgTitle = computed(() => (props.buttonText ? undefined : t('copy'))); </script> <script lang="ts"> /** * An internal button component that copies text to the user's clipboard. * * @example * import { Copy } from '@adyen/bento-vue2'; * * export default { * components: { Copy }, * template: ` * <copy :text="textToCopy" /> * ` * } */ export default { i18n: { messages }, name: 'copy', }; </script> <style lang="scss" scoped src="./copy.scss" />
@@ -0,0 +1 @@
1
+ <template> <div class="b-checkbox-input" :class="conditionalClasses" role="presentation"> <!-- Don't change order because it matters for CSS. Icons first --> <minus-small-icon v-if="indeterminate" class="b-checkbox-input__icon" svg-title="" aria-hidden="true" /> <double-checkmark-small-icon v-else-if="allSelected" class="b-checkbox-input__icon" svg-title="" aria-hidden="true" data-testid="decorative-checkbox-double-checkmark-icon" /> <checkmark-small-icon v-else-if="checked" class="b-checkbox-input__icon" svg-title="" aria-hidden="true" /> <div class="b-checkbox-input__outline" :class="outlineConditionalClasses" /> </div> </template> <script setup lang="ts"> import { computed } from 'vue'; import { CheckboxInputState } from '@/internal/checkbox-input/checkbox-input.types'; import CheckmarkSmallIcon from '@adyen/ui-assets-icons-16/vue/checkmark-small'; import MinusSmallIcon from '@adyen/ui-assets-icons-16/vue/minus-small'; import DoubleCheckmarkSmallIcon from '@adyen/ui-assets-icons-16/vue/double-checkmark-small'; const props = withDefaults( defineProps<{ /** * Setting this property to true will disable the checkbox in your form. */ disabled?: boolean; /** * The :indeterminate CSS pseudo-class represents any form element * whose state is indeterminate, such as checkboxes which have their HTML indeterminate attribute * set to true, radio buttons which are members of a group in which all radio buttons are unchecked, * and indeterminate <progress> elements. https://developer.mozilla.org/en-US/docs/Web/CSS/:indeterminate * This property lets you control the input.indeterminate native property. */ indeterminate?: boolean; /** * Setting this to true will show the checkbox as checked */ checked?: boolean; /** * Setting this to true will show the checkbox as having all items selected, which shows the double checkmark icon */ allSelected?: boolean; }>(), { disabled: false, indeterminate: false, checked: false, allSelected: false, } ); const conditionalClasses = computed(() => ({ [`b-checkbox-input--${CheckboxInputState.DISABLED}`]: props.disabled, })); const outlineConditionalClasses = computed(() => ({ [`b-checkbox-input__outline--${CheckboxInputState.UNCHECKED}`]: !props.checked && !props.indeterminate && !props.allSelected, [`b-checkbox-input__outline--${CheckboxInputState.CHECKED}`]: (props.checked || props.allSelected) && !props.indeterminate, [`b-checkbox-input__outline--${CheckboxInputState.INDETERMINATE}`]: props.indeterminate, })); </script> <script lang="ts"> export default {}; </script> <style lang="scss" scoped src="@/internal/checkbox-input/checkbox-input.scss" />
@@ -0,0 +1 @@
1
+ <template> <Transition :name="animation" mode="out-in" @after-leave="updateAnimation" @leave="startAnimation"> <div v-if="isOpen && showPage" class="b-dialog-page"> <div v-if="hasSlot('header')" class="b-dialog-page__header" :class="headerConditionalClasses"> <div v-if="showBackButton" class="b-dialog-page__back-button" :style="backButtonStyling" data-testid="back-button" > <bento-button variant="tertiary" @click="goToPreviousPage(previousPage)"> <arrow-left-icon :svg-title="t('goToPreviousPage')" /> </bento-button> </div> <slot name="header"></slot> </div> <div v-if="hasSlot('content')" ref="contentElement" class="b-dialog-page__content" :tabindex="isScrollable ? 0 : null" @scroll="onScroll" > <slot name="content" /> </div> <div v-if="hasSlot('footer')" class="b-dialog-page__footer" :class="footerConditionalClasses"> <slot name="footer" /> </div> </div> </Transition> </template> <script setup lang="ts"> import { computed, inject, nextTick, onUnmounted, type PropType, ref, useSlots, watch } from 'vue'; import { BentoButton } from '@/components/button'; import ArrowLeftIcon from '@adyen/ui-assets-icons-16/vue/arrow-left'; import { useHasSlot } from '@/composables/use-has-slot/use-has-slot'; import { useI18n } from '@/utils/ts/i18n'; import { isDevelopmentEnviroment } from '@/utils/ts/dev-environment'; import { BentoDialogPageAnimation, type BentoDialogPageConfigData, type BentoDialogPageRegisterPage, } from './dialog-page.types'; import type { CSSProperties } from 'vue/types/jsx.d.ts'; import { DIALOG_PAGE_CONFIG_INJECTION_KEY, DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY } from './dialog-page.keys'; import messages from './messages.json'; import { useScroll } from '@vueuse/core'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * Identifier of the page, used with activePage prop to handle navigation */ pageId: { type: String, default: undefined, }, /** * Show header and actions border */ alwaysShowBorder: { type: Boolean, default: false, }, /** * Pass custom styling to the back button to position it correctly with the header slot */ backButtonStyling: { type: Object as PropType<CSSProperties>, default: null }, /** * ID of the page for the back button to navigate to. Only use it to overwrite the default back behavior * Used when the back button should not take the user to the natural previous page * Set to `null` to hide back button in the page */ previousPage: { type: String, default: undefined, }, }); const emit = defineEmits<{ /** * Fires when the animation ends */ (e: 'animationend'): void; }>(); const slots = useSlots(); const hasSlot = useHasSlot(slots); const { t } = useI18n<{ message: MessageSchema }>({ messages }); // Refs const contentElement = ref<HTMLDivElement>(null); const hasScroll = ref(false); const { arrivedState } = useScroll(contentElement); // Logic to choose which page is active/shown const showPage = computed(() => (activePage.value ? activePage.value === props.pageId : true)); // Scrolling styling logic const headerConditionalClasses = computed(() => ({ 'b-dialog-page__header--with-border': props.alwaysShowBorder || (hasScroll.value && !arrivedState.top), })); const footerConditionalClasses = computed(() => ({ 'b-dialog-page__footer--with-border': props.alwaysShowBorder || (hasScroll.value && !arrivedState.bottom), })); const isScrollable = computed(() => !(arrivedState.bottom && arrivedState.top)); const onScroll = () => { if (!contentElement.value) { return; } }; const calculateScroll = async watchedValue => { if (!watchedValue) { return; } // Wait for content to be rendered to be able to calculate the height await nextTick(); hasScroll.value = contentElement.value && contentElement.value.scrollHeight > contentElement.value.clientHeight; }; const { activePage, animation, isAnimationActive, isOpen, pages, previousPages, updatePage } = inject<BentoDialogPageConfigData>(DIALOG_PAGE_CONFIG_INJECTION_KEY); watch(() => isOpen.value, calculateScroll, { immediate: true }); watch(() => showPage.value, calculateScroll, { immediate: true }); // Logic to determine whether the back button should be shown on the page const showBackButton = computed(() => { if (props.previousPage) { return true; } if (props.previousPage === null) { return false; } return hasPreviousPages.value && showPage.value; }); // Animation const updateAnimation = () => { animation.value = BentoDialogPageAnimation.TRANSITION_LEVEL_DEEPER; if (isAnimationActive) { isAnimationActive.value = false; } emit('animationend'); }; const startAnimation = () => { if (isAnimationActive) { isAnimationActive.value = true; } }; // Previous page logic const hasPreviousPages = computed(() => previousPages.value.length > 1); const lastPage = computed(() => previousPages.value[previousPages.value.length - 1]); const isActivePageValid = computed(() => pages.value.some(page => page === activePage.value)); const goToPreviousPage = async (page: string) => { if (page) { const index = previousPages.value.indexOf(page); if (index !== -1) { previousPages.value = previousPages.value.slice(0, index + 1); } else { previousPages.value = [page]; } } else { previousPages.value.pop(); } animation.value = BentoDialogPageAnimation.TRANSITION_LEVEL_UPPER; await nextTick(); updatePage(lastPage.value); }; // Add new page to the previousPage array to keep track of user navigation watch( () => activePage.value, async () => { await nextTick(); if (!isOpen.value) { return; } if (!!activePage.value && !isActivePageValid.value) { if (isDevelopmentEnviroment) { // eslint-disable-next-line no-console console.error( `The activePage passed "${activePage.value}" does not match the id of any of the pages` ); return; } } if (activePage.value !== lastPage.value) { previousPages.value.push(activePage.value); } }, { immediate: true } ); // Register page to parent component and inject data from parent component const registerPage = inject<BentoDialogPageRegisterPage>(DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY); const { unregisterPage } = registerPage(props.pageId); // Unregister page on unmounting the page onUnmounted(() => unregisterPage()); </script> <script lang="ts"> /** * DialogPage is an internal component used to render different pages of dialog components (Modal, ModalFullscreen and Sidepanel) * * @example * import { DialogPage } from '@/internal'; * * export default { * components: { DialogPage }, * template: ` * <dialog-page pageId='step1' always-show-border> * <template #header>Header</template> * <template #content>Content</template> * <template #footer>Footer</template> * </dialog-page> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./dialog-page.scss" />
@@ -0,0 +1 @@
1
+ <template> <bento-typography :id="$attrs.id" class="b-error-message" el="span" aria-live="polite"> <div class="b-error-message__icon"> <cross-circle-icon :svg-title="t('error')" /> </div> {{ errorMessage }} </bento-typography> </template> <script setup lang="ts"> import { BentoTypography } from '@/components/typography'; import CrossCircleIcon from '@adyen/ui-assets-icons-16/vue/cross-circle-fill'; import { useI18n } from '@/utils/ts/i18n'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; /** * An error message component should be used whenever it is necessary to add an error message to a component. * If adding to a form component, the proper accessibility measures should be taken when necessary * (ex. associating an element with this one using aria-describedby). * @example * import { ErrorMessage } from '@/components/internal/error-message'; * * export default { * components: { ErrorMessage }, * template: ` * <error-message :error-message="errorMessage" /> * ` * } */ defineProps({ /** * The rendered error message */ errorMessage: { type: String, required: true, }, }); const { t } = useI18n<{ message: MessageSchema }>({ messages }); </script> <script lang="ts"> export default { i18n: { messages }, name: 'error-message', }; </script> <style lang="scss" scoped src="./error-message.scss" />
@@ -0,0 +1 @@
1
+ <template> <label class="b-field-label" :class="conditionalClasses"> <bento-typography el="span" stronger> <template v-if="label">{{ label }}</template> <template v-else><slot></slot></template> </bento-typography> <bento-typography v-if="!!tooltipText" el="span" class="b-field-label__tooltip-wrapper"> <bento-info-icon :tooltip-text="tooltipText" class="b-field-label__tooltip" /> </bento-typography> <bento-typography v-if="required" class="b-field-label__requirement" el="span" >({{ t('required') }})</bento-typography > <bento-typography v-else-if="optional" class="b-field-label__requirement" el="span" >({{ t('optional') }})</bento-typography > </label> </template> <script setup lang="ts"> import { BentoInfoIcon } from '@/components/info-icon'; import { BentoTypography } from '@/components/typography'; import { useI18n } from '@/utils/ts/i18n'; import messages from './messages.json'; import { computed } from 'vue'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * Renders the dropdown with a condensed style. */ condensed: { type: Boolean, default: false }, /** * Field label */ label: { type: String, default: null, }, /** * Sets the field as optional */ optional: { type: Boolean, default: false, }, /** * Sets the field as required */ required: { type: Boolean, default: false, }, /** * Adds a info icon next to the label to display the passed tooltip message. */ tooltipText: { type: String, default: null, }, }); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const conditionalClasses = computed(() => ({ 'b-field-label--condensed': props.condensed })); </script> <script lang="ts"> /** * Internal component to handle the labels for all field components * @example * import { FieldLabel } from '@/components/internal/field-label'; * * export default { * components: { FieldLabel }, * template: ` * <field-label label="Label" tooltip-text="Tooltip info" required /> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./field-label.scss" />
@@ -0,0 +1,21 @@
1
+ import { Canvas, Meta } from '@storybook/blocks';
2
+ import * as FixedScrollerStories from './fixed-scroller.stories';
3
+
4
+ <Meta of={FixedScrollerStories} />
5
+
6
+ # FixedScroller
7
+
8
+ The fixed scroller component allows to scroll the content by one view on each click. If the final scroll is shorter than
9
+ a whole view, the last element stops at the end of the scrollable area, so that no additional gap is created.
10
+
11
+ <Canvas of={FixedScrollerStories.Default} />
12
+
13
+ ## Use Cases
14
+
15
+ Use a FixedScroller if:
16
+
17
+ - The content needs to be scrolled horizontally by one view at a time
18
+
19
+ ## Resources
20
+
21
+ - [Figma link](https://www.figma.com/file/uLabwF3243jdMDsNSP7U9I/branch/pouQdMaQzLTixntp0khsCy/Bento---Components?node-id=9742%3A9601&mode=dev)
@@ -0,0 +1 @@
1
+ import { storybookDocsParameter } from '@/utils/ts/storybook'; import { BentoButton } from '@/components/button'; import type { Meta, StoryObj } from '@storybook/vue'; import { FixedScroller } from './index'; import { isVue2 } from 'vue-demi'; const meta: Meta = { title: 'Fixed scroller', component: FixedScroller, }; export default meta; type Story = StoryObj<typeof FixedScroller>; const defaultCode = ` <template> <fixed-scroller> <div style="display: flex; gap: 8px;"> {{ Content here }} </div> </fixed-scroller> </template> <script setup lang="ts"> import { FixedScroller } from '@/internal' </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { FixedScroller, BentoButton }, props: Object.keys(argTypes), template: ` <div style="width: 500px;"> <fixed-scroller v-bind="args"> <div style="display: flex; gap: 8px;"> <bento-button v-for="(x, index) in 13" :key="index" variant="secondary" :condensed="args.condensed"> Index: {{x}} </bento-button> </div> </fixed-scroller> </div> `, setup(props) { return { args: isVue2 ? props : _args, }; }, }), parameters: storybookDocsParameter(defaultCode), };
@@ -0,0 +1 @@
1
+ <template> <div ref="fixedScrollerRef" class="b-fixed-scroller"> <div class="b-fixed-scroller__container"> <div ref="contentRef" class="b-fixed-scroller__content" :style="{ transform: `translateX(${offset}px)`, paddingTop: shouldShowLeftButton || shouldShowRightButton ? `${extraPaddingTop}px` : null, }" > <slot></slot> </div> </div> <div v-if="shouldShowLeftButton" ref="leftButtonRef" class="b-fixed-scroller__left"> <bento-button variant="secondary" :aria-label="t('scrollLeft')" :condensed="condensed" @click="scrollLeft"> <template #iconLeft> <chevron-left-icon svg-title="checked" aria-hidden="true" /> </template> </bento-button> </div> <div v-if="shouldShowRightButton" ref="rightButtonRef" class="b-fixed-scroller__right"> <bento-button variant="secondary" :aria-label="t('scrollRight')" :condensed="condensed" @click="scrollRight" > <template #iconLeft> <chevron-right-icon svg-title="checked" aria-hidden="true" /> </template> </bento-button> </div> </div> </template> <script setup lang="ts"> import { ref } from 'vue'; import { BentoButton } from '@/components/button'; import { useFixedScroller } from './composables'; import { useI18n } from '@/utils/ts/i18n'; import ChevronLeftIcon from '@adyen/ui-assets-icons-16/vue/chevron-left'; import ChevronRightIcon from '@adyen/ui-assets-icons-16/vue/chevron-right'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const contentRef = ref(null); const fixedScrollerRef = ref(null); const leftButtonRef = ref(null); const rightButtonRef = ref(null); defineProps({ /** * Applies the condensed style to the buttons */ condensed: { type: Boolean, default: false }, /** * Padding to be applied to the top of the scroller container */ extraPaddingTop: { type: Number, default: 0 }, }); const { offset, shouldShowLeftButton, shouldShowRightButton, scrollLeft, scrollRight } = useFixedScroller({ contentRef, containerRef: fixedScrollerRef, leftButtonRef, rightButtonRef, }); </script> <script lang="ts"> /** * Allows to horizontally scroll content by one view at a time. * * @example * import { FixedScroller } from '@/internal'; * * export default { * components: { FixedScroller }, * template: ` * <fixed-scroller> * <div style="display: flex; gap: 8px;"> * {{ Content here }} * </div> * </fixed-scroller> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./fixed-scroller.scss" />
@@ -0,0 +1 @@
1
+ <template> <div class="b-footer-actions"> <div class="b-footer-actions__wrapper"> <bento-typography v-if="actionsLabel" class="b-footer-actions__label" el="span" variant="body" stronger> {{ actionsLabel }} </bento-typography> <bento-button-actions v-if="alignedActions.leftAligned" class="b-footer-actions__left-group" :actions="alignedActions.leftAligned" layout="buttons-start" ></bento-button-actions> <bento-button-actions class="b-footer-actions__right-group" :actions="alignedActions.rightAligned" layout="buttons-end" ></bento-button-actions> </div> </div> </template> <script setup lang="ts"> import { BentoTypography } from '@/components/typography'; import { computed, onMounted, type PropType, toRefs } from 'vue'; import { BentoButtonActions, type BentoButtonActionsList, BentoButtonVariant } from '@/components/button'; const props = defineProps({ /** * List of actions that will be used to render the buttons. * First element in the list will be rendered as "primary". * All elements after the first will be rendered as "secondary". * But you may change the color of 1-st action to red with 'critial' key * Not more than 3 action buttons can be provided * Or 2 action buttons and 1 Label for them * * Each object in the list should have a `title`, an `event` and * (optional) a Vue `icon` from the library `@adyen/ui-assets-icons-16`. * * `title`: Button's text. * * `event`: Event triggered when clicking the Button. * * `icon`: Vue Icon from the library `@adyen/ui-assets-icons-16`. * * ``` * [{ * title: 'Primary',2 * event: () => { ... }, * icon: FourPeopleIcon * }, { * title: 'Secondary', * event: () => { ... } * }] * ``` */ actions: { type: Array as PropType<BentoButtonActionsList>, required: true, }, /** * Text label that is displayed left to the action buttons */ actionsLabel: { type: String, default: null, }, /** * If passed - sets first button color to red */ destructiveActions: { type: Boolean, default: false, }, }); const { actions, destructiveActions } = toRefs(props); const alignedActions = computed(() => { const rightAlignedButtons = actions.value.length < 3 ? actions.value : actions.value.slice(0, actions.value.length - 1); // Add red color to the 1-st Action button rightAlignedButtons[0].critical = destructiveActions.value || rightAlignedButtons[0].critical; return { leftAligned: actions.value.length === 3 ? [ { ...actions.value[actions.value.length - 1], variant: BentoButtonVariant.SECONDARY, }, ] : null, rightAligned: [...rightAlignedButtons], }; }); onMounted(() => { if (props.actions && props.actions.length > 3) { throw new Error('Not more than 3 action buttons may be provided to Bento Modal default variant'); } if (props.actionsLabel && props.actions && props.actions.length > 2) { throw new Error( 'Not more than 2 action buttons may be provided with actions label to Bento Modal default variant' ); } }); </script> <script lang="ts"> /** * Internal component to handle the logic of displaying actions in default Modal and Sidepanel * @example * import { FooterActions } from '@/components/internal/footer-actions'; * * export default { * components: { FooterActions }, * template: ` * <footer-actions :actions="BentoButtonActionsList" actions-label="Label" :destructive-actions="true" /> * ` * } */ export default {}; </script> <style lang="scss" scoped src="./footer-actions.scss" />
@@ -0,0 +1 @@
1
+ <template> <div class="b-grid-layout" :class="computedClasses" data-testid="grid-layout"> <div v-if="hasSlot('col-1')" class="b-grid-layout__column" :class="computeColumnSize(0)" data-testid="grid-layout-col-1" > <slot name="col-1"></slot> </div> <div v-if="hasSlot('col-2')" class="b-grid-layout__column" :class="computeColumnSize(1)" data-testid="grid-layout-col-2" > <slot name="col-2"></slot> </div> <div v-if="hasSlot('col-3')" class="b-grid-layout__column" :class="computeColumnSize(2)" data-testid="grid-layout-col-3" > <slot name="col-3"></slot> </div> <div v-if="hasSlot('col-4')" class="b-grid-layout__column" :class="computeColumnSize(3)" data-testid="grid-layout-col-4" > <slot name="col-4"></slot> </div> <div v-if="hasSlot('col-5')" class="b-grid-layout__column" :class="computeColumnSize(4)" data-testid="grid-layout-col-5" > <slot name="col-5"></slot> </div> <div v-if="hasSlot('col-6')" class="b-grid-layout__column" :class="computeColumnSize(5)" data-testid="grid-layout-col-6" > <slot name="col-6"></slot> </div> </div> </template> <script lang="ts"> import { computed, defineComponent, type PropType } from 'vue'; import { useHasSlot } from '@/composables'; import { type GridLayoutColumnWidth, type GridLayoutColumnWidths, GridLayoutContainerType } from './grid-layout.types'; /** * Component used to render the grid layout in the bento-summary-grid. For internal use only */ export default defineComponent({ name: 'grid-layout', props: { /** * Sets the responsive columns widths for the grid. */ columnWidth: { type: Array as PropType<GridLayoutColumnWidths>, default: null, validator: (array: GridLayoutColumnWidths) => array && array.length <= 6, }, /** * Set the type of CSS container the grid layout has * @values inline-size, size, normal */ containerType: { type: String as PropType<GridLayoutContainerType>, default: GridLayoutContainerType.INLINE, }, /** * Sets the gap in the grid to 0. */ noGap: { type: Boolean, default: false, }, }, setup(props, { slots }) { const hasSlot = useHasSlot(slots); // TODO: add a computedItems computed here that only returns the slots that are presents so as to be able to render the divs in the template via a v-for const computeColumnSize = index => { if (props.columnWidth?.[index] && props.columnWidth?.[index] !== 'auto') { const columnWidth = props.columnWidth[index] as GridLayoutColumnWidth; return [ `b-grid-layout__column--${columnWidth.small}`, `b-grid-layout-m__column--${columnWidth.medium}`, `b-grid-layout-l__column--${columnWidth.large}`, ]; } return null; }; const computedClasses = computed(() => ({ 'b-grid-layout--container-type-normal': props.containerType === GridLayoutContainerType.NORMAL, 'b-grid-layout--container-type-size': props.containerType === GridLayoutContainerType.SIZE, 'b-grid-layout--all-auto': props.columnWidth === null || props.columnWidth.every(width => width === 'auto'), 'b-grid-layout--no-gap': props.noGap, })); return { // Values computeColumnSize, computedClasses, // Methods hasSlot, }; }, }); </script> <style lang="scss" scoped src="./grid-layout.scss" />
@@ -0,0 +1 @@
1
+ <template> <div class="b-header-meta__container"> <bento-typography v-if="description || hasSlot('description')" class="b-header-meta__description" el="div"> <slot name="description">{{ description }}</slot> </bento-typography> <div v-if="lastUpdated" class="b-header-meta__timestamp-section"> <bento-typography class="b-header-meta__timestamp" el="div"> {{ timestamp }} </bento-typography> <bento-loading-button v-if="reloadButton" variant="tertiary" :state="reloadButton.state" @click="reloadButton.event" ><template #iconLeft><reload-icon aria-hidden="true" /></template >{{ reloadButton.title }}</bento-loading-button > </div> </div> </template> <script setup lang="ts"> import { computed, useSlots } from 'vue'; import { useDynamicDateFnsLocale, useHasSlot } from '@/composables'; import { formatDistanceToNow } from 'date-fns/formatDistanceToNow'; import { BentoTypography } from '@/components/typography'; import { BentoLoadingButton } from '@/components/button'; import ReloadIcon from '@adyen/ui-assets-icons-16/vue/refresh'; import { useI18n } from '@/utils/ts/i18n'; import type { HeaderMetaProps } from './header-meta.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const { t, locale } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<HeaderMetaProps>(), { description: null, lastUpdated: null, reloadButton: null, }); const activeLocale = computed(() => (props.lastUpdated ? locale.value : '')); const dateLocale = useDynamicDateFnsLocale(activeLocale); const timestamp = computed(() => props.lastUpdated ? t('lastUpdated', { date: formatDistanceToNow(props.lastUpdated, { locale: dateLocale.value, addSuffix: true, }), }) : null ); const slots = useSlots(); const hasSlot = useHasSlot(slots); </script> <script lang="ts"> /** * An internal component used by the BentoHeader and BentoHeaderWithViews components. * * @example * import { HeaderMeta } from '@/internal'; * * export default { * components: { HeaderMeta }, * template: ` * <header-meta description="header description" :last-updated="new Date()" /> * ` * } * @slot description - The description content. Overrides the `description` prop. * @property description - Text showed under the title. * @property lastUpdated - JavaScript "Date" object for when the content was last updated. * @property reloadButton - Reload button which shows next to the header's timestamp. */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./header-meta.scss" />
@@ -0,0 +1 @@
1
+ <template> <div ref="infoIconRef" class="b-info-icon-with-popover" tabindex="0" data-testid="info-icon-with-popover" @mouseenter="openPopover" @mouseleave="closePopover" @focus="infoIconOnFocus" @focusout="infoIconOnFocusOut" > <info-icon ref="targetElement" class="b-info-icon-with-popover__svg" :aria-expanded="isOpen" :aria-describedby="popoverId" :svg-title="t('moreInformation')" /> <bento-popover v-if="targetElement" :open="isOpen" :target-element="targetElement" :position="popoverPosition" role="tooltip" fit-content disable-focus-trap > <bento-typography> <slot> <div v-if="popoverText"> {{ popoverText }} </div> </slot> </bento-typography> </bento-popover> </div> </template> <script setup lang="ts"> import { BentoPopover, BentoPopoverPositions } from '@/components/popover'; import { BentoTypography } from '@/components/typography'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import InfoIcon from '@adyen/ui-assets-icons-16/vue/info'; import { type PropType, ref } from 'vue'; import type { BentoPopoverProps } from '../../popover/popover.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const CLOSING_POPOVER_DELAY_MS = 200; defineProps({ /** * Text showed in the popover on hover */ popoverText: { type: String, default: null }, /** * Position the popover should take if space is available */ popoverPosition: { type: String as PropType<BentoPopoverProps['position']>, default: 'bottom-start', validator: (value: BentoPopoverPositions) => Object.values(BentoPopoverPositions).includes(value), }, }); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const infoIconRef = ref<HTMLDivElement>(); const targetElement = ref(null); const popoverId = generateUid('bento-info-icon-with-popover'); const isOpen = ref(); const closingTimeOutId = ref(); const openPopover = () => { isOpen.value = true; if (closingTimeOutId.value) { clearTimeout(closingTimeOutId.value); } }; const closePopover = () => { // We want to delay closing for a couple of seconds so // it doesn't flicker if a user hovers over the icon and then inside the popover. const timeOutId = setTimeout(() => { isOpen.value = false; }, CLOSING_POPOVER_DELAY_MS); closingTimeOutId.value = timeOutId; }; const infoIconOnFocus = () => { openPopover(); }; /** * Only close popover on blur if the newly focus element is not a child of this element * @param e - the event that contains the newly focused element */ const infoIconOnFocusOut = (e: FocusEvent) => { if ( !infoIconRef.value || !(e?.relatedTarget instanceof Element) || !infoIconRef.value.contains(e.relatedTarget) ) { closePopover(); } }; </script> <script lang="ts"> /** * Internal component to be used whenever an info icon needs to be rendered with a popover instead of a tooltip. * * @example * import { BentoInfoIconWithPopover } from '@adyen/bento-vue2'; * * export default { * components: { BentoInfoIconWithPopover }, * template: ` * <info-icon-with-popover popover-text="Content can be passed as a string like" :popover-position="position"> * Content can also be rendered as a slot * </info-icon-with-popover> * ` * } */ export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./info-icon-with-popover.scss" />