@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,53 @@
1
+ import { Canvas, Story, Meta } from '@storybook/blocks';
2
+ import * as TextareaStories from './textarea.stories';
3
+
4
+ <Meta of={TextareaStories} />
5
+
6
+ # Textarea
7
+
8
+ The Textarea represents a multi-line plain-text editing control, useful when you want to allow users to enter a sizeable
9
+ amount of free-form text, for example a comment on a payment or feedback form.
10
+
11
+ It's a complete form control including a label, input, icons and description text.
12
+
13
+ <Canvas withSource="closed">
14
+ <Story of={TextareaStories.Default} />
15
+ </Canvas>
16
+
17
+ - [Textarea](#textarea)
18
+ - [Disabled](#disabled)
19
+ - [Tooltip](#tooltip)
20
+ - [Validation](#validation)
21
+ - [Accessibility](#accessibility)
22
+ - [Focus](#focus)
23
+
24
+ ## Disabled
25
+
26
+ The disabled property determines whether the input component is enabled or disabled.
27
+
28
+ When an input component is disabled, it will not allow user interaction and the value of the input cannot be changed.
29
+
30
+ The visual representation of a disabled input-field component is as a grayed-out color or and lack of hover effect.
31
+
32
+ ## Tooltip
33
+
34
+ A tooltip with extra information for the field can be rendered by passing the desired information to the `tooltipText`
35
+ prop.
36
+
37
+ ## Validation
38
+
39
+ The error prop toggles the error state.
40
+
41
+ ## Accessibility
42
+
43
+ ### Focus
44
+
45
+ When nn input-field or label is clicked or selected, the focus ring should be visible. The focus ring is an affordance
46
+ that allows users to easily identify which element is currently focused.
47
+
48
+ Identifying the current focused element is key to keep tab order obvious and explicit. Not providing a focus state on
49
+ click events could result in making focus management unintuitive and more complex, and fail to comply with basic
50
+ accessibility guidelines such as:
51
+
52
+ 2.4.3 Focus order: If a Web page can be navigated sequentially and the navigation sequences affect meaning or operation,
53
+ focusable components receive focus in an order that preserves meaning and operability.
@@ -0,0 +1 @@
1
+ import { action } from '@storybook/addon-actions'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { type Meta, type StoryObj } from '@storybook/vue'; import { isVue2 } from 'vue-demi'; import BentoTextarea from './textarea.vue'; import { ref } from 'vue'; import { BentoTextareaResizeOption } from './textarea.types'; const meta: Meta = { title: 'Textarea', component: BentoTextarea, argTypes: { resizable: { options: Object.values(BentoTextareaResizeOption), mapping: BentoTextareaResizeOption, control: { type: 'select', }, }, input: { control: { disable: true }, table: { category: 'Deprecated' }, description: 'Deprecated since version 2.0. Use `v-model` or `update:model-value` instead', }, value: { control: { type: 'text' }, table: { category: 'Deprecated' }, description: 'Deprecated for usage with properties since version 2.0. Use `v-model` or `model-value` property instead', }, }, }; export default meta; type Story = StoryObj<typeof BentoTextarea>; const DEFAULT_TEXTAREA_PROPS = { description: 'Description', label: 'Label', placeholder: 'Enter text', clearable: true, optional: true, height: '', characterLimit: 100, }; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoTextarea }, props: Object.keys(argTypes), template: ` <bento-textarea v-bind="args" @blur="blurAction" @change="changeAction" @clear="clearAction" @focus="focusAction" @update:model-value="updateModelValueAction" @keydown="keydownAction" @keyup="keyupAction" /> `, setup(props) { const args = isVue2 ? props : _args; const textareaValue = ref(args.modelValue); const blurAction = (value: string) => action('blur')(value); const changeAction = (value: string) => action('change')(value); const focusAction = (value: string) => action('focus')(value); const updateModelValueAction = (value: string) => action('update:model-value')(value); const clearAction = () => { textareaValue.value = ''; action('clear')(); }; const keydownAction = ({ key, code }: KeyboardEvent) => action('keydown')({ key, code }); const keyupAction = ({ key, code }: KeyboardEvent) => action('keyup')({ key, code }); return { // Methods blurAction, clearAction, changeAction, focusAction, updateModelValueAction, keydownAction, keyupAction, // Values args, textareaValue, }; }, }), args: { ...DEFAULT_TEXTAREA_PROPS, }, parameters: storybookDocsParameter(` <bento-textarea :aria-label="string" :character-limit="number" :clearable="boolean" :description="string" :disabled="boolean" :error="boolean" :height="string" :label="string" :optional="boolean" :placeholder="string" :readonly="boolean" :resizable="boolean" :required="boolean" :model-value="textareaValue" @blur="blurAction" @change="changeAction" @clear="clearAction" @focus="focusAction" @update:model-value="modelValueUpdate" @keydown="keydownAction" @keyup="keyupAction" /> `), };
@@ -0,0 +1 @@
1
+ <template> <div ref="textareaComponent" class="b-textarea" :class="conditionalClasses"> <div :style="{ width: textareaWrapperWidth }"> <div class="b-textarea__label-box" :class="conditionalClassesLabelBox"> <field-label v-if="label" :for="textareaId" :label="label" :tooltip-text="tooltipText" :optional="optional" :required="required" /> <bento-typography v-if="characterLimit" el="span" class="b-textarea__label-item" :class="conditionalClassesLabelItem" > {{ counter }} </bento-typography> </div> <bento-typography el="div" class="b-textarea__input-box" :aria-disabled="disabled"> <textarea :id="textareaId" ref="textareaElement" :value="textareaValue" class="b-textarea__input" :class="conditionalClassesInput" :aria-label="computedAriaLabel" :aria-invalid="invalidInput" :readonly="isReadOnly" :required="required" :disabled="disabled" :placeholder="placeholder" :style="{ resize: resizable, height: textareaHeight }" @blur="onBlur" @change="onChange" @focus="onFocus" @input="onInput" @keydown.esc="onPressEscape" @keydown="onKeyDown" @keyup="onKeyUp" @mousedown="onMouseDown" @mouseup="onMouseUp" > </textarea> <div v-if="showClearButton" class="b-textarea__clear-button"> <bento-button variant="tertiary" :disabled="disabled" @click="onClear" @keypress.enter="onClear" @keyup.space="onClear" > <template #iconLeft> <cross-circle-fill-small-icon :svg-title="t('clearText')" /> </template> </bento-button> </div> </bento-typography> <error-message v-if="isOverCharacterLimit" :error-message="counterErrorText" /> <error-message v-if="errorMessage" :error-message="errorMessage" /> <bento-typography v-if="description" class="b-textarea__description" el="span"> {{ description }} </bento-typography> </div> </div> </template> <script setup lang="ts"> import { computed, nextTick, onMounted, onUnmounted, ref, toRef, toRefs, useAttrs, watch } from 'vue'; // Components import { BentoButton } from '@/components/button'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; import CrossCircleFillSmallIcon from '@adyen/ui-assets-icons-16/vue/cross-circle-fill-small'; // Utils import { generateUid } from '@/core/utils/ts'; import { observeSizeOfElement } from '@/utils/ts/resize'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; // Composables import { useFormLayoutFieldLoading } from '@/composables/use-form-layout-loading/use-form-layout-loading'; // Constants import { BBorderWidthAttention } from '@adyen/bento-design-tokens/dist/js/bento/es6'; // Types import { BentoTextareaEvent, type BentoTextareaProps, BentoTextareaStateClass } from './textarea.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const props = withDefaults(defineProps<BentoTextareaProps>(), { ariaLabel: undefined, characterLimit: undefined, description: '', disabled: false, clearable: false, errorMessage: '', height: null, label: '', optional: false, placeholder: '', readonly: false, resizable: 'both', required: false, tooltipText: null, value: '', modelValue: '', }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: BentoTextareaEvent.BLUR): void; /** * Emitted when the user modifies the element's value. Unlike the 'input' event, the change event is not necessarily fired for each alteration to an element's value */ (e: BentoTextareaEvent.CHANGE): void; /** * Emitted when the "Clear" button was clicked */ (e: BentoTextareaEvent.CLEAR): void; /** * Emitted when the "ESC" key is pressed */ (e: BentoTextareaEvent.ESCAPE_PRESSED): void; /** * Emitted when the element has received focus */ (e: BentoTextareaEvent.FOCUS): void; /** * Emitted when the component's model changes * * @deprecated since version 2.0. Use `v-model` or `update:model-value` instead. * */ (e: BentoTextareaEvent.INPUT, value: string): void; /** * Emitted when the component's model changes */ (e: BentoTextareaEvent.UPDATE_MODEL_VALUE, value: string): void; /** * Emitted when the text area keydown event is fired */ (e: BentoTextareaEvent.KEYDOWN, event: KeyboardEvent): void; /** * Emitted when the text area keyup event is fired */ (e: BentoTextareaEvent.KEYUP, event: KeyboardEvent): void; }>(); const { height } = toRefs(props); const textareaId = generateUid('textarea'); const textareaElement = ref<HTMLTextAreaElement>(null); const textareaComponent = ref<HTMLDivElement>(null); const textareaValue = ref(props.modelValue || props.value); const defaultTextareaHeight = 44; const textareaHeight = ref(props.height || `${defaultTextareaHeight}px`); const userSetHeight = ref<number>(0); const textareaWrapperWidth = ref('100%'); // The width set to the wrapper of the label, textarea and description const isUserResizing = ref(false); let textareaElementObserver: ResizeObserver | null = null; let textareaComponentObserver: ResizeObserver | null = null; const { t, n } = useI18n<{ message: MessageSchema }>({ messages }); const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const { emitValue } = useFormFieldEmits<string>(emit); const counter = computed(() => `${n(textareaValue?.value?.length ?? 0)} / ${n(props.characterLimit)}`); const isDynamicHeight = computed(() => !height.value); const isOverCharacterLimit = computed( () => props.characterLimit && textareaValue.value.length > props.characterLimit ); /** * Calculates and sets the height of the textarea. * The height is determined by the maximum of the following values: * * 1. The content's scroll height (with a minimum of `defaultTextareaHeight`) + the border width. * 2. The height set by the user manually resizing the textarea (`userSetHeight`). * 3. The default minimum height (`defaultTextareaHeight`). * * This ensures the textarea grows with content, shrinks when content is removed, * but never goes below the manually set height or the default height. */ const calculateHeight = () => { if (textareaElement.value) { const newHeight = Math.max( textareaElement.value.scrollHeight <= defaultTextareaHeight ? defaultTextareaHeight : textareaElement.value.scrollHeight + TEXTAREA_BORDER_WIDTH * 2, userSetHeight.value, defaultTextareaHeight ); textareaHeight.value = `${newHeight}px`; } }; watch(height, () => { if (isDynamicHeight.value) { calculateHeight(); } else { textareaHeight.value = height.value; } }); watch( () => [props.value, props.modelValue], () => { textareaValue.value = props.modelValue || props.value; }, { immediate: true } ); watch( () => textareaValue.value, async () => { if (isDynamicHeight.value) { // Hack to make the textarea calculate its height properly // Set the textarea back to the original value before calculating the height textareaHeight.value = `${defaultTextareaHeight}px`; if (!textareaValue.value) { return; } await nextTick(); calculateHeight(); } }, { immediate: true } ); const invalidInput = computed(() => !!props.errorMessage || isOverCharacterLimit.value); const conditionalClasses = computed(() => ({ [`b-textarea--${BentoTextareaStateClass.DISABLED}`]: props.disabled, })); const conditionalClassesInput = computed(() => ({ [`b-textarea__input--${BentoTextareaStateClass.CLEARABLE}`]: props.clearable, [`b-textarea__input--${BentoTextareaStateClass.READONLY}`]: isReadOnly.value, [`b-textarea__input--${BentoTextareaStateClass.ERROR}`]: invalidInput.value, [`b-textarea__input--dynamic-height`]: isDynamicHeight.value, })); const conditionalClassesLabelBox = computed(() => ({ [`b-textarea__label-box--no-label`]: !props.label, })); const conditionalClassesLabelItem = computed(() => ({ [`b-textarea__label-item--${BentoTextareaStateClass.ERROR}`]: isOverCharacterLimit.value, })); const counterErrorText = computed(() => t('yourTextCanBenAtMost', { characterLimit: props.characterLimit })); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel, label } = toRefs(props); const computedAriaLabel = useAriaLabel({ ariaLabel: ariaLabelAttribute.value as string, // TODO: take this line away when the aria-label props is removed label: ariaLabel, defaultFallback: label, }); const showClearButton = computed( () => props.clearable && textareaValue.value && !props.disabled && !isReadOnly.value ); const onClear = () => { textareaValue.value = ''; emit(BentoTextareaEvent.CLEAR); emitValue(''); }; const onInput = (event: InputEvent) => { event.stopImmediatePropagation(); if (!props.disabled && !isReadOnly.value) { textareaValue.value = (event.target as HTMLInputElement).value; /** * Triggered when input value is changed * * @event input * @property {String} inputValue - the updated textarea value */ emitValue(textareaValue.value); } }; const onBlur = () => { textareaElement.value.scrollTop = 0; emit(BentoTextareaEvent.BLUR); }; const onFocus = () => { textareaElement.value.focus(); emit(BentoTextareaEvent.FOCUS); }; const onChange = () => { emit(BentoTextareaEvent.CHANGE); }; const onPressEscape = () => { emit(BentoTextareaEvent.ESCAPE_PRESSED); }; const onKeyDown = (event: KeyboardEvent) => { event.stopPropagation(); emit(BentoTextareaEvent.KEYDOWN, event); }; const onKeyUp = (event: KeyboardEvent) => { event.stopPropagation(); emit(BentoTextareaEvent.KEYUP, event); }; const onMouseDown = () => { isUserResizing.value = true; }; const onMouseUp = () => { isUserResizing.value = false; }; /** * Updates the textarea width depending if the container or the user update its size. * @param isTextareaElement - sets to true if the element being observed is the textarea HTML element. */ const resizeTextareaWrapper = (isTextareaElement = false) => { // Width assigned by the browser whenever the textarea is manually resized. String of the format `<value>px`. const textareaStyleWidth = textareaElement.value.style.width; // Make sure the tracked textareaHeight is always in sync with the height size in case we manually resize the textarea if (textareaElement.value.style.height) { if (isUserResizing.value) { userSetHeight.value = parseInt(textareaElement.value.style.height, 10); } textareaHeight.value = textareaElement.value.style.height; } // If the textarea element observer is being triggered, but there's no textareaStyleWidth, we want to ignore it, as no manual resize as happened yet // The component wrapper will handle the resizing logic instead if (isTextareaElement && !textareaStyleWidth) { return; } // Width of the whole bento-textarea component const textareaComponentWidth = textareaComponent?.value?.offsetWidth; // If there is no textareaStyleWidth or the textareaStyleWidth is larger than the component container, the wrapper should be 100% of the container if (!textareaStyleWidth || parseInt(textareaStyleWidth, 10) > textareaComponentWidth) { textareaWrapperWidth.value = '100%'; } else { // Else the wrapper should always be as wide as the textarea element itself textareaWrapperWidth.value = textareaStyleWidth; } }; const focus = () => { textareaElement.value.focus(); }; defineExpose({ focus, }); onMounted(() => { textareaElementObserver = observeSizeOfElement(textareaElement.value, () => { resizeTextareaWrapper(true); }); textareaComponentObserver = observeSizeOfElement(textareaComponent.value, () => { resizeTextareaWrapper(); }); if (isDynamicHeight.value) { calculateHeight(); } if (props.value) { deprecate( 'BentoTextarea "value" property', `The use of "value" prop in "BentoTextarea" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } }); onUnmounted(() => { textareaElementObserver?.disconnect(); textareaComponentObserver?.disconnect(); }); </script> <script lang="ts"> const TEXTAREA_BORDER_WIDTH = parseFloat(BBorderWidthAttention); export default { i18n: { messages }, name: 'bento-textarea', model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./textarea.scss" />
@@ -0,0 +1 @@
1
+ import BentoTimelineItem from './timeline-item.vue'; import BentoTimeline from '../../timeline.vue'; import type { Meta, StoryObj } from '@storybook/vue'; import { BentoTimelineItemVariant } from '../../timeline.types'; import { isVue2 } from 'vue-demi'; import { BentoDateFormat } from '@/components/date'; const meta: Meta = { title: 'Timeline/Timeline Item', component: BentoTimelineItem, argTypes: { variant: { options: Object.keys(BentoTimelineItemVariant), mapping: BentoTimelineItemVariant, defaultValue: BentoTimelineItemVariant.DEFAULT, control: { type: 'select', }, table: { category: 'Deprecated' }, }, type: { control: { type: 'select', }, }, status: { control: { type: 'select', }, }, }, }; export default meta; type Story = StoryObj<typeof BentoTimeline>; const DEFAULT_PROPS = { title: 'Milestone', timestamp: { date: new Date(), value: 'AuthorName', format: BentoDateFormat.FULL_DATE_EXACT_TIME_WITHOUT_PERIOD, }, dataList: [ { label: 'Label', value: 'Value' }, { label: 'Label', value: 'Value' }, ], description: 'Space to put some description about this milestone', tag: { label: 'Tag' }, status: 'black', }; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoTimeline, BentoTimelineItem }, props: Object.keys(argTypes), template: ` <bento-timeline> <bento-timeline-item v-bind="args" /> </bento-timeline> `, setup(props) { return { args: isVue2 ? props : _args, }; }, }), args: { ...DEFAULT_PROPS, }, };
@@ -0,0 +1 @@
1
+ <template> <li v-if="isVisible" class="b-timeline-item" :class="conditionalClasses"> <div v-if="timegap" class="b-timeline-item__row"> <div class="b-timeline-item__marker" aria-hidden="true"> <div class="b-timeline-item__icon b-timeline-item__icon--timegap"></div> <div class="b-timeline-item__separator"></div> </div> <div class="b-timeline-item__content"> <bento-typography class="b-timeline-item__timegap" :aria-label="timegapAriaLabel"> {{ timegap }} </bento-typography> </div> </div> <div class="b-timeline-item__row"> <div class="b-timeline-item__marker" aria-hidden="true"> <square-small-icon class="b-timeline-item__icon" :class="iconConditionalClasses" /> <div v-if="!isLastItem" class="b-timeline-item__separator" data-testid="last-timeline-item-separator" ></div> </div> <div class="b-timeline-item__content"> <bento-typography v-if="!isNoteType" el="div" variant="body" strongest class="b-timeline-item__title" role="heading" aria-level="2" > <bento-link v-if="isLinkTitle" v-bind="linkTitleProps" disable-typography>{{ linkTitleProps.title }}</bento-link> <span v-else>{{ title }}</span> </bento-typography> <bento-typography v-if="isNoteType && (description || hasSlot('default'))" el="div" variant="body" class="b-timeline-item__description" > <slot>{{ description }}</slot> </bento-typography> <bento-typography v-if="timestamp" class="b-timeline-item__timestamp" el="div" variant="caption"> <bento-date :date="timestamp.date" :format="timestamp.format" disable-typography /> {{ timestamp.value }} </bento-typography> <bento-typography v-if="!isNoteType && additionalTimestamp" class="b-timeline-item__timestamp" el="div" variant="caption" > <bento-date :date="additionalTimestamp.date" :format="additionalTimestamp.format" disable-typography /> {{ additionalTimestamp.value }} </bento-typography> <bento-typography v-if="!isNoteType && (description || hasSlot('default'))" el="div" variant="body" class="b-timeline-item__description" > <slot>{{ description }}</slot> </bento-typography> <div v-if="!isNoteType && dataList" class="b-timeline-item__data-list"> <bento-structured-list> <bento-structured-list-item v-for="listEntry in dataList" :key="generateUid(listEntry.label)" :label="listEntry.label" > <bento-typography variant="caption">{{ listEntry.value }}</bento-typography> </bento-structured-list-item> </bento-structured-list> </div> <bento-tag v-if="!isNoteType && tag" v-bind="tag" class="b-timeline-item__tag" /> </div> </div> <div v-if="displayShowMoreButton" class="b-timeline-item__row"> <div class="b-timeline-item__marker" aria-hidden="true"> <div class="b-timeline-item__separator"></div> </div> <div class="b-timeline-item__content"> <bento-button class="b-timeline__show-more" variant="tertiary" @click="showAll = !showAll"> <template v-if="!showAll">{{ t('showMore', { items: hiddenItems }) }}</template> <template v-else>{{ t('showLess') }}</template> </bento-button> </div> </div> </li> </template> <script setup lang="ts"> import { computed, defineComponent, inject, onUnmounted, provide, useSlots } from 'vue'; // Components import { BentoButton } from '@/components/button'; import { BentoDate } from '@/components/date'; import { BentoLink } from '@/components/link'; import { BentoTag } from '@/components/tag'; import { BentoTypography } from '@/components/typography'; import { BentoStructuredList, BentoStructuredListItem } from '@/components/structured-list'; import SquareSmallIcon from '@adyen/ui-assets-icons-16/vue/square-small-fill'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { useHasSlot } from '@/composables'; import { formatDistanceStrict } from 'date-fns/formatDistanceStrict'; // Types import { type BentoTimelineConfigData, type BentoTimelineEntry, type BentoTimelineItemProps, type BentoTimelineItemTitle, BentoTimelineItemVariant, type BentoTimelineRegisterEntry, } from '../../timeline.types'; // Keys import { TIMELINE_CONFIG_INJECTION_KEY, TIMELINE_REGISTER_ENTRY_INJECTION_KEY } from '../../timeline.keys'; import { STRUCTURED_LIST_CONDENSED_INJECTION_KEY } from '@/components/structured-list/structured-list.keys'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoTimelineItemProps>(), { additionalTimestamp: null, dataList: null, description: null, status: 'black', tag: undefined, title: null, type: 'milestone', variant: null, }); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const slots = useSlots(); const hasSlot = useHasSlot(slots); const isNoteType = computed(() => props.type === 'note'); // Handle variant deprecation, by setting `critical` and `success` variants to their respective colors const computedStatus = computed(() => { if (props.variant === BentoTimelineItemVariant.CRITICAL) { return 'red'; } if (props.variant === BentoTimelineItemVariant.SUCCESS) { return 'green'; } return props.status; }); if (props.variant) { deprecate( 'BentoTimelineItem "variant" property', `Use the BentoTimelineItem "status" modifier instead to modify the color of the timeline icon. <bento-timeline-item status="red" />`, '2.0.0' ); } const iconConditionalClasses = computed(() => ({ [`b-timeline-item__icon--${computedStatus.value}`]: computedStatus.value && !isNoteType.value, })); const conditionalClasses = computed(() => ({ 'b-timeline-item--note': isNoteType.value, })); // Provide STRUCTURED_LIST_CONDENSED_INJECTION_KEY as true, to set the internal condensed style in the BentoStructuredList comp provide(STRUCTURED_LIST_CONDENSED_INJECTION_KEY, true); const entry: BentoTimelineEntry = { title: props.title, date: props.timestamp.date, }; // Register timeline entry to the timeline entries array const registerTimelineEntry: BentoTimelineRegisterEntry = inject(TIMELINE_REGISTER_ENTRY_INJECTION_KEY); const { timelineEntries, unregisterTimelineEntry } = registerTimelineEntry(entry); // On unmounting timeline item, unregister it from the parent timeline component onUnmounted(() => unregisterTimelineEntry()); // Get timeline config data from the parent Timeline comp const { showAll, showMoreIndex, hiddenItems, visibleIndexes, timegapLimit }: BentoTimelineConfigData = inject(TIMELINE_CONFIG_INJECTION_KEY); const index = computed(() => timelineEntries.value.indexOf(entry)); const allItemsShowing = computed(() => showAll.value || !visibleIndexes.value); const isVisible = computed(() => (allItemsShowing.value ? true : visibleIndexes.value.includes(index.value))); const isLinkTitle = computed(() => typeof props.title === 'object'); const linkTitleProps = computed(() => props.title as BentoTimelineItemTitle); const timegap = computed(() => { if (!timegapLimit.value) { return null; } const previousDate = timelineEntries.value[index.value - 1]?.date; const isPreviousItemVisible = allItemsShowing.value || visibleIndexes.value.includes(index.value - 1); if (!previousDate || !isPreviousItemVisible) { return null; } const { date } = timelineEntries.value[index.value]; const timegapInSelectedUnit = parseInt( formatDistanceStrict(previousDate, date, { unit: timegapLimit.value.unit }), 10 ); return timegapLimit.value.amount < timegapInSelectedUnit ? formatDistanceStrict(previousDate, date, { roundingMethod: 'floor' }) : null; }); const timegapAriaLabel = computed(() => (timegap.value ? t('timegap', { timegap: timegap.value }) : '')); const displayShowMoreButton = computed(() => hiddenItems.value && showMoreIndex.value === index.value); const isLastItem = computed(() => { // The index can be -1 before the parent has had a chance to sort the array. return index.value !== -1 && index.value === timelineEntries.value.length - 1; }); </script> <script lang="ts"> /** * BentoTimelineItem renders each event in the Timeline component * * @example * import { BentoDateFormat, BentoTimelineItem } from '@adyen/bento-vue2'; * * export default { * components: { BentoTimelineItem }, * template: ` * <bento-timeline-item * title="Milestone" * :timestamp="{ date: new Date(), value: 'Created at', format: BentoDateFormat.FULL_DATE_EXACT_TIME_WITHOUT_PERIOD }" * :additional-timestamp="{ date: new Date(), value: 'Modified at' }" * description="Milestone description" * :data-list="[ * { label: 'Label', value: 'Value' }, * { label: 'Label', value: 'Value' }, * ]" * :tag="{ label: 'Tag'}" * /> * ` * } */ export default defineComponent({ name: 'bento-timeline-item', i18n: { messages }, }); </script> <style lang="scss" scoped src="./timeline-item.scss" />
@@ -0,0 +1,131 @@
1
+ import { Canvas, Meta } from '@storybook/blocks';
2
+ import * as TimelineStories from './timeline.stories';
3
+
4
+ <Meta of={TimelineStories} />
5
+
6
+ # Timeline
7
+
8
+ Timeline displays a collection of events in a reverse chronological order (newest event is displayed on the top).
9
+
10
+ <Canvas of={TimelineStories.Default} />
11
+
12
+ ## Use Cases
13
+
14
+ Use the `bento-timeline` component to display a collection of chronological events. They should always include at least
15
+ a title and corresponding timestamp.
16
+
17
+ ### Do not use
18
+
19
+ Don't use the timeline for notes, comments or to guide a user through a multistep flow - these will be handled by
20
+ separate components.
21
+
22
+ ## Variations
23
+
24
+ ### Variant (deprecated)
25
+
26
+ > ⚠️ **Deprecation Notice**: Timeline item variants are deprecated and will be removed in release **2.0.0**. Use
27
+ > [`status` modifier](#status) instead.
28
+
29
+ There are three variants for the Timeline items: `Default`, `Success` and `Critical`.
30
+
31
+ Use `Success` and `Critical` item variants sparingly - only when it's necessary to grab user's attention, for example,
32
+ when the timeline is used for troubleshooting. Otherwise stick to the default styling.
33
+
34
+ ### Links
35
+
36
+ It’s possible to add links to either title or description of a timeline item. To make the timeline item title render as
37
+ a link, pass an object containing the `bento-link` props to the `title` prop. If you want to render a link in your
38
+ description, don't pass the `description` prop but instead use a `default slot`.
39
+
40
+ <Canvas of={TimelineStories.WithLinks} />
41
+
42
+ ### Notes
43
+
44
+ Timeline can be a list of just events, but also a combination of events and notes or just notes, as all of these things
45
+ have a chronological order. To render a note in a `bento-timeline`, use a `bento-timeline-item` component by setting its
46
+ `type` property to `note`. Note that when setting `type` to `note`, you need to pass a `description` and a `timestamp`
47
+ to the timeline item component.
48
+
49
+ <Canvas of={TimelineStories.WithNotes} />
50
+
51
+ #### Adding a note
52
+
53
+ You can choose to let the user manually add notes to the timeline by setting the `add-note` prop to true.
54
+
55
+ Every time the user will confirm they want to add a note, the event `@add-timeline-note` containing the data inputted by
56
+ the user will be emitted. You can listen to this event and add the note item to the timeline the same way provided in
57
+ the code snippet example below.
58
+
59
+ Note that you can manipulate the data before passing it to the timeline item to suit your needs, i.e. if you need to add
60
+ the author name to the timestamp.
61
+
62
+ <Canvas of={TimelineStories.AddNote} />
63
+
64
+ #### Adding timeline items dynamically
65
+
66
+ It's also possible to add timeline items dynamically. For a correct implementation, consult the code snippet below.
67
+
68
+ <Canvas of={TimelineStories.DynamicItemAddition} />
69
+
70
+ ## Modifiers
71
+
72
+ ### Show more
73
+
74
+ When a timeline is really long, it's possible to hide part of it under a `Show X more` button. Teams can specify after
75
+ how many milestones in total additional ones should be hidden, and where the button should be placed.
76
+
77
+ **Possible placements:**
78
+
79
+ - after the most recent item (`AFTER_FIRST`)
80
+ - before the last item (`BEFORE_LAST`)
81
+ - at the bottom (`BOTTOM`)
82
+
83
+ Pressing `Show X more` reveals all the previously hidden items in one go. Revealed items can be hidden again by pressing
84
+ `Show less` button.
85
+
86
+ ### Time gap
87
+
88
+ The timeline component supports automatic gap detection which helps to indicate if a task is taking too much time. If
89
+ the time elapsed between 2 items is greater than the minimum gap, it will automatically insert a time gap indicator. The
90
+ time from which a gap should be added can be configured by the teams.
91
+
92
+ When there’s a time gap in between two items and one of them is hidden, the time gap is also hidden. Once the additional
93
+ milestones are revealed, the time gap is also revealed as the first thing under `Show less` button.
94
+
95
+ ### Status
96
+
97
+ Status will change the color of the timeline icon to its matching color, from `bento-status` component variants, and can
98
+ be used to show progression through statuses (for example, to track what happened with a payment), but also to attract
99
+ attention (for example, to highlight that a document is missing).
100
+
101
+ **Possible values:**
102
+
103
+ - `grey`: renders the icon in the color grey.
104
+ - `blue`: renders the icon in the color blue.
105
+ - `green`: renders the icon in the color green.
106
+ - `yellow`: renders the icon in the color yellow.
107
+ - `orange`: renders the icon in the color orange.
108
+ - `red`: renders the icon in the color red.
109
+
110
+ > **IMPORTANT**: Do not rely on `status` alone to provide context, as this is not accessible. Make sure that the
111
+ > timeline event always clearly states its purpose and that the status icon is merely a visual aid.
112
+
113
+ ## Accessibility
114
+
115
+ The Timeline component is rendered as a ordered list of items.
116
+
117
+ ### Keyboard Interaction
118
+
119
+ The only interactive element in Timeline is the `Show X more` button, which is reachable through keyboard navigation in
120
+ the logical navigation flow of the page. The following keys trigger the following actions:
121
+
122
+ | Keyboard interaction | Action |
123
+ | -------------------- | --------------------------------------------------------------- |
124
+ | `Tab` | Moves focus to the `Show X more` button or away from it. |
125
+ | `Space` | Activates the `Show X more` button, triggering the click event. |
126
+ | `Enter` | Activates the `Show X more` button, triggering the click event. |
127
+
128
+ ## Resources
129
+
130
+ - [Figma link](https://www.figma.com/file/uLabwF3243jdMDsNSP7U9I/Bento---Components?type=design&node-id=13069-168343&mode=design&t=vHGM2jHS68oKH69N-0)
131
+ - [Accessible timelines](https://roodesign.co.uk/articles/2020-10-15-accessible-timelines/)
@@ -0,0 +1 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { onMounted, ref } from 'vue'; import { isVue2 } from 'vue-demi'; import BentoTimeline from './timeline.vue'; import BentoTimelineItem from './components/timeline-item/timeline-item.vue'; import { BentoTimelineGapUnit, BentoTimelineShowMorePlacement } from './timeline.types'; import TimelineDynamicAddition from './__tests__/timeline-dynamic-add-stub.vue?raw'; import { BentoDateFormat } from '@/components/date/date.types'; import BentoLink from '@/components/link/link.vue'; import BentoButton from '@/components/button/button.vue'; const meta: Meta = { title: 'Timeline', component: BentoTimeline, argTypes: { // Slots default: { name: 'default', type: { name: 'string', required: false }, description: 'BentoTimelineItem components', control: 'text', table: { type: { summary: 'VNode[]' }, }, }, }, }; export default meta; type Story = StoryObj<typeof BentoTimeline>; const DEFAULT_PROPS = { timegapLimit: { unit: BentoTimelineGapUnit.DAY, amount: 15 }, showMore: { limit: 3, placement: BentoTimelineShowMorePlacement.AFTER_FIRST }, }; const DEFAULT_ITEM_PROPS = { timestamp: { date: new Date(), value: 'AuthorName', format: BentoDateFormat.FULL_DATE_EXACT_TIME_WITHOUT_PERIOD, }, dataList: [ { label: 'Label', value: 'Value' }, { label: 'Label', value: 'Value' }, ], description: 'Space to put some description about this milestone', }; const { description: _, ...DEFAULT_PROPS_WITHOUT_DESCRIPTION } = DEFAULT_ITEM_PROPS; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoTimeline, BentoTimelineItem }, props: Object.keys(argTypes), template: ` <bento-timeline v-bind="args"> <bento-timeline-item v-bind="milestone5" /> <bento-timeline-item v-bind="milestone4" /> <bento-timeline-item v-bind="milestone3" /> <bento-timeline-item v-bind="milestone2" /> <bento-timeline-item v-bind="milestone1" /> </bento-timeline> `, setup(props) { const milestone5 = { ...DEFAULT_ITEM_PROPS, title: 'Milestone 5', status: 'green', }; const milestone4 = { ...DEFAULT_ITEM_PROPS, title: 'Milestone 4', timestamp: { date: new Date().setDate(-2), value: 'AuthorName', }, }; const milestone3 = { ...DEFAULT_ITEM_PROPS, title: 'Milestone 3', timestamp: { date: new Date().setDate(-4), value: 'AuthorName', }, status: 'red', }; const milestone2 = { ...DEFAULT_ITEM_PROPS, title: 'Milestone 2', timestamp: { date: new Date().setDate(-14), value: 'AuthorName', }, additionalTimestamp: { date: new Date().setDate(-15), value: 'AuthorName', }, }; const milestone1 = { ...DEFAULT_ITEM_PROPS, title: 'Milestone 1', timestamp: { date: new Date().setDate(-150), value: 'AuthorName', }, tag: { label: 'Tag1', }, }; return { args: isVue2 ? props : _args, milestone1, milestone2, milestone3, milestone4, milestone5, }; }, }), args: { ...DEFAULT_PROPS, }, }; export const WithLinks: Story = { render: (_args, { argTypes }) => ({ components: { BentoLink, BentoTimeline, BentoTimelineItem }, props: Object.keys(argTypes), template: ` <bento-timeline v-bind="args"> <bento-timeline-item v-bind="milestone3" /> <bento-timeline-item v-bind="milestone2"> A description with <bento-link to="/">a link</bento-link> </bento-timeline-item> <bento-timeline-item v-bind="milestone1" /> </bento-timeline> `, setup(props) { const milestone3 = { ...DEFAULT_ITEM_PROPS, title: { title: 'Milestone 3', to: '/', }, timestamp: { date: new Date().setDate(-4), value: 'AuthorName', }, status: 'green', }; const milestone2 = { ...DEFAULT_PROPS_WITHOUT_DESCRIPTION, title: 'Milestone 2', timestamp: { date: new Date().setDate(-14), value: 'AuthorName', }, additionalTimestamp: { date: new Date().setDate(-15), value: 'AuthorName', }, }; const milestone1 = { ...DEFAULT_ITEM_PROPS, title: 'Milestone 1', timestamp: { date: new Date().setDate(-150), value: 'AuthorName', }, tag: { label: 'Tag1', }, }; return { args: isVue2 ? props : _args, milestone1, milestone2, milestone3, }; }, }), args: { ...DEFAULT_PROPS, }, }; const withNotesCode = ` <template> <bento-timeline v-bind="args" @add-timeline-note="addNote"> <bento-timeline-item v-for="item in timelineItems" v-bind="item" /> </bento-timeline> </template> <script setup> const milestone3 = { ...DEFAULT_ITEM_PROPS, title: 'Milestone 2', timestamp: { date: new Date().setDate(-4), value: 'AuthorName', }, additionalTimestamp: { date: new Date().setDate(-5), value: 'AuthorName', }, }; const milestone2 = { type: 'note', description: 'Milestone Note. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', timestamp: { date: new Date().setDate(-14), value: 'AuthorName', }, }; const milestone1 = { ...DEFAULT_ITEM_PROPS, title: 'Milestone 1', timestamp: { date: new Date().setDate(-150), value: 'AuthorName', }, tag: { label: 'Tag1', }, }; const timelineItems = ref(); onMounted(() => { timelineItems.value = [milestone3, milestone2, milestone1]; }); const addNote = e => { timelineItems.value.unshift(e); }; return { args: isVue2 ? props : _args, timelineItems, addNote, }; </script> `; export const WithNotes: Story = { render: (_args, { argTypes }) => ({ components: { BentoTimeline, BentoTimelineItem }, props: Object.keys(argTypes), template: ` <bento-timeline v-bind="args"> <bento-timeline-item v-bind="milestone3" /> <bento-timeline-item v-bind="milestone2" /> <bento-timeline-item v-bind="milestone1" /> </bento-timeline> `, setup(props) { const milestone3 = { ...DEFAULT_ITEM_PROPS, title: 'Milestone 2', timestamp: { date: new Date().setDate(-4), value: 'AuthorName', }, additionalTimestamp: { date: new Date().setDate(-5), value: 'AuthorName', }, }; const milestone2 = { type: 'note', description: 'Milestone Note. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', timestamp: { date: new Date().setDate(-14), value: 'AuthorName', }, }; const milestone1 = { ...DEFAULT_ITEM_PROPS, title: 'Milestone 1', timestamp: { date: new Date().setDate(-150), value: 'AuthorName', }, tag: { label: 'Tag1', }, }; return { args: isVue2 ? props : _args, milestone1, milestone2, milestone3, }; }, }), args: { ...DEFAULT_PROPS, showMore: null, }, parameters: storybookDocsParameter(withNotesCode), }; const addNoteCode = ` <template> <bento-timeline v-bind="args" add-note @add-timeline-note="addNote"> <bento-timeline-item v-for="item in timelineItems" v-bind="item" /> </bento-timeline> </template> <script setup> const milestone3 = { ...DEFAULT_ITEM_PROPS, title: 'Milestone 2', timestamp: { date: new Date().setDate(-4), value: 'AuthorName', }, additionalTimestamp: { date: new Date().setDate(-5), value: 'AuthorName', }, }; const milestone2 = { type: 'note', description: 'Milestone Note. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', timestamp: { date: new Date().setDate(-14), value: 'AuthorName', }, }; const milestone1 = { ...DEFAULT_ITEM_PROPS, title: 'Milestone 1', timestamp: { date: new Date().setDate(-150), value: 'AuthorName', }, tag: { label: 'Tag1', }, }; const timelineItems = ref(); onMounted(() => { timelineItems.value = [milestone3, milestone2, milestone1]; }); const addNote = e => { timelineItems.value.unshift(e); }; return { timelineItems, addNote, }; </script> `; export const AddNote: Story = { render: (_args, { argTypes }) => ({ components: { BentoTimeline, BentoTimelineItem }, props: Object.keys(argTypes), template: ` <bento-timeline v-bind="args" add-note @add-timeline-note="addNote"> <bento-timeline-item v-for="item in timelineItems" v-bind="item" /> </bento-timeline> `, setup(props) { const milestone3 = { ...DEFAULT_ITEM_PROPS, title: 'Milestone 2', timestamp: { date: new Date().setDate(-4), value: 'AuthorName', }, additionalTimestamp: { date: new Date().setDate(-5), value: 'AuthorName', }, }; const milestone2 = { type: 'note', description: 'Milestone Note. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', timestamp: { date: new Date().setDate(-14), value: 'AuthorName', }, }; const milestone1 = { ...DEFAULT_ITEM_PROPS, title: 'Milestone 1', timestamp: { date: new Date().setDate(-150), value: 'AuthorName', }, tag: { label: 'Tag1', }, }; const timelineItems = ref(); onMounted(() => { timelineItems.value = [milestone3, milestone2, milestone1]; }); const addNote = e => { const newNote = { ...e, timestamp: { ...e.timestamp, value: 'AuthorName' } }; timelineItems.value.unshift(newNote); }; return { args: isVue2 ? props : _args, timelineItems, addNote, }; }, }), args: { ...DEFAULT_PROPS, showMore: null, addNotes: true, }, parameters: storybookDocsParameter(addNoteCode), }; export const DynamicItemAddition: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoTimeline, BentoTimelineItem }, props: Object.keys(argTypes), template: ` <div style="display: flex; flex-direction: row; align-items: flex-start; gap: 16px;"> <bento-button @click="addItem">Add timeline item</bento-button> <bento-timeline v-if="timelineEntries"> <bento-timeline-item v-for="entry in timelineEntries" :key="entry.title" v-bind="entry" /> </bento-timeline> </div> `, setup(props) { const timelineEntries = ref([]); let index = 0; const addItem = () => { if (index < 5) { timelineEntries.value.unshift(milestones[index]); index++; } }; const milestone5 = { ...DEFAULT_ITEM_PROPS, title: 'Milestone 5', status: 'green', }; const milestone4 = { ...DEFAULT_ITEM_PROPS, title: 'Milestone 4', timestamp: { date: new Date().setDate(-2), value: 'AuthorName', }, }; const milestone3 = { ...DEFAULT_ITEM_PROPS, title: 'Milestone 3', timestamp: { date: new Date().setDate(-4), value: 'AuthorName', }, status: 'red', }; const milestone2 = { ...DEFAULT_ITEM_PROPS, title: 'Milestone 2', timestamp: { date: new Date().setDate(-14), value: 'AuthorName', }, additionalTimestamp: { date: new Date().setDate(-15), value: 'AuthorName', }, }; const milestone1 = { ...DEFAULT_ITEM_PROPS, title: 'Milestone 1', timestamp: { date: new Date().setDate(-150), value: 'AuthorName', }, tag: { label: 'Tag1', }, }; const milestones = [milestone1, milestone2, milestone3, milestone4, milestone5]; return { args: isVue2 ? props : _args, timelineEntries, addItem, }; }, }), args: { ...DEFAULT_PROPS, }, parameters: storybookDocsParameter(TimelineDynamicAddition), };
@@ -0,0 +1 @@
1
+ <template> <div class="b-timeline"> <div v-if="addNote" class="b-timeline__add-note-section"> <bento-button v-if="!showAddNoteTextArea" ref="addNoteButtonRef" variant="tertiary" @click="toggleAddNote" >{{ addNoteLabel }}</bento-button > <div v-if="showAddNoteTextArea"> <bento-textarea ref="addNoteTextareaRef" :label="addNoteLabel" :character-limit="150" resizable="vertical" @update:model-value="updateNoteContent" /> <bento-button-actions :actions="[ { title: addNoteLabel, event: addNoteToTimeline }, { title: cancelLabel, event: toggleAddNote }, ]" /> </div> </div> <ol class="b-timeline__items"> <slot /> </ol> </div> </template> <script setup lang="ts"> import { computed, defineComponent, nextTick, provide, ref, type Ref, toRefs, watch } from 'vue'; import { BentoButton, BentoButtonActions } from '@/components/button'; import { BentoTextarea } from '@/components/textarea'; import { type BentoInternalTimelineRegisterEntry, type BentoTimelineConfigData, type BentoTimelineEntry, type BentoTimelineProps, BentoTimelineShowMorePlacement, } from './timeline.types'; import { TIMELINE_CONFIG_INJECTION_KEY, TIMELINE_REGISTER_ENTRY_INJECTION_KEY } from './timeline.keys'; import messages from './messages.json'; import { useI18n } from '@/utils/ts/i18n'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoTimelineProps>(), { addNote: false, showMore: null, timegapLimit: null, }); const emit = defineEmits(['add-timeline-note']); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const addNoteLabel = computed(() => t('addNote') as string); const cancelLabel = computed(() => t('cancel') as string); const newNoteContent: Ref<string> = ref(); const updateNoteContent = (newTimelineNoteContent: string): void => { newNoteContent.value = newTimelineNoteContent; }; const addNoteTextareaRef: Ref<InstanceType<typeof BentoTextarea>> = ref(null); const addNoteButtonRef: Ref<InstanceType<typeof BentoButton>> = ref(null); const showAddNoteTextArea = ref(false); const toggleAddNote = async () => { showAddNoteTextArea.value = !showAddNoteTextArea.value; await nextTick(); if (showAddNoteTextArea.value) { (addNoteTextareaRef.value.$refs.textareaElement as HTMLTextAreaElement)?.focus(); } else { (addNoteButtonRef.value.$el as HTMLButtonElement)?.focus(); } }; const addNoteToTimeline = () => { emit('add-timeline-note', { description: newNoteContent.value, timestamp: { date: new Date() }, type: 'note', }); toggleAddNote(); }; const timelineEntries = ref<Array<BentoTimelineEntry>>([]); const hiddenItems = computed(() => props.showMore && timelineEntries.value.length > props.showMore.limit ? timelineEntries.value.length - props.showMore.limit : null ); const showAll = ref(false); const { timegapLimit } = toRefs(props); const timelineIndexes = computed(() => Array.from(timelineEntries.value.keys())); const visibleIndexes = computed(() => { if (!props.showMore) { return null; } if (props.showMore.placement === BentoTimelineShowMorePlacement.BEFORE_LAST) { return [ ...timelineIndexes.value.slice(0, props.showMore.limit - 1), timelineIndexes.value[timelineIndexes.value.length - 1], ]; } if (props.showMore.placement === BentoTimelineShowMorePlacement.AFTER_FIRST) { return [timelineIndexes.value[0], ...timelineIndexes.value.slice(-(props.showMore.limit - 1))]; } return timelineIndexes.value.slice(0, props.showMore.limit); }); const showMoreIndex = computed(() => { if (!props.showMore) { return null; } switch (props.showMore.placement) { case BentoTimelineShowMorePlacement.BEFORE_LAST: return props.showMore.limit - 2; case BentoTimelineShowMorePlacement.AFTER_FIRST: return 0; case BentoTimelineShowMorePlacement.BOTTOM: default: return props.showMore.limit - 1; } }); const sortEntries = () => { // Sorts timeline entries based on date in reverse chronological order (newest first). timelineEntries.value.sort((a, b) => { return new Date(b.date).getTime() - new Date(a.date).getTime(); }); }; const unregisterTimelineEntry = (entryToRemove: BentoTimelineEntry) => () => { const index = timelineEntries.value.indexOf(entryToRemove); // The index can be -1 if the entry has already been removed by a parent re-render. if (index > -1) { timelineEntries.value.splice(index, 1); } }; const registerTimelineEntry: BentoInternalTimelineRegisterEntry = entry => { timelineEntries.value.push(entry); return { timelineEntries, unregisterTimelineEntry: unregisterTimelineEntry(entry) }; }; watch( () => timelineEntries.value.length, () => sortEntries() ); const configData: BentoTimelineConfigData = { showAll, showMoreIndex, hiddenItems, visibleIndexes, timegapLimit, timelineEntries, }; // provide register timeline entry and config data timeline items provide(TIMELINE_REGISTER_ENTRY_INJECTION_KEY, registerTimelineEntry); provide(TIMELINE_CONFIG_INJECTION_KEY, configData); </script> <script lang="ts"> /** * Timeline displays a collection of events in a reverse chronological order (newest event is displayed on the top). * * @example * import { BentoDateFormat, BentoTimeline } from '@adyen/bento-vue2'; * * export default { * components: { BentoTimeline }, * template: ` * <bento-timeline> * <bento-timeline-item * title="Milestone 1" * :timestamp="{ date: new Date(), value: 'Created at' }" * :additional-timestamp="{ date: new Date(), value: 'Modified at' }" * description="Milestone description" * :tag="{ label: 'Tag'}" * /> * <bento-timeline-item * title="Milestone 2" * :timestamp="{ * date: new Date(), * value: 'Created at', * format: BentoDateFormat.FULL_DATE_EXACT_TIME_WITHOUT_PERIOD * }" * description="Milestone description" * :data-list="[ * { label: 'Label', value: 'Value' }, * { label: 'Label', value: 'Value' }, * ]" * /> * </bento-timeline> * ` * } */ export default defineComponent({ i18n: { messages }, name: 'bento-timeline', }); </script> <style lang="scss" scoped src="./timeline.scss" />
@@ -0,0 +1 @@
1
+ <template> <div class="b-toast-item"> <output class="b-toast-item__text" role="status"> <bento-typography :title="toast.text" el="span" variant="body" strongest> {{ toast.text }} </bento-typography> </output> <bento-button v-if="toast.action" class="b-toast-item__btn" variant="tertiary" data-testid="toast-item-action-btn" @click="onAction" > {{ toast.action.text }} </bento-button> <bento-button class="b-toast-item__btn" variant="tertiary" data-testid="toast-item-close-btn" @click="toast.dismiss()" > <template #iconLeft> <cross-icon :svg-title="t('dismiss')" /> </template> </bento-button> </div> </template> <script setup lang="ts"> import { type PropType } from 'vue'; import CrossIcon from '@adyen/ui-assets-icons-16/vue/cross-small'; import { useI18n } from '@/utils/ts/i18n'; import { BentoButton } from '@/components/button'; import { BentoTypography } from '@/components/typography'; import type { BentoToastItem } from '@/composables'; import messages from './messages.json'; const props = defineProps({ toast: { type: Object as PropType<BentoToastItem>, required: true }, }); type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const onAction = () => { if (typeof props.toast.action.handler === 'function') { props.toast.action.handler(); } props.toast.dismiss(); }; </script> <script lang="ts"> export default { i18n: { messages }, }; </script> <style lang="scss" scoped src="./toast-item.scss" />
@@ -0,0 +1 @@
1
+ <template> <div ref="shadowStack" class="b-toast-shadow-stack"> <!-- since we only interested in the size of the last 3 toasts and order of the toast items is not guaranteed when we use ref with v-for (more info at the end of this section https://vuejs.org/guide/essentials/template-refs.html#refs-inside-v-for), toast items would be rendered independently with individual ref references. --> <toast-item v-if="toasts[0]" ref="toast0" :toast="toasts[0]" /> <toast-item v-if="toasts[1]" ref="toast1" :toast="toasts[1]" /> <toast-item v-if="toasts[2]" ref="toast2" :toast="toasts[2]" /> </div> </template> <script setup lang="ts"> /** * This component will "shadow-render" ToastItems positioned relatively to each other, * would grab their positions and size and would emit augmented list of toasts with those * additional details. This would allow us to have perfect control over toasts positioning and animation * in the parent Toast component, where ToastItems would actually be visible. */ import { computed, nextTick, onMounted, ref, watch } from 'vue'; import { useBentoToastController } from '@/composables'; import type { BentoToastItemWithDimensions } from './toast-shadow-stack.types'; import ToastItem from '../toast-item/toast-item.vue'; const { stack } = useBentoToastController(); const emit = defineEmits<{ /** * */ (e: 'update', toasts: Array<BentoToastItemWithDimensions>): void; }>(); const shadowStack = ref<HTMLElement>(); const toast0 = ref<InstanceType<typeof ToastItem>>(); const toast1 = ref<InstanceType<typeof ToastItem>>(); const toast2 = ref<InstanceType<typeof ToastItem>>(); const toastRefs = [toast0, toast1, toast2]; // only get 3 last toast as only they would be rendered on the screen const toasts = computed(() => stack.value.slice(0, 3)); const shareToastsWithDimensions = async () => { // Wait for all toasts to render, so we would be able to get their ClientRect await nextTick(); // Parent container "top" value should be 0, but can be something else due to CSS. So have to account for it const { top: parentTop } = shadowStack.value.getBoundingClientRect(); const toastsWithDimensions = stack.value.map((toast, index) => { if (index > 2) { return { ...toast, bottom: 0, height: 0 }; } const { height, top } = toastRefs[index].value.$el.getBoundingClientRect(); return { ...toast, height, bottom: top - parentTop }; }); emit('update', toastsWithDimensions); }; watch(stack, shareToastsWithDimensions); onMounted(() => { // Refs would be null until component is mounted, so let's make sure we only do it after that if (stack.value.length > 0) { shareToastsWithDimensions(); } }); </script> <style lang="scss" scoped src="./toast-shadow-stack.scss" />
@@ -0,0 +1,151 @@
1
+ # Toast
2
+
3
+ Toast is a concise and temporary message that provides feedback on app’s processes after user takes an action, without
4
+ disrupting their flow or requiring them to act on it.
5
+
6
+ ### Behaviour
7
+
8
+ When a user performs an action that triggers a toast message, it appears on the bottom right corner of the screen (or in
9
+ the middle on smaller screens).
10
+
11
+ The message can be immediately dismissed by the user by clicking the `close` icon or action button, but if no action is
12
+ taken, it will disappear on its own after 5 seconds.
13
+
14
+ ### Stacking
15
+
16
+ If there are multiple toast messages triggered at the same time, they stack. The auto-dismiss timer starts only when a
17
+ message is on top of the stack to ensure the user has time to read them all.
18
+
19
+ Hovering over the toasts will pause the auto-dismiss timer.
20
+
21
+ The stack shows a maximum of three toast messages, even if there are more. The rest is queued and will appear when
22
+ another message is dismissed.
23
+
24
+ If user hovers over the stack, if unfolds showing up to last 3 toasts and the rest of the toast stacked (if there are
25
+ more than three).
26
+
27
+ ## Use Cases
28
+
29
+ Due to their temporary nature, use toasts for at-a-glance, non-critical feedback about the app’s processes, i.e. a
30
+ confirmation message that a setting has been saved.
31
+
32
+ ### Do not use
33
+
34
+ For important messages, explanations or errors that require user to take action, use an alert instead.
35
+
36
+ Do not use toast in combination with modal, because modal’s overlay always appears above all the other content and will
37
+ obscure the toast. When you need to provide feedback on actions taken within a modal, use alert instead.
38
+
39
+ ## Usage
40
+
41
+ The `bento-toast` component doesn't accept any props, however its state is managed by a global reactive state. To be
42
+ able to add and display toast messages, you should:
43
+
44
+ - Initialize the **Bento plugin** with the toasts enabled by setting the `withToast` property to `true`
45
+
46
+ ```typescript
47
+ // index.ts
48
+ import Vue from 'vue';
49
+ import BentoVue from '@adyen/bento-vue2';
50
+
51
+ Vue.use(BentoVue, { withToast: true });
52
+ ```
53
+
54
+ - Import and instantiate the `bento-toast` component in the root of your application. The Toast component can be
55
+ anywhere on the app but it should only be included once, therefore it is recommended to have it on the root of your
56
+ app, namely in the `App.vue` file.
57
+
58
+ ```html
59
+ // App.vue
60
+
61
+ <template>
62
+ <div>
63
+ <!-- your code here-->
64
+ <bento-toast />
65
+ </div>
66
+ </template>
67
+
68
+ <script lang="ts">
69
+ import { defineComponent } from 'vue';
70
+ import { BentoToast } from '@adyen/bento-vue2';
71
+
72
+ export default defineComponent({
73
+ components: { BentoToast },
74
+ });
75
+ </script>
76
+ ```
77
+
78
+ - Import the `useBentoToastController` composable in whatever component you might need to trigger a toast message.
79
+ Upon executing, `useBentoToastController()` composable will retrun `addToast()` method which would allow to register
80
+ new toasts messages.
81
+
82
+ ```typescript
83
+ <script lang="ts">
84
+ import { defineComponent } from 'vue';
85
+ import { useBentoToastController } from '@adyen/bento-vue2';
86
+
87
+ export default defineComponent({
88
+ setup() {
89
+ const { addToast } = useBentoToastController();
90
+
91
+ addToast({
92
+ text: 'Toast text',
93
+ });
94
+
95
+ addToast({
96
+ text: 'Toast with action',
97
+ // optional property
98
+ action: {
99
+ text: 'Do action',
100
+ handler: () => {},
101
+ },
102
+ });
103
+
104
+ const { dismiss } = addToast({ text: 'Toast' });
105
+ // Toasts can be programmatically dimissed from the code by calling `dismiss()` function returned from the `addToast()` call
106
+ dismiss();
107
+ }
108
+ })
109
+ <script>
110
+ ```
111
+
112
+ ## Variations
113
+
114
+ There are no variations for this component.
115
+
116
+ ## Modifiers
117
+
118
+ There are no modifiers for this component.
119
+
120
+ ## Accessibility
121
+
122
+ The `bento-toast` component renders in an
123
+ [`aria-live` region](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions) with a `polite`
124
+ value - in practice this means that the screen reader will speak changes whenever the user is idle.
125
+
126
+ Each toast item's message is in turned marked with a role of `status` so that the content of the toast region will be
127
+ announced to the user without interrupting the flow whenever there is an update to its content.
128
+
129
+ ### Keyboard interaction
130
+
131
+ The `dismiss` button of the latest stacked toast element (i.e. the one that is visible to the user) is accessible via
132
+ keyboard navigation. All other toast elements are rendered as `inert` so that they won't be reachable by keyboard
133
+ navigation and will be announced as non-interactive by assistive technology.
134
+
135
+ | Keyboard interaction | Output |
136
+ | -------------------- | -------------------------------------------------------------------- |
137
+ | `Tab` | Moves focus to the visible toast's `dismiss` button or away from it. |
138
+ | `Space` | When focus is on the toast's `dismiss` button, it clicks the button. |
139
+ | `Enter` | When focus is on the toast's `dismiss` button, it clicks the button. |
140
+
141
+ ### Roles, states and properties
142
+
143
+ | ARIA attribute | is automatic? | fallback used | manual override |
144
+ | -------------- | -------------------------------------------- | ------------- | --------------- |
145
+ | `aria-live` | Yes, set to `polite` on the toast region. | - | - |
146
+ | `role` | Yes, set to `status` on each toast element`. | - | - |
147
+
148
+ ## Resources
149
+
150
+ - [Figma link](https://www.figma.com/design/uLabwF3243jdMDsNSP7U9I/Bento---Components?node-id=2342-42474)
151
+ - [WAI-ARIA guidelines](https://www.w3.org/WAI/WCAG22/Techniques/aria/ARIA22)
@@ -0,0 +1 @@
1
+ import { onMounted, ref } from 'vue'; import { action } from '@storybook/addon-actions'; import { BentoButton } from '@/components/button'; import { BentoFormLayout } from '@/components/form-layout'; import { BentoInputField } from '@/components/input-field'; import { BentoCheckbox } from '@/components/checkbox'; import { withToastDecorator } from '@/utils/ts/storybook'; import { useBentoToastController } from '@/composables'; import BentoToast from './toast.vue'; import { isVue2 } from 'vue-demi'; export default { title: 'Toast', component: BentoToast, decorators: [withToastDecorator], args: { onToastAction: action('Toast action button clicked'), }, }; const items = [ { text: 'Toast 1' }, { text: 'Toast 2 with very long title that supppose not to fit in a single line and wrap to the next line, or even further', action: true, }, { text: 'Toast 3' }, { text: 'Toast 4' }, ]; export const Default = (_args, { argTypes }) => ({ components: { BentoButton, BentoFormLayout, BentoInputField, BentoCheckbox }, props: Object.keys(argTypes), template: ` <bento-form-layout form style="max-width: 400px" @submit.prevent.native="onNewToast"> <bento-input-field v-model="text" /> <bento-checkbox v-model="withAction">With action button</bento-checkbox> <template #actions-left> <bento-button type="submit">Add toast</bento-button> </template> </bento-form-layout> `, setup(props) { const { onToastAction } = isVue2 ? props : _args; const { stack, addToast } = useBentoToastController(); let counter = 1; const addNewToast = toast => { const toastNum = counter; addToast({ ...toast, text: `#${toastNum}: ${toast.text}`, action: toast.action ? { text: 'Action', handler: () => onToastAction(`#${toastNum}`) } : null, }); counter += 1; }; onMounted(() => { items.forEach(toast => addNewToast(toast)); }); const text = ref('Toast'); const withAction = ref(false); const onNewToast = () => addNewToast({ text: text.value, action: withAction.value, }); return { onNewToast, text, withAction, stack, }; }, });
@@ -0,0 +1 @@
1
+ <template> <div v-if="renderToast" ref="root" class="b-toast" data-testid="toast" :style="{ height: stackHeight }" @mouseenter="hovered = true" @mouseleave="hovered = false" > <toast-shadow-stack @update="toastsWithDimensions = $event" /> <transition-group name="b-toast__queue" tag="div" aria-live="polite" enter-class="b-toast__queue--enter" leave-to-class="b-toast__queue--leave-to" @before-leave="onBeforeToastLeave" @after-leave="checkHover" > <toast-item v-for="toast in queue" v-bind="{ 'data-dismiss-transform': toast.dismissTransform }" :key="toast.id" :toast="toast" :style="toast.styles" :class="{ 'b-toast__item--first': toast.isFirst }" :inert="toast.isInert" data-testid="toast-item" class="b-toast__item" /> </transition-group> </div> </template> <script setup lang="ts"> import { computed, ref, watch } from 'vue'; import ToastShadowStack from './components/toast-shadow-stack/toast-shadow-stack.vue'; import ToastItem from './components/toast-item/toast-item.vue'; import { useBentoToastController } from '@/composables/use-bento-toast-controller'; import type { BentoToastItemWithDimensions } from './components/toast-shadow-stack/toast-shadow-stack.types'; // By design, top-most toast should be visible for 5 seconds before auto-dismiss const TOAST_DISPLAY_DURATION = 5000; // Delay first render of the toast until we actually need it, to skip all the positioning calculation logic // until we actually have anything to render. const renderToast = ref(false); const { stack } = useBentoToastController(); watch( stack, toastStack => { if (toastStack.length > 0 && !renderToast.value) { renderToast.value = true; } }, { immediate: true } ); const hovered = ref(false); const toastsWithDimensions = ref<Array<BentoToastItemWithDimensions>>([]); // In collapsed mode we need for toast to "stick out", but not overflow another toast in the forefront. // Random number that satisfies this condition (single-line toast is roughly 52px height) const collapsedToastHeight = 40; // How much pixels of the stacked toast will be "sticking out" from the visible toast const stackIndicatorHeight = 10; const queueSize = computed(() => toastsWithDimensions.value.length); const lastVisibleToastIndex = computed(() => Math.min(hovered.value ? 2 : 0, queueSize.value - 1)); const lastVisibleToastBottom = computed(() => { const toast = toastsWithDimensions.value[lastVisibleToastIndex.value]; return toast ? toast.bottom + toast.height : 0; }); const queue = computed(() => { const translateBaseValue = lastVisibleToastBottom.value - collapsedToastHeight; return toastsWithDimensions.value.map((toast, index) => { const collapsed = index > lastVisibleToastIndex.value; const isSmallerStack = !hovered.value && index > 1; const translateY = collapsed ? translateBaseValue + stackIndicatorHeight * (isSmallerStack ? 2 : 1) : toast.bottom; return { ...toast, styles: { height: `${collapsed ? collapsedToastHeight : toast.height}px`, transform: collapsed ? `translateY(${translateY * -1}px) scaleX(${isSmallerStack ? '0.8' : '0.9'})` : `translateY(${translateY * -1}px)`, 'z-index': queueSize.value - index, }, // first toast has different animation, and toasts above index 2/3 are not visible so no need for a custom animation dismissTransform: index > 0 && index < (hovered.value ? 4 : 3) ? `translateY(${(translateY - toast.height) * -1}px) scale(0.5, 0.1)` : null, isFirst: index === 0, isInert: index > (hovered.value ? 2 : 0), }; }); }); const onBeforeToastLeave = (el: HTMLElement) => { // We have to swap transformation on the toast element that is about to be removed // Unfortunately we cannot do it via transition classes as transformation function is // unique per toast, and we cannot use reactivity because it won't be applied for a toast // that is removed. So direct DOM manipulation is the only way it seems. if (el.dataset.dismissTransform) { /* eslint-disable no-param-reassign -- explicitly manipulating with DOM properties, no way around it unfortunately */ el.style.transform = el.dataset.dismissTransform; } }; const stackHeight = computed(() => { // Track stack container height, so that hover over would work without glitches // (when mouse is over space between toasts) let height = 0; if (lastVisibleToastIndex.value >= 0) { const stackIndicator = stackIndicatorHeight * Math.min(queueSize.value - 1 - lastVisibleToastIndex.value, hovered.value ? 1 : 2); height = Math.min(lastVisibleToastBottom.value + stackIndicator, document.body.parentElement.clientHeight); } return `${height}px`; }); let dismissTimout = null; let timeoutStart = null; let remainingTimeToDismiss = TOAST_DISPLAY_DURATION; const startTimer = (toast, duration) => { timeoutStart = Date.now(); dismissTimout = setTimeout(() => toast.dismiss(), duration); }; const startNewDismissTimer = toast => { // stop previous timeouts, if any clearTimeout(dismissTimout); if (toast) { remainingTimeToDismiss = TOAST_DISPLAY_DURATION; if (!hovered.value) { startTimer(toast, remainingTimeToDismiss); } } else { remainingTimeToDismiss = 0; } }; watch( () => queue.value[0], (firstToast, oldFirstToast) => { if (firstToast?.id === oldFirstToast?.id) { return; } startNewDismissTimer(firstToast); }, { immediate: true } ); const pauseDismissTimer = () => { remainingTimeToDismiss = remainingTimeToDismiss - Date.now() + timeoutStart; clearTimeout(dismissTimout); }; const resumeDismissTimer = () => { if (remainingTimeToDismiss > 0) { startTimer(queue.value[0], remainingTimeToDismiss); } }; watch(hovered, isHovered => { if (isHovered) { pauseDismissTimer(); } else { resumeDismissTimer(); } }); const root = ref<HTMLElement>(); // "mouseleave" event would be emitted for the root element when the mouse moves out of the root element, // but browser wouldn't emit this event when root element shrinks to the point when mouse is not over element anymore. // This can happen when user clicks on dismiss button the top-most toast and doesn't move the mouse during animation, // after which browser would think that root element is still hovered. This workaround addresses this particular issue. const checkHover = () => setTimeout(() => { hovered.value = hovered.value ? root.value.matches(':hover') : false; }); </script> <style lang="scss" scoped src="./toast.scss" />
@@ -0,0 +1,81 @@
1
+ import { Canvas, Story, Meta } from '@storybook/blocks';
2
+ import * as ComponentStories from './toggle.stories';
3
+
4
+ <Meta of={ComponentStories} />
5
+
6
+ # Toggle
7
+
8
+ <Canvas of={ComponentStories.Default} />
9
+
10
+ A toggle represents a state. Use a toggle when you want users to quickly switch and save between two states, like
11
+ turning a feature on or off.
12
+
13
+ ## Use Cases
14
+
15
+ Use a toggle if:
16
+
17
+ - You have a binary option that can be either active or inactive (on or off).
18
+ - The user needs to perform instantaneous actions without confirmation.
19
+
20
+ Toggles are mostly are used for settings or filters.
21
+
22
+ ### Do not use
23
+
24
+ Don't use a toggle if the user has to perform additional steps for changes to become effective (e.g. clicking a `Save`
25
+ button)`.
26
+
27
+ ## Variations
28
+
29
+ ### Toggle only
30
+
31
+ It's a toggle without the label or the description text. In this case, it is strongly recommended to provide an
32
+ accessible name through the `aria-label` prop in order to make the component accessible.
33
+
34
+ <Canvas of={ComponentStories.toggleOnly} />
35
+
36
+ ### Label position
37
+
38
+ If a label is passed through the right `slots`, this can be positioned on the left or on the right of the toggle itself.
39
+ To achieve this, one the following values need to be passed to the `labelPosition` prop:
40
+
41
+ - `before`: positions the label to the left of the toggle.
42
+ - `after`: positions the label to the right of the toggle.
43
+
44
+ ## Modifiers
45
+
46
+ ### Disabled
47
+
48
+ Renders the toggle in a disabled state. In practice this makes the component non-interactive and renders a `not-allowed`
49
+ cursor on hover.
50
+
51
+ ### Readonly
52
+
53
+ Renders the toggle in a readonly state. In practice this makes the component non-interactive, but it can still be
54
+ submitted in a form and won't show `not-allowed` cursor.
55
+
56
+ ## Accessibility
57
+
58
+ ### Keyboard interaction
59
+
60
+ The toggle is reachable through keyboard navigation and follows logical navigation flow of the page. A detailed list of
61
+ keyboard interactions is provided in the table below.
62
+
63
+ | Keyboard interaction | Output |
64
+ | -------------------- | ------------------------------------------------------ |
65
+ | `Tab` | Moves focus to the toggle or away from it. |
66
+ | `Space` | When focus is on the toggle, toggles the toggle button |
67
+ | `Enter` | When focus is on the toggle, toggles the toggle button |
68
+
69
+ ### Roles, states and properties
70
+
71
+ | ARIA attribute | is automatic? | fallback used | manual override |
72
+ | ------------------ | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
73
+ | `aria-label` | Yes, corresponds to the default slot text. | If no slot text is present (e.g. toggle only), then a default value of 'toggle' will be passed. | The `aria-label` prop can be passed to provide an accessible name for the toggle. |
74
+ | `role` | Yes, through the semantic `input[type="checkbox"]` element with `role="switch"` set. | - | - |
75
+ | `aria-describedby` | Yes, it corresponds to the description text, if this one is present. | - | - |
76
+ | `aria-checked` | Yes, set when the toggle gets toggled or untoggled through the `input`'s `checked` property. | - | - |
77
+
78
+ ## Resources
79
+
80
+ - [Figma link](https://www.figma.com/file/uLabwF3243jdMDsNSP7U9I/ADL-2.0---Components?node-id=302-1925&t=7Zhgxe1lwQU3XdU1-0)
81
+ - [WAI-ARIA guidelines](https://www.w3.org/WAI/ARIA/apg/patterns/switch/)
@@ -0,0 +1 @@
1
+ import { storybookDocsParameter } from '@/utils/ts/storybook'; import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import BentoToggle from './toggle.vue'; const TOGGLE_LABEL = 'Label'; const TOGGLE_DESCRIPTION = 'Description'; export default { title: 'Toggle', component: BentoToggle, argTypes: { default: { name: 'Default slot', description: 'Toggle label', defaultValue: '', table: { type: { summary: 'VNode[]' }, }, control: 'text', }, description: { name: 'Descriptive sub label slot', description: 'Toggle description', defaultValue: '', table: { type: { summary: 'VNode[]' }, }, control: 'text', }, input: { name: 'input', defaultValue: '', description: 'Input event handler.', table: { type: { summary: '($event) => void' }, }, control: { disable: true }, }, }, }; export const Default = (_args, { argTypes }) => ({ components: { BentoToggle }, props: Object.keys(argTypes), template: ` <bento-toggle v-bind="args" @input="clickedToggleAction"> {{args.default}} <template #description>{{args.description}}</template> </bento-toggle> `, setup(props) { return { clickedToggleAction: action('input'), args: isVue2 ? props : _args, }; }, }); Default.args = { default: TOGGLE_LABEL, description: TOGGLE_DESCRIPTION, disabled: false, }; const defaultCode = ` <bento-toggle @input="clickedToggleAction"> ${TOGGLE_LABEL} <template #description>${TOGGLE_DESCRIPTION}</template> </bento-toggle> `; Default.parameters = storybookDocsParameter(defaultCode); export const toggleOnly = (_args, { argTypes }) => ({ components: { BentoToggle }, props: Object.keys(argTypes), template: ` <bento-toggle v-bind="args" @input="clickedToggleAction" /> `, setup(props) { return { clickedToggleAction: action('input'), args: isVue2 ? props : _args, }; }, }); toggleOnly.args = { disabled: false, }; const toggleOnlyCode = ` <bento-toggle @input="clickedToggleAction" /> `; toggleOnly.parameters = storybookDocsParameter(toggleOnlyCode);
@@ -0,0 +1 @@
1
+ <template> <label class="b-toggle" :class="conditionalClasses" data-testid="label"> <input v-model="toggleInput" :aria-label="computedAriaLabel" :aria-describedby="computedDescription" :disabled="disabled" :aria-readonly="readonly" role="switch" class="b-toggle__input" type="checkbox" @change="$event => onChange($event)" @click="onClick" /> <span class="b-toggle__track" aria-hidden="true"> <span class="b-toggle__handle"> <checkmark-small v-if="toggleInput" /> </span> </span> <span v-if="hasSlot('default')" class="b-toggle__label-container"> <bento-typography v-if="hasSlot('default')" el="span" variant="body" data-testid="inner-label"> <slot></slot> </bento-typography> <bento-typography v-if="hasSlot('default') && hasSlot('description')" :id="descriptionId" el="span" variant="body" class="b-toggle__description" > <slot name="description" /> </bento-typography> </span> </label> </template> <script setup lang="ts"> import { computed, type PropType, ref, toRefs, useAttrs, useSlots, watch } from 'vue'; import CheckmarkSmall from '@adyen/ui-assets-icons-16/vue/checkmark-small'; import { BentoToggleLabelPosition, BentoTogglePropClass } from './toggle.types'; import { BentoTypography } from '@/components/typography'; import { generateUid } from '@/core/utils/ts'; import { useDefaultSlotValidator, useHasSlot } from '@/composables'; import { getSlotText } from '@/utils/ts/get-slot-text/get-slot-text'; import { useAriaLabel } from '@/utils/ts/aria-label'; const slots = useSlots(); const attrs = useAttrs(); const hasSlot = useHasSlot(slots); const props = defineProps({ /** * @deprecated from version 2.0.0. Use the aria-label attribute instead. */ /** * Accessible name for the toggle. It is strongly recommended to pass such value when the toggle is used without a label */ ariaLabel: { type: String, default: undefined, }, /** * Disable the toggle (optional) */ disabled: { type: Boolean, default: false, }, /** * Position of the label relative to the toggle (optional) * @values before, after */ labelPosition: { type: String as PropType<BentoToggleLabelPosition | `${BentoToggleLabelPosition}`>, default: BentoToggleLabelPosition.BEFORE, validator: (value: BentoToggleLabelPosition) => Object.values(BentoToggleLabelPosition).includes(value), }, /** * Setting this property to true will set the toggle to read only state. */ readonly: { type: Boolean, default: false, }, /** * Indicates if the switch is "on" or "off" */ value: { type: Boolean, default: false, }, }); const emit = defineEmits<{ (e: 'input', value: boolean): void; }>(); const toggleInput = ref(props.value); const descriptionId: string = generateUid('toggle-description'); useDefaultSlotValidator('bento-toggle', slots.default, slots.description); const labelSlot = computed((): string | null => (hasSlot('default') ? getSlotText(slots)('default') : null)); const ariaLabelAttribute = computed(() => attrs['aria-label'] as string); const { ariaLabel } = toRefs(props); const computedAriaLabel = useAriaLabel({ ariaLabel: ariaLabelAttribute.value, // TODO: take this line away when the aria-label props is removed label: ariaLabel, defaultFallback: labelSlot, }); const computedDescription = computed((): string | null => descriptionId ?? null); const conditionalClasses = computed((): { [key: string]: boolean } => ({ [`b-toggle--${BentoTogglePropClass.LABEL_FIRST}`]: props.labelPosition === BentoToggleLabelPosition.BEFORE, 'b-toggle--disabled': props.disabled, 'b-toggle--readonly': props.readonly, })); const onChange = (event: Event) => { emit('input', (event.target as HTMLInputElement).checked); }; // readonly attr doesn't work with checkbox, so we need to manually ignore the click instead const onClick = (event: Event) => { if (props.readonly) { event.preventDefault(); } }; watch( () => props.value, newValue => { toggleInput.value = newValue; } ); </script> <script lang="ts"> /** * A wrapper around the bento toggle, providing checkbox functionality with a knob-like toggle * * @usage * import { BentoToggle, BentoLabelPosition } from '@adyen/adl-vue2'; * * export default { * components: { BentoToggle }, * template: ` * <bento-toggle * :value="toggleValue"` * label-position="LabelPosition.BEFORE" * @input="optionalHandler($event)" * > * Label Text * </bento-toggle> * `, * } */ export default { name: 'bento-toggle', }; </script> <style lang="scss" scoped src="./toggle.scss" />
@@ -0,0 +1 @@
1
+ <template> <popper-container :target-element="targetElement" :middleware="middleware" position="top-start" fixed-positioning disabled-focus-trap class="b-tutorial-indicator" data-testid="tutorial-indicator" > <div class="b-tutorial-indicator__highlight" :style="computedStyles"></div> </popper-container> </template> <script setup lang="ts"> import { ref } from 'vue'; import { hide, type Middleware, offset } from '@floating-ui/vue'; import { PopperContainer } from '@/components/popper-container'; const BORDER_OFFSET = 2; defineProps({ /** * Tutorial indicator's target element. * The indicator will position itself around the target element. */ targetElement: { type: PopperContainer.props?.targetElement.type, required: true, }, }); const computedStyles = ref({ width: '0', height: '0', borderRadius: '0', }); function getBorderRadius(element: HTMLElement) { try { const radius = parseFloat(getComputedStyle(element).getPropertyValue('border-radius')); return Number.isNaN(radius) ? '' : `${radius + 2 * BORDER_OFFSET}px`; } catch { return ''; } } const middleware = [ hide(), offset({ mainAxis: BORDER_OFFSET, alignmentAxis: -BORDER_OFFSET, }), { name: 'updateIndicatorSize', fn: ({ x, y, elements }) => { const target = elements.reference as HTMLElement; computedStyles.value.width = `${target.offsetWidth + 4 * BORDER_OFFSET}px`; computedStyles.value.height = `${target.offsetHeight + 4 * BORDER_OFFSET}px`; computedStyles.value.borderRadius = getBorderRadius(target); return { x, y }; }, } as Middleware, ]; </script> <style lang="scss" scoped src="./tutorial-indicator.scss" />