@adyen/bento-mcp 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.md +24 -2
- package/SKILL.md +37 -7
- package/dist/assets/components/accordion/accordion.stories.ts +1 -1
- package/dist/assets/components/accordion/components/accordion-item.stories.ts +1 -1
- package/dist/assets/components/accordion/components/accordion-item.vue +1 -1
- package/dist/assets/components/action-bar/action-bar.docs.mdx +10 -3
- package/dist/assets/components/action-bar/action-bar.internal.types.ts +1 -0
- package/dist/assets/components/action-bar/action-bar.stories.ts +1 -1
- package/dist/assets/components/action-bar/action-bar.types.ts +1 -1
- package/dist/assets/components/action-bar/action-bar.vue +1 -1
- package/dist/assets/components/ai-tag/ai-tag.stories.ts +1 -1
- package/dist/assets/components/ai-tag/ai-tag.vue +1 -1
- package/dist/assets/components/alert/alert.stories.ts +1 -1
- package/dist/assets/components/alert/alert.types.ts +1 -1
- package/dist/assets/components/anchor-scroller/anchor-scroller.stories.ts +1 -1
- package/dist/assets/components/anchor-scroller/anchor-scroller.vue +1 -1
- package/dist/assets/components/avatar/avatar.internal.types.ts +1 -0
- package/dist/assets/components/avatar/avatar.stories.ts +1 -1
- package/dist/assets/components/avatar/avatar.types.ts +1 -1
- package/dist/assets/components/button/button.docs.mdx +4 -3
- package/dist/assets/components/button/button.stories.ts +1 -1
- package/dist/assets/components/button/button.types.ts +1 -1
- package/dist/assets/components/button/components/ai-button/ai-button.stories.ts +1 -1
- package/dist/assets/components/button/components/ai-button/ai-button.vue +1 -1
- package/dist/assets/components/button/components/button-actions/button-actions.docs.mdx +3 -0
- package/dist/assets/components/button/components/button-actions/button-actions.internal.types.ts +1 -0
- package/dist/assets/components/button/components/button-actions/button-actions.stories.ts +1 -1
- package/dist/assets/components/button/components/button-actions/button-actions.types.ts +1 -1
- package/dist/assets/components/button/components/button-actions/button-actions.vue +1 -1
- package/dist/assets/components/button/components/loading-button/loading-button.docs.mdx +26 -23
- package/dist/assets/components/button/components/loading-button/loading-button.stories.ts +1 -1
- package/dist/assets/components/button/components/loading-button/loading-button.vue +1 -1
- package/dist/assets/components/button/components/toggle-button/toggle-button.stories.ts +1 -1
- package/dist/assets/components/card/card.docs.mdx +18 -1
- package/dist/assets/components/card/card.stories.ts +1 -1
- package/dist/assets/components/card/card.types.ts +1 -1
- package/dist/assets/components/card/card.vue +1 -1
- package/dist/assets/components/checkbox/checkbox.stories.ts +1 -1
- package/dist/assets/components/checkbox/checkbox.types.ts +1 -1
- package/dist/assets/components/checkbox/checkbox.vue +1 -1
- package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.docs.mdx +9 -14
- package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.stories.ts +1 -1
- package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.types.ts +1 -1
- package/dist/assets/components/checkbox/components/checkbox-group/checkbox-group.vue +1 -1
- package/dist/assets/components/chip/chip.stories.ts +1 -1
- package/dist/assets/components/chip/chip.vue +1 -1
- package/dist/assets/components/click-outside/click-outside.stories.ts +1 -1
- package/dist/assets/components/code-snippet/code-snippet.types.ts +1 -1
- package/dist/assets/components/code-snippet/code-snippet.vue +1 -1
- package/dist/assets/components/country/country.stories.ts +1 -1
- package/dist/assets/components/country/country.types.ts +1 -1
- package/dist/assets/components/country/country.vue +1 -1
- package/dist/assets/components/currency/currency.stories.ts +1 -1
- package/dist/assets/components/currency/currency.vue +1 -1
- package/dist/assets/components/dashed-underline/dashed-underline.stories.ts +1 -1
- package/dist/assets/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.types.ts +1 -1
- package/dist/assets/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.vue +1 -1
- package/dist/assets/components/data-grid/components/data-grid-config-settings/data-grid-config-settings.vue +1 -1
- package/dist/assets/components/data-grid/components/data-grid-row/data-grid-row.vue +1 -1
- package/dist/assets/components/data-grid/components/data-grid-static-icon/data-grid-static-icon.stories.ts +1 -1
- package/dist/assets/components/data-grid/components/data-grid-summary-cell/data-grid-summary-cell.stories.ts +1 -1
- package/dist/assets/components/data-grid/components/data-grid-summary-cell/data-grid-summary-cell.vue +1 -1
- package/dist/assets/components/data-grid/components/data-grid-tag-with-text-cell/data-grid-tag-with-text-cell.stories.ts +1 -1
- package/dist/assets/components/data-grid/components/data-grid-trend-cell/data-grid-trend-cell.stories.ts +1 -1
- package/dist/assets/components/data-grid/components/data-grid-trend-cell/data-grid-trend-cell.vue +1 -1
- package/dist/assets/components/data-grid/data-grid.docs.mdx +3 -60
- package/dist/assets/components/data-grid/data-grid.stories.ts +1 -1
- package/dist/assets/components/data-grid/data-grid.vue +1 -1
- package/dist/assets/components/date/date.stories.ts +1 -1
- package/dist/assets/components/date-picker/date-picker.stories.ts +1 -1
- package/dist/assets/components/date-picker/date-picker.types.ts +1 -1
- package/dist/assets/components/date-picker/date-picker.vue +1 -1
- package/dist/assets/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.vue +1 -1
- package/dist/assets/components/date-range-picker/components/date-time-input/date-time-input.types.ts +1 -1
- package/dist/assets/components/date-range-picker/components/date-time-input/date-time-input.vue +1 -1
- package/dist/assets/components/date-range-picker/date-range-picker.docs.mdx +31 -6
- package/dist/assets/components/date-range-picker/date-range-picker.stories.ts +1 -1
- package/dist/assets/components/date-range-picker/date-range-picker.types.ts +1 -1
- package/dist/assets/components/date-range-picker/date-range-picker.vue +1 -1
- package/dist/assets/components/date-time-picker/date-time-picker.stories.ts +1 -1
- package/dist/assets/components/date-time-picker/date-time-picker.types.ts +1 -1
- package/dist/assets/components/divider/divider.internal.types.ts +1 -0
- package/dist/assets/components/divider/divider.stories.ts +1 -1
- package/dist/assets/components/divider/divider.types.ts +1 -1
- package/dist/assets/components/divider/divider.vue +1 -1
- package/dist/assets/components/draggable/components/draggable-handle/draggable-handle.stories.ts +1 -1
- package/dist/assets/components/draggable/draggable.stories.ts +1 -1
- package/dist/assets/components/draggable/draggable.vue +1 -1
- package/dist/assets/components/drawer/drawer.stories.ts +1 -1
- package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.internal.types.ts +1 -0
- package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.types.ts +1 -1
- package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-default-textbox/dropdown-default-textbox.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-options-container/dropdown-options-container.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-small-textbox/dropdown-small-textbox.vue +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.stories.ts +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.types.ts +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-avatar/dropdown-avatar.vue +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-country/dropdown-country.stories.ts +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-country/dropdown-country.types.ts +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-country/dropdown-country.vue +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-currency/dropdown-currency.stories.ts +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-currency/dropdown-currency.types.ts +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-currency/dropdown-currency.vue +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.stories.ts +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.types.ts +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-payment-method/dropdown-payment-method.vue +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-tag/dropdown-tag.stories.ts +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-tag/dropdown-tag.types.ts +1 -1
- package/dist/assets/components/dropdown/components/variants/dropdown-tag/dropdown-tag.vue +1 -1
- package/dist/assets/components/dropdown/dropdown.docs.mdx +4 -40
- package/dist/assets/components/dropdown/dropdown.internal.types.ts +1 -0
- package/dist/assets/components/dropdown/dropdown.stories.ts +1 -1
- package/dist/assets/components/dropdown/dropdown.types.ts +1 -1
- package/dist/assets/components/dropdown/dropdown.vue +1 -1
- package/dist/assets/components/empty-state/empty-state.stories.ts +1 -1
- package/dist/assets/components/empty-state/empty-state.types.ts +1 -1
- package/dist/assets/components/file-uploader/components/file-uploader-file-card/file-uploader-file-card.stories.ts +1 -1
- package/dist/assets/components/file-uploader/components/file-uploader-file-card/file-uploader-file-card.types.ts +1 -1
- package/dist/assets/components/file-uploader/components/file-uploader-file-card/file-uploader-file-card.vue +1 -1
- package/dist/assets/components/file-uploader/components/file-uploader-restrictions/file-uploader-restrictions.types.ts +1 -1
- package/dist/assets/components/file-uploader/components/file-uploader-restrictions/file-uploader-restrictions.vue +1 -1
- package/dist/assets/components/file-uploader/file-uploader.internal.types.ts +1 -0
- package/dist/assets/components/file-uploader/file-uploader.stories.ts +1 -1
- package/dist/assets/components/file-uploader/file-uploader.types.ts +1 -1
- package/dist/assets/components/file-uploader/file-uploader.vue +1 -1
- package/dist/assets/components/filter-bar/components/all-filters-modal/all-filters-modal.stories.ts +1 -1
- package/dist/assets/components/filter-bar/components/all-filters-modal/all-filters-modal.types.ts +1 -1
- package/dist/assets/components/filter-bar/components/all-filters-modal/all-filters-modal.vue +1 -1
- package/dist/assets/components/filter-bar/components/all-filters-modal/components/all-filters-modal-filter-selection/all-filters-modal-filter-selection.types.ts +1 -0
- package/dist/assets/components/filter-bar/components/all-filters-modal/components/all-filters-modal-filter-selection/all-filters-modal-filter-selection.vue +1 -0
- package/dist/assets/components/filter-bar/components/all-filters-modal/components/all-filters-modal-selected-filter/all-filters-modal-selected-filter.vue +1 -0
- package/dist/assets/components/filter-bar/components/base-filter/base-filter.internal.types.ts +1 -0
- package/dist/assets/components/filter-bar/components/base-filter/base-filter.types.ts +1 -1
- package/dist/assets/components/filter-bar/components/base-filter/base-filter.vue +1 -1
- package/dist/assets/components/filter-bar/components/boolean-filter/boolean-filter.vue +1 -1
- package/dist/assets/components/filter-bar/components/date-filter/date-filter.vue +1 -1
- package/dist/assets/components/filter-bar/components/date-range-filter/date-range-filter.vue +1 -1
- package/dist/assets/components/filter-bar/components/input-filter/input-filter.types.ts +1 -1
- package/dist/assets/components/filter-bar/components/input-filter/input-filter.vue +1 -1
- package/dist/assets/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.stories.ts +1 -1
- package/dist/assets/components/filter-bar/components/input-with-dropdown-filter/input-with-dropdown-filter.vue +1 -1
- package/dist/assets/components/filter-bar/components/select-filter/components/select-filter-button/select-filter-button.vue +1 -1
- package/dist/assets/components/filter-bar/components/select-filter/select-filter.vue +1 -1
- package/dist/assets/components/filter-bar/components/select-filter/variants/select-avatar-filter/select-avatar-filter.stories.ts +1 -1
- package/dist/assets/components/filter-bar/components/select-filter/variants/select-country-filter/select-country-filter.stories.ts +1 -1
- package/dist/assets/components/filter-bar/components/select-filter/variants/select-country-filter/select-country-filter.vue +1 -1
- package/dist/assets/components/filter-bar/components/toggle-filter/toggle-filter.vue +1 -1
- package/dist/assets/components/filter-bar/filter-bar.docs.mdx +4 -62
- package/dist/assets/components/filter-bar/filter-bar.stories.ts +1 -1
- package/dist/assets/components/filter-bar/filter-bar.types.ts +1 -1
- package/dist/assets/components/filter-bar/filter-bar.vue +1 -1
- package/dist/assets/components/form-layout/form-layout.docs.mdx +0 -23
- package/dist/assets/components/form-layout/form-layout.stories.ts +1 -1
- package/dist/assets/components/header/header.stories.ts +1 -1
- package/dist/assets/components/header/header.types.ts +1 -1
- package/dist/assets/components/header/header.vue +1 -1
- package/dist/assets/components/header-with-views/components/header-with-views-all-views/header-with-views-all-views.vue +1 -1
- package/dist/assets/components/header-with-views/components/header-with-views-item-edit-form/header-with-views-edit-form.types.ts +1 -1
- package/dist/assets/components/header-with-views/components/header-with-views-item-edit-form/header-with-views-edit-form.vue +1 -1
- package/dist/assets/components/header-with-views/header-with-views.internal.types.ts +1 -0
- package/dist/assets/components/header-with-views/header-with-views.stories.ts +1 -1
- package/dist/assets/components/header-with-views/header-with-views.types.ts +1 -1
- package/dist/assets/components/header-with-views/header-with-views.vue +1 -1
- package/dist/assets/components/image/image.stories.ts +1 -1
- package/dist/assets/components/info-icon/info-icon.stories.ts +1 -1
- package/dist/assets/components/input-field/input-field.docs.mdx +1 -13
- package/dist/assets/components/input-field/input-field.stories.ts +1 -1
- package/dist/assets/components/input-field/input-field.types.ts +1 -1
- package/dist/assets/components/input-field/input-field.vue +1 -1
- package/dist/assets/components/input-field-password/input-field-password.internal.types.ts +1 -0
- package/dist/assets/components/input-field-password/input-field-password.stories.ts +1 -1
- package/dist/assets/components/input-field-password/input-field-password.types.ts +1 -1
- package/dist/assets/components/input-field-password/input-field-password.vue +1 -1
- package/dist/assets/components/input-field-phone-number/input-field-phone-number.stories.ts +1 -1
- package/dist/assets/components/input-field-phone-number/input-field-phone-number.types.ts +1 -1
- package/dist/assets/components/input-field-phone-number/input-field-phone-number.vue +1 -1
- package/dist/assets/components/input-time/input-time.docs.mdx +17 -0
- package/dist/assets/components/input-time/input-time.stories.ts +1 -1
- package/dist/assets/components/input-time/input-time.types.ts +1 -1
- package/dist/assets/components/input-time/input-time.vue +1 -1
- package/dist/assets/components/inspector/inspector.stories.ts +1 -1
- package/dist/assets/components/inspector/inspector.vue +1 -1
- package/dist/assets/components/layout/components/layout-row/layout-row.stories.ts +1 -1
- package/dist/assets/components/layout/components/layout-row/layout-row.vue +1 -1
- package/dist/assets/components/layout/layout.docs.mdx +10 -0
- package/dist/assets/components/layout/layout.internal.types.ts +1 -0
- package/dist/assets/components/layout/layout.stories.ts +1 -1
- package/dist/assets/components/layout/layout.types.ts +1 -1
- package/dist/assets/components/link/link.stories.ts +1 -1
- package/dist/assets/components/link/link.types.ts +1 -1
- package/dist/assets/components/link/link.vue +1 -1
- package/dist/assets/components/list/list.stories.ts +1 -1
- package/dist/assets/components/loading-indicator/loading-indicator.stories.ts +1 -1
- package/dist/assets/components/menu/components/menu-item/menu-item.types.ts +1 -1
- package/dist/assets/components/menu/components/menu-item/menu-item.vue +1 -1
- package/dist/assets/components/menu/components/menu-item-list/menu-item-list.vue +1 -1
- package/dist/assets/components/menu/components/sub-menu-item-list/sub-menu-item-list.vue +1 -1
- package/dist/assets/components/menu/menu.docs.mdx +17 -7
- package/dist/assets/components/menu/menu.stories.ts +1 -1
- package/dist/assets/components/menu/menu.types.ts +1 -1
- package/dist/assets/components/menu/menu.vue +1 -1
- package/dist/assets/components/modal/components/base-modal/base-modal.vue +1 -1
- package/dist/assets/components/modal/components/modal-page/modal-page.vue +1 -1
- package/dist/assets/components/modal/modal.internal.types.ts +1 -0
- package/dist/assets/components/modal/modal.stories.ts +1 -1
- package/dist/assets/components/modal/modal.types.ts +1 -1
- package/dist/assets/components/modal/modal.vue +1 -1
- package/dist/assets/components/modal-fullscreen/components/modal-fullscreen-page.vue +1 -1
- package/dist/assets/components/modal-fullscreen/modal-fullscreen.stories.ts +1 -1
- package/dist/assets/components/modal-fullscreen/modal-fullscreen.vue +1 -1
- package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.internal.types.ts +1 -0
- package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.types.ts +1 -1
- package/dist/assets/components/navigation-menu/components/navigation-menu-group/navigation-menu-group.vue +1 -1
- package/dist/assets/components/navigation-menu/components/navigation-menu-item/navigation-menu-item.vue +1 -1
- package/dist/assets/components/navigation-menu/navigation-menu.internal.types.ts +1 -0
- package/dist/assets/components/navigation-menu/navigation-menu.stories.ts +1 -1
- package/dist/assets/components/navigation-menu/navigation-menu.types.ts +1 -1
- package/dist/assets/components/navigation-menu/navigation-menu.vue +1 -1
- package/dist/assets/components/pagination/components/pagination-context/pagination-context.vue +1 -1
- package/dist/assets/components/pagination/components/pagination-results-per-page/pagination-results-per-page.vue +1 -1
- package/dist/assets/components/pagination/pagination.stories.ts +1 -1
- package/dist/assets/components/pagination/pagination.types.ts +1 -1
- package/dist/assets/components/pagination/pagination.vue +1 -1
- package/dist/assets/components/payment-method/payment-method.stories.ts +1 -1
- package/dist/assets/components/payment-method/payment-method.types.ts +1 -1
- package/dist/assets/components/popover/popover.docs.mdx +32 -9
- package/dist/assets/components/popover/popover.stories.ts +1 -1
- package/dist/assets/components/popover/popover.types.ts +1 -1
- package/dist/assets/components/popover/popover.vue +1 -1
- package/dist/assets/components/popper-container/popper-container.types.ts +1 -1
- package/dist/assets/components/popper-container/popper-container.vue +1 -1
- package/dist/assets/components/progress-bar/progress-bar.docs.mdx +102 -0
- package/dist/assets/components/progress-bar/progress-bar.stories.ts +1 -0
- package/dist/assets/components/progress-bar/progress-bar.types.ts +1 -0
- package/dist/assets/components/progress-bar/progress-bar.vue +1 -0
- package/dist/assets/components/promo-banner/promo-banner.stories.ts +1 -1
- package/dist/assets/components/promo-banner/promo-banner.types.ts +1 -1
- package/dist/assets/components/promo-banner/promo-banner.vue +1 -1
- package/dist/assets/components/radio-group/components/radio-button/radio-button.vue +1 -1
- package/dist/assets/components/radio-group/radio-group.docs.mdx +1 -6
- package/dist/assets/components/radio-group/radio-group.stories.ts +1 -1
- package/dist/assets/components/radio-group/radio-group.types.ts +1 -1
- package/dist/assets/components/radio-group/radio-group.vue +1 -1
- package/dist/assets/components/rich-text-editor/rich-text-editor.internal.types.ts +1 -0
- package/dist/assets/components/rich-text-editor/rich-text-editor.stories.ts +1 -1
- package/dist/assets/components/rich-text-editor/rich-text-editor.types.ts +1 -1
- package/dist/assets/components/rich-text-editor/rich-text-editor.vue +1 -1
- package/dist/assets/components/search-bar/search-bar.stories.ts +1 -1
- package/dist/assets/components/search-bar/search-bar.vue +1 -1
- package/dist/assets/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue +1 -1
- package/dist/assets/components/secondary-nav/secondary-nav.stories.ts +1 -1
- package/dist/assets/components/secondary-nav/secondary-nav.vue +1 -1
- package/dist/assets/components/segmented-control/components/segmented-control-button/segmented-control-button.vue +1 -1
- package/dist/assets/components/segmented-control/segmented-control.stories.ts +1 -1
- package/dist/assets/components/segmented-control/segmented-control.types.ts +1 -1
- package/dist/assets/components/segmented-control/segmented-control.vue +1 -1
- package/dist/assets/components/selection-card/components/selection-card-group/selection-card-group.docs.mdx +12 -13
- package/dist/assets/components/selection-card/components/selection-card-group/selection-card-group.stories.ts +1 -1
- package/dist/assets/components/selection-card/components/selection-card-group/selection-card-group.vue +1 -1
- package/dist/assets/components/selection-card/selection-card.stories.ts +1 -1
- package/dist/assets/components/selection-card/selection-card.types.ts +1 -1
- package/dist/assets/components/selection-card/selection-card.vue +1 -1
- package/dist/assets/components/sidepanel/sidepanel.docs.mdx +1 -1
- package/dist/assets/components/sidepanel/sidepanel.stories.ts +1 -1
- package/dist/assets/components/sidepanel/sidepanel.vue +1 -1
- package/dist/assets/components/status/status.stories.ts +1 -1
- package/dist/assets/components/status/status.types.ts +1 -1
- package/dist/assets/components/stepper/components/step/step.vue +1 -1
- package/dist/assets/components/stepper/stepper.stories.ts +1 -1
- package/dist/assets/components/stepper/stepper.vue +1 -1
- package/dist/assets/components/structured-list/structured-list.stories.ts +1 -1
- package/dist/assets/components/structured-list/structured-list.vue +1 -1
- package/dist/assets/components/summary-grid/components/summary-grid-item/summary-grid-item.types.ts +1 -1
- package/dist/assets/components/summary-grid/summary-grid.stories.ts +1 -1
- package/dist/assets/components/summary-grid/summary-grid.types.ts +1 -1
- package/dist/assets/components/table-of-contents/components/table-of-contents-list/table-of-contents-list.vue +1 -1
- package/dist/assets/components/table-of-contents/table-of-contents.internal.types.ts +1 -0
- package/dist/assets/components/table-of-contents/table-of-contents.stories.ts +1 -1
- package/dist/assets/components/table-of-contents/table-of-contents.types.ts +1 -1
- package/dist/assets/components/table-of-contents/table-of-contents.vue +1 -1
- package/dist/assets/components/tabs/components/tab.internal.types.ts +1 -0
- package/dist/assets/components/tabs/components/tab.types.ts +1 -1
- package/dist/assets/components/tabs/components/tab.vue +1 -1
- package/dist/assets/components/tabs/tabs.stories.ts +1 -1
- package/dist/assets/components/tabs/tabs.vue +1 -1
- package/dist/assets/components/tag/tag.docs.mdx +1 -1
- package/dist/assets/components/tag/tag.stories.ts +1 -1
- package/dist/assets/components/tag/tag.types.ts +1 -1
- package/dist/assets/components/tag/tag.vue +1 -1
- package/dist/assets/components/textarea/textarea.docs.mdx +0 -7
- package/dist/assets/components/textarea/textarea.stories.ts +1 -1
- package/dist/assets/components/textarea/textarea.types.ts +1 -1
- package/dist/assets/components/textarea/textarea.vue +1 -1
- package/dist/assets/components/timeline/components/timeline-item/timeline-item.stories.ts +1 -1
- package/dist/assets/components/timeline/components/timeline-item/timeline-item.vue +1 -1
- package/dist/assets/components/timeline/timeline.docs.mdx +0 -10
- package/dist/assets/components/timeline/timeline.internal.types.ts +1 -0
- package/dist/assets/components/timeline/timeline.stories.ts +1 -1
- package/dist/assets/components/timeline/timeline.types.ts +1 -1
- package/dist/assets/components/timeline/timeline.vue +1 -1
- package/dist/assets/components/toast/components/toast-item/toast-item.vue +1 -1
- package/dist/assets/components/toggle/toggle.stories.ts +1 -1
- package/dist/assets/components/toggle/toggle.types.ts +1 -1
- package/dist/assets/components/toggle/toggle.vue +1 -1
- package/dist/assets/components/tutorial/tutorial.stories.ts +1 -1
- package/dist/assets/components/typography/typography.stories.ts +1 -1
- package/dist/assets/components/typography/typography.types.ts +1 -1
- package/dist/assets/components/typography/typography.vue +1 -1
- package/dist/assets/components.json +1 -0
- package/dist/assets/composables/use-bento-base-filter/index.ts +1 -0
- package/dist/assets/composables/use-bento-base-filter/use-bento-base-filter.ts +1 -0
- package/dist/assets/composables/use-bento-base-filter/utils/index.ts +1 -0
- package/dist/assets/composables/use-bento-base-filter/utils/utils.ts +1 -0
- package/dist/assets/composables/use-bento-currency/index.ts +1 -0
- package/dist/assets/composables/use-bento-currency/use-bento-currency.stories.ts +1 -1
- package/dist/assets/composables/use-bento-currency/use-bento-currency.ts +1 -0
- package/dist/assets/composables/use-bento-delayed-hover/index.ts +1 -0
- package/dist/assets/composables/use-bento-delayed-hover/use-bento-delayed-hover.stories.ts +1 -1
- package/dist/assets/composables/use-bento-delayed-hover/use-bento-delayed-hover.ts +1 -0
- package/dist/assets/composables/use-bento-theme/index.ts +1 -0
- package/dist/assets/composables/use-bento-theme/install.ts +1 -1
- package/dist/assets/composables/use-bento-theme/install.types.ts +1 -1
- package/dist/assets/composables/use-bento-theme/use-bento-theme.ts +1 -0
- package/dist/assets/composables/use-bento-toast-controller/index.ts +1 -0
- package/dist/assets/composables/use-bento-toast-controller/toast-stack.ts +1 -0
- package/dist/assets/composables/use-bento-toast-controller/use-bento-toast-controller.ts +1 -0
- package/dist/assets/composables/use-click-outside/index.ts +1 -0
- package/dist/assets/composables/use-click-outside/use-click-outside.stories.ts +1 -1
- package/dist/assets/composables/use-click-outside/use-click-outside.ts +1 -0
- package/dist/assets/composables/use-form-validate/index.ts +1 -0
- package/dist/assets/composables/use-form-validate/use-form.ts +1 -0
- package/dist/assets/composables/use-form-validate/use-form.types.ts +1 -1
- package/dist/assets/composables.json +9 -0
- package/dist/assets/deprecations.md +139 -243
- package/dist/assets/directives/highlight/highlight.stories.ts +1 -1
- package/dist/assets/directives/keyboard-navigation/keyboard-navigation.stories.ts +1 -1
- package/dist/assets/directives/tooltip/tooltip.internal.types.ts +1 -0
- package/dist/assets/directives/tooltip/tooltip.stories.ts +1 -1
- package/dist/assets/directives/tooltip/tooltip.types.ts +1 -1
- package/dist/assets/icons.json +1 -0
- package/dist/assets/index.ts +1 -1
- package/dist/assets/install.ts +1 -1
- package/dist/assets/tailwind/README.md +3 -3
- package/dist/assets/templates/dashboard-template/components/dashboard-section/dashboard-section.stories.ts +1 -1
- package/dist/assets/templates/dashboard-template/dashboard-template.stories.ts +1 -1
- package/dist/assets/templates/data-grid-template/data-grid-template.stories.ts +1 -1
- package/dist/assets/usage.json +81 -80
- package/dist/assets/variables.css +152 -140
- package/dist/main.js +228 -34
- package/package.json +2 -2
- package/dist/assets/components/internal/base-button/base-button.types.ts +0 -1
- package/dist/assets/components/internal/base-button/base-button.vue +0 -1
- package/dist/assets/components/internal/button-actions-with-menu/button-actions-with-menu.stories.ts +0 -1
- package/dist/assets/components/internal/button-actions-with-menu/button-actions-with-menu.types.ts +0 -1
- package/dist/assets/components/internal/button-actions-with-menu/button-actions-with-menu.vue +0 -1
- package/dist/assets/components/internal/calendar/calendar.stories.ts +0 -1
- package/dist/assets/components/internal/calendar/calendar.types.ts +0 -1
- package/dist/assets/components/internal/calendar/calendar.vue +0 -1
- package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.stories.ts +0 -1
- package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.types.ts +0 -1
- package/dist/assets/components/internal/calendar/components/calendar-month/calendar-month.vue +0 -1
- package/dist/assets/components/internal/calendar/components/calendar-month/components/calendar-month-day/__test__/calendar-month-day-stub.vue +0 -1
- package/dist/assets/components/internal/calendar/components/calendar-month/components/calendar-month-day/calendar-month-day.stories.ts +0 -1
- package/dist/assets/components/internal/calendar/components/calendar-month/components/calendar-month-day/calendar-month-day.types.ts +0 -1
- package/dist/assets/components/internal/calendar/components/calendar-month/components/calendar-month-day/calendar-month-day.vue +0 -1
- package/dist/assets/components/internal/calendar/components/calendar-month/composables/granular-min-max-date.types.ts +0 -1
- package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.types.ts +0 -1
- package/dist/assets/components/internal/calendar/components/calendar-year/calendar-year.vue +0 -1
- package/dist/assets/components/internal/calendar/components/calendar-year/components/calendar-year-month/__test__/calendar-year-month-stub.vue +0 -1
- package/dist/assets/components/internal/calendar/components/calendar-year/components/calendar-year-month/calendar-year-month.types.ts +0 -1
- package/dist/assets/components/internal/calendar/components/calendar-year/components/calendar-year-month/calendar-year-month.vue +0 -1
- package/dist/assets/components/internal/calendar/components/calendar-year-selector/calendar-year-selector.types.ts +0 -1
- package/dist/assets/components/internal/calendar/components/calendar-year-selector/calendar-year-selector.vue +0 -1
- package/dist/assets/components/internal/calendar/composables/use-granularity-adjustments.types.ts +0 -1
- package/dist/assets/components/internal/calendar/composables/use-range-pane.types.ts +0 -1
- package/dist/assets/components/internal/checkbox-input/checkbox-input.stories.ts +0 -1
- package/dist/assets/components/internal/checkbox-input/checkbox-input.types.ts +0 -1
- package/dist/assets/components/internal/checkbox-input/checkbox-input.vue +0 -1
- package/dist/assets/components/internal/controls-group/controls-group.vue +0 -1
- package/dist/assets/components/internal/copy/copy.docs.mdx +0 -43
- package/dist/assets/components/internal/copy/copy.stories.ts +0 -1
- package/dist/assets/components/internal/copy/copy.types.ts +0 -1
- package/dist/assets/components/internal/copy/copy.vue +0 -1
- package/dist/assets/components/internal/decorative-checkbox/decorative-checkbox.vue +0 -1
- package/dist/assets/components/internal/dialog-page/dialog-page.types.ts +0 -1
- package/dist/assets/components/internal/dialog-page/dialog-page.vue +0 -1
- package/dist/assets/components/internal/error-message/error-message.vue +0 -1
- package/dist/assets/components/internal/field-label/field-label.vue +0 -1
- package/dist/assets/components/internal/fixed-scroller/fixed-scroller.docs.mdx +0 -21
- package/dist/assets/components/internal/fixed-scroller/fixed-scroller.stories.ts +0 -1
- package/dist/assets/components/internal/fixed-scroller/fixed-scroller.vue +0 -1
- package/dist/assets/components/internal/footer-actions/footer-actions.vue +0 -1
- package/dist/assets/components/internal/grid-layout/grid-layout.types.ts +0 -1
- package/dist/assets/components/internal/grid-layout/grid-layout.vue +0 -1
- package/dist/assets/components/internal/header-meta/header-meta.types.ts +0 -1
- package/dist/assets/components/internal/header-meta/header-meta.vue +0 -1
- package/dist/assets/components/internal/info-icon-with-popover/info-icon-with-popover.vue +0 -1
- package/dist/assets/components/internal/listbox/components/listbox-lazy-load/listbox-lazy-load.types.ts +0 -1
- package/dist/assets/components/internal/listbox/components/listbox-lazy-load/listbox-lazy-load.vue +0 -1
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-category/listbox-multi-select-category.types.ts +0 -1
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-category/listbox-multi-select-category.vue +0 -1
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-nested-lazy-load/listbox-multi-select-nested-lazy-load.types.ts +0 -1
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/components/listbox-multi-select-nested-lazy-load/listbox-multi-select-nested-lazy-load.vue +0 -1
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/listbox-multi-select-options.types.ts +0 -1
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/listbox-multi-select-options.vue +0 -1
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/listbox-multi-select.types.ts +0 -1
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/listbox-multi-select.vue +0 -1
- package/dist/assets/components/internal/listbox/components/listbox-option/listbox-option.vue +0 -1
- package/dist/assets/components/internal/listbox/components/listbox-option-tag/listbox-option-tag.vue +0 -1
- package/dist/assets/components/internal/listbox/components/listbox-single-select/components/listbox-single-select-category/listbox-single-select-category.vue +0 -1
- package/dist/assets/components/internal/listbox/components/listbox-single-select/listbox-single-select.vue +0 -1
- package/dist/assets/components/internal/listbox/components/listbox-single-select-option/listbox-single-select-option.vue +0 -1
- package/dist/assets/components/internal/listbox/listbox.stories.ts +0 -1
- package/dist/assets/components/internal/listbox/listbox.types.ts +0 -1
- package/dist/assets/components/internal/listbox/listbox.vue +0 -1
- package/dist/assets/components/internal/radio-input/radio-input.stories.ts +0 -1
- package/dist/assets/components/internal/radio-input/radio-input.types.ts +0 -1
- package/dist/assets/components/internal/radio-input/radio-input.vue +0 -1
- package/dist/assets/components/internal/teleport/teleport.types.ts +0 -1
- package/dist/assets/components/internal/teleport/teleport.vue +0 -1
- package/dist/assets/components/internal/tooltip/tooltip.test.vue +0 -1
- package/dist/assets/components/internal/tooltip/tooltip.types.ts +0 -1
- package/dist/assets/components/internal/tooltip/tooltip.vue +0 -1
- package/dist/assets/composables/use-form-validate-zod-4/use-form.types.ts +0 -1
- package/dist/assets/composables/use-numeric-input/use-numeric-input.types.ts +0 -1
- package/dist/assets/composables/use-section-navigation-keyboard/use-section-navigation-keyboard.types.ts +0 -1
- package/dist/assets/composables/use-virtual-list/use-virtual-list.types.ts +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <bento-typography class="b-input-field" v-bind="getRootAttributes($attrs)" :data-testid="getDataTestId($attrs)" :class="inputFieldConditionalClasses" el="div" variant="body" > <field-label v-if="hasSlot('default') || label" :for="inputFieldId" data-testid="input-field-label" :tooltip-text="tooltipText" :optional="optional" :required="required" :label="label" @click="focusInput" > <slot></slot> </field-label> <div class="b-input-field__input-box" data-testid="input-box" :aria-disabled="disabled" @click="focusInput"> <!-- Optional Icon for Default text variant --> <span v-if="isDefaultVariant && (hasSlot('defaultIconBefore') || hasSlot('iconBefore'))" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-before" > <slot v-if="hasSlot('iconBefore')" name="iconBefore" /> <slot v-if="hasSlot('defaultIconBefore') && !hasSlot('iconBefore')" name="defaultIconBefore" /> </span> <!-- Mandatory Icon for Payment Method variant --> <span v-if="isPaymentMethodVariant && hasSlot('paymentMethod')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__payment-method" > <slot name="paymentMethod" /> </span> <!-- Dropdown at start --> <div v-if="shouldDisplayDropdownAtStart" :id="dropdownId" class="b-input-field__dropdown b-input-field__dropdown--start" @click.stop > <bento-dropdown v-if="dropdown" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> <!-- Static Value at start --> <bento-typography v-if="shouldDisplayStaticValueAtStart" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--start" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <div class="b-input-field__input-container"> <input :id="inputFieldId" v-bind="$attrs" ref="inputFieldElement" class="b-input-field__input" :aria-label="computedAriaLabel" :aria-describedby="computedAriaDescribedBy" :aria-owns="computedAriaOwns" :aria-invalid="shouldShowError" :placeholder="placeholder" :required="required" :aria-required="required" :type="type" :value="modelValue ?? value" :disabled="disabled" :readonly="isReadOnly" @input="onInput" @change="emit('change')" @focus="onFocus" @blur="onBlur" @keydown="emit('keydown', $event)" @keyup="emit('keyup', $event)" @keydown.esc="emit('escape-pressed')" @click="emit('click', $event)" /> <!-- Hint at the end --> <bento-typography v-if="hint && isFocused && (modelValue || value)" el="span" class="b-input-field__hint" > {{ hint }} </bento-typography> </div> <span v-if="hasSlot('iconAfter')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-after" data-testid="input-text__icon-after" > <slot name="iconAfter" /> </span> <!-- Static Value at the end --> <bento-typography v-if="shouldDisplayStaticValueAtEnd" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--end" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <!-- Dropdown at end --> <div v-if="shouldDisplayDropdownAtEnd" :id="dropdownId" data-testid="input-field-dropdown-container-end" class="b-input-field__dropdown b-input-field__dropdown--end" @click.stop > <bento-dropdown v-if="dropdown" :id="dropdownId" ref="inputDropdownElement" v-bind="inputFieldDropdownProps" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> </div> <div v-if="(shouldShowError && !!errorMessage) || hasSlot('description') || description" class="b-input-field__footer" > <error-message v-if="shouldShowError && !!errorMessage" :id="errorId" :error-message="errorMessage" /> <span v-if="hasSlot('description') || description" :id="descriptionId" class="b-input-field__description" @click="focusInput" > <bento-typography el="span" variant="body"> <slot id="description" name="description"> {{ description }} </slot> </bento-typography> </span> </div> </bento-typography> </template> <script setup lang="ts"> import { computed, inject, provide, type Ref, ref, toRef, toRefs, useAttrs, useSlots } from 'vue'; // Components import { BentoDropdown } from '@/components/dropdown'; import { DROPDOWN_SMALL_SIZE_INJECTION_KEY } from '@/components/dropdown/dropdown.keys'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { generateUid } from '@/core/utils/ts'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { useAriaLabel } from '@/utils/ts/aria-label'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { INPUT_FIELD_HINT_INJECTION_KEY, INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, } from '@/components/input-field/input-field.keys'; import { type BentoListboxSelectedValue } from '@/types/listbox'; // Composables import { useFormLayoutFieldLoading, useHasSlot } from '@/composables'; // Types import { type BentoInputDropdownProps, BentoInputFieldElementPosition, type BentoInputFieldProps, BentoInputFieldStateClass, BentoInputFieldType, type BentoInputFieldValue, BentoInputFieldVariant, } from './input-field.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const attrs = useAttrs(); const slots = useSlots(); const props = withDefaults(defineProps<BentoInputFieldProps>(), { ariaHidden: true, ariaLabel: undefined, condensed: false, description: '', disabled: false, dropdown: undefined, dropdownPosition: BentoInputFieldElementPosition.START, error: false, errorMessage: null, label: '', modelValue: undefined, optional: false, placeholder: '', required: false, readonly: false, slashedZero: false, staticValue: '', staticValuePosition: BentoInputFieldElementPosition.START, tooltipText: null, type: BentoInputFieldType.TEXT, value: undefined, variant: BentoInputFieldVariant.DEFAULT, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the input is clicked */ (e: 'click', event: MouseEvent); /** * 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: 'change'): void; /** * Emitted when the Input Field's internal dropdown changes it's value */ (e: 'dropdown-input', selectedValue: BentoListboxSelectedValue): void; /** * Emitted when the "ESC" key is pressed */ (e: 'escape-pressed'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when a key is pressed and lifted up */ (e: 'keyup', event: KeyboardEvent): void; /** * Emitted when the component's model changes * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', inputValue: string): void; /** * Emitted when the Input Field's internal dropdown changes it's value. Contains every dropdown prop passed with the updated selected value as 'value' */ (e: 'update:dropdown', updatedDropdownProps: BentoInputDropdownProps): void; /** * Emitted when the component's model changes */ (e: 'update:model-value', inputValue: BentoInputFieldValue): void; }>(); const { emitValue } = useFormFieldEmits<BentoInputFieldValue>(emit); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const hasSlot = useHasSlot(slots); const inputFieldElement = ref<HTMLInputElement>(null); const inputDropdownElement = ref(null); const inputFieldId = generateUid('input'); const dropdownId = generateUid('input-dropdown'); const staticValueId = generateUid('input-static-value'); const descriptionId = generateUid('input-description'); const errorId = generateUid('input-error'); const isFocused = ref(false); /** * Gets the overriden or the default data-testid attribute * @param attributes - the live attributes object */ function getDataTestId(attributes: Record<string, unknown>) { return (attributes['data-testid'] as string) ?? 'input-text-container'; } /** * Filters out all aria attributes for the root element. * @param attributes - the live attributes object */ function getRootAttributes(attributes: Record<string, unknown>) { return Object.fromEntries( Object.entries(attributes).filter(([key]) => !key.startsWith('aria-') && key !== 'data-testid') ); } // Provide DROPDOWN_SMALL_SIZE_INJECTION_KEY as true, to set the input only dropdown size variant in the BentoDropdown comp provide(DROPDOWN_SMALL_SIZE_INJECTION_KEY, true); // Renders the input with a hint if a string is provided const hint = inject<Ref<string | undefined>>(INPUT_FIELD_HINT_INJECTION_KEY, ref(undefined)); const onFocus = () => { isFocused.value = true; emit('focus'); }; const onBlur = () => { isFocused.value = false; emit('blur'); }; const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const isDropdownReadOnly = computed<BentoInputDropdownProps['readonly']>( () => isReadOnly.value || props.dropdown?.readonly ); const shouldShowStaticValue = computed( () => props.variant === BentoInputFieldVariant.STATIC_VALUE && !!slots.staticValue ); const isDefaultVariant = computed(() => props.variant === BentoInputFieldVariant.DEFAULT); const isPaymentMethodVariant = computed(() => props.variant === BentoInputFieldVariant.PAYMENT_METHOD); const shouldDisplayStaticValueAtStart = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.START ); const shouldDisplayStaticValueAtEnd = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.END ); const shouldShowDropdown = computed(() => props.variant === BentoInputFieldVariant.DROPDOWN && !!props.dropdown); const shouldDisplayDropdownAtStart = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.START ); const shouldDisplayDropdownAtEnd = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.END ); const shouldShowError = computed( () => !props.disabled && !isReadOnly.value && (!!props.errorMessage || props.error) ); const computedLabel = computed(() => (getSlotText(slots)('default') as string) || props.label); const computedDescription = computed(() => (getSlotText(slots)('description') as string) || props.description); const computedAriaOwns = computed(() => { if (shouldShowDropdown.value === true && shouldShowStaticValue.value === true) { return `${staticValueId} ${dropdownId}`; } if (shouldShowDropdown.value === true) { return `${dropdownId}`; } if (shouldShowStaticValue.value === true) { return `${staticValueId}`; } return null; }); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => attrs?.['aria-labelledby'] ? undefined : t('ariaLabelFallback') ); const computedAriaLabelDropdowFallback = computed(() => t('ariaLabelDropdownFallback', { inputLabel: computedAriaLabel.value }) ); const ariaLabelAttribute = ref(attrs['aria-label']); const { ariaLabel } = 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: computedLabel.value || computedAriaLabelFallbackMessage, }); const computedAriaDescribedBy = computed( () => `${computedDescription.value ? descriptionId : ''} ${ shouldShowError.value && !!props.errorMessage ? errorId : '' }` ); const inputFieldConditionalClasses = computed(() => ({ [`b-input-field--${BentoInputFieldStateClass.CONDENSED}`]: props.condensed, [`b-input-field--${BentoInputFieldStateClass.DISABLED}`]: props.disabled, [`b-input-field--${BentoInputFieldStateClass.READONLY}`]: isReadOnly.value, [`b-input-field--${BentoInputFieldStateClass.ERROR}`]: shouldShowError.value, [`b-input-field--slashed-zero`]: props.slashedZero, })); const inputFieldDropdownProps = computed(() => ({ 'aria-label': computedAriaLabelDropdowFallback.value, ...props.dropdown, })); const onInput = (event: Event) => { const inputValue = (event.target as HTMLInputElement).value; if (!props.disabled && !isReadOnly.value) { emitValue(inputValue); } }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { emit('dropdown-input', selectedValue); emit('update:dropdown', { ...props?.dropdown, value: selectedValue }); }; const focusInput = () => { inputFieldElement.value.focus(); }; const focus = () => { if (props.variant === 'dropdown') { inputDropdownElement.value.focus(); } else { focusInput(); } }; // Expose focus methods for parent components in Vue 3. defineExpose({ /** * Focuses the input field. */ focusInput, /** * Focuses the dropdown when using the dropdown variant; otherwise focuses the input field. */ focus, /** * Reference to the native input element. */ inputFieldElement, }); const inputFieldParentComponent = inject(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, ''); if (!Object.values(BentoInputFieldType).includes(props.type as BentoInputFieldType) && !inputFieldParentComponent) { if (props.type === 'tel') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-phone-number" instead.` ); } else if (props.type === 'password') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-password" instead.` ); } else { printDevelopmentWarning(`"bento-input-field" dropdown does not support the type '${props.type}'`); } } /** * Deprecations */ if (props.error) { deprecate( 'BentoInputField "error" property', `Use the BentoInputField "errorMessage" property instead to indicate that there is a modifier error. Having the field as "undefined" will hide/remove the error. <bento-dropdown errorMessage="Error message text" />`, '2.0.0' ); } if (hasSlot('defaultIconBefore')) { deprecate( 'BentoInputField "defaultIconBefore" slot', `Use the BentoInputField "iconBefore" slot instead to add an icon at the left side of the input field <bento-input-field> \t<template #iconBefore> \t\t<my-icon-goes-here /> \t</template> </bento-input-field>`, '2.0.0' ); } if (props.value) { deprecate( 'BentoInputField "value" property', `The use of "value" prop in "BentoInputField" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field component is used to create interactive controls for web-based * forms in order to accept data from the user. * * @example * import { BentoInputField } from '@adyen/bento-vue2' * * const inputValue = ref(''); * * export default { * components: { BentoInputField }, * tempate: ` * <bento-input-field * :model-value="inputValue" * :@update:model-value="newValue => inputValue.value = newValue" * > * } */ export default { name: 'bento-input-field', inheritAttrs: false, model: { prop: 'modelValue' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./input-field.scss" />
|
|
1
|
+
<template> <bento-typography class="b-input-field" v-bind="getRootAttributes($attrs)" :data-testid="getDataTestId($attrs)" :class="inputFieldConditionalClasses" el="div" variant="body" > <field-label v-if="hasSlot('default') || label" :for="inputFieldId" data-testid="input-field-label" :tooltip-text="tooltipText" :optional="optional" :required="required" :label="label" @click="focusInput" > <slot></slot> </field-label> <div class="b-input-field__input-box" data-testid="input-box" :aria-disabled="disabled" @click="focusInput"> <!-- Optional Icon for Default text variant --> <span v-if="isDefaultVariant && (hasSlot('defaultIconBefore') || hasSlot('iconBefore'))" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-before" > <slot v-if="hasSlot('iconBefore')" name="iconBefore" /> <slot v-if="hasSlot('defaultIconBefore') && !hasSlot('iconBefore')" name="defaultIconBefore" /> </span> <!-- Mandatory Icon for Payment Method variant --> <span v-if="isPaymentMethodVariant && hasSlot('paymentMethod')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__payment-method" > <slot name="paymentMethod" /> </span> <!-- Dropdown at start --> <div v-if="shouldDisplayDropdownAtStart" :id="dropdownId" class="b-input-field__dropdown b-input-field__dropdown--start" @click.stop > <bento-dropdown v-if="dropdown" ref="inputDropdownElement" v-bind="getInputFieldDropdownProps($attrs)" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> <!-- Static Value at start --> <bento-typography v-if="shouldDisplayStaticValueAtStart" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--start" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <div class="b-input-field__input-container"> <input :id="inputFieldId" v-bind="$attrs" ref="inputFieldElement" class="b-input-field__input" :aria-label="getAriaLabel($attrs)" :aria-describedby="computedAriaDescribedBy" :aria-owns="computedAriaOwns" :aria-invalid="shouldShowError" :placeholder="placeholder" :required="required" :aria-required="required" :type="type" :value="modelValue" :disabled="disabled" :readonly="isReadOnly" @input="onInput" @change="emit('change')" @focus="onFocus" @blur="onBlur" @keydown="emit('keydown', $event)" @keyup="emit('keyup', $event)" @keydown.esc="emit('escape-pressed')" @click="emit('click', $event)" /> <!-- Hint at the end --> <bento-typography v-if="hint && isFocused && modelValue" el="span" class="b-input-field__hint"> {{ hint }} </bento-typography> </div> <span v-if="hasSlot('iconAfter')" :aria-hidden="ariaHidden ? 'true' : undefined" class="b-input-field__icon-after" data-testid="input-text__icon-after" > <slot name="iconAfter" /> </span> <!-- Static Value at the end --> <bento-typography v-if="shouldDisplayStaticValueAtEnd" :id="staticValueId" class="b-input-field__static-value b-input-field__static-value--end" el="span" variant="body" > <slot name="staticValue" /> </bento-typography> <!-- Dropdown at end --> <div v-if="shouldDisplayDropdownAtEnd" :id="dropdownId" data-testid="input-field-dropdown-container-end" class="b-input-field__dropdown b-input-field__dropdown--end" @click.stop > <bento-dropdown v-if="dropdown" :id="dropdownId" ref="inputDropdownElement" v-bind="getInputFieldDropdownProps($attrs)" :disabled="disabled" :readonly="isDropdownReadOnly" @update:model-value="onDropdownInput" /> </div> </div> <div v-if="(shouldShowError && !!errorMessage) || hasSlot('description') || description" class="b-input-field__footer" > <error-message v-if="shouldShowError && !!errorMessage" :id="errorId" :error-message="errorMessage" /> <span v-if="hasSlot('description') || description" :id="descriptionId" class="b-input-field__description" @click="focusInput" > <bento-typography el="span" variant="body"> <slot id="description" name="description"> {{ description }} </slot> </bento-typography> </span> </div> </bento-typography> </template> <script setup lang="ts"> import { computed, inject, provide, type Ref, ref, toRef, useSlots } from 'vue'; // Components import { BentoDropdown } from '@/components/dropdown'; import { DROPDOWN_SMALL_SIZE_INJECTION_KEY } from '@/components/dropdown/dropdown.keys'; import { BentoTypography } from '@/components/typography'; import { ErrorMessage, FieldLabel } from '@/internal'; // Utils import { deprecate } from '@/utils/ts/deprecate'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { generateUid } from '@/utils/ts/generate-uid'; import { getSlotText } from '@/utils/ts/get-slot-text'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { INPUT_FIELD_HINT_INJECTION_KEY, INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, } from '@/components/input-field/input-field.keys'; import { type BentoListboxSelectedValue } from '@/types/listbox'; // Composables import { useFormLayoutFieldLoading, useHasSlot } from '@/composables'; // Types import { type BentoInputDropdownProps, BentoInputFieldElementPosition, type BentoInputFieldProps, BentoInputFieldStateClass, BentoInputFieldType, type BentoInputFieldValue, BentoInputFieldVariant, } from './input-field.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const slots = useSlots(); const props = withDefaults(defineProps<BentoInputFieldProps>(), { ariaHidden: true, condensed: false, description: '', disabled: false, dropdown: undefined, dropdownPosition: BentoInputFieldElementPosition.START, errorMessage: null, label: '', modelValue: undefined, optional: false, placeholder: '', required: false, readonly: false, slashedZero: false, staticValue: '', staticValuePosition: BentoInputFieldElementPosition.START, tooltipText: null, type: BentoInputFieldType.TEXT, variant: BentoInputFieldVariant.DEFAULT, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the input is clicked */ (e: 'click', event: MouseEvent); /** * 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: 'change'): void; /** * Emitted when the Input Field's internal dropdown changes it's value */ (e: 'dropdown-input', selectedValue: BentoListboxSelectedValue): void; /** * Emitted when the "ESC" key is pressed */ (e: 'escape-pressed'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when a key is pressed and lifted up */ (e: 'keyup', event: KeyboardEvent): void; /** * Emitted when the Input Field's internal dropdown changes it's value. Contains every dropdown prop passed with the updated selected value as 'value' */ (e: 'update:dropdown', updatedDropdownProps: BentoInputDropdownProps): void; /** * Emitted when the component's model changes */ (e: 'update:model-value', inputValue: BentoInputFieldValue): void; }>(); const { emitValue } = useFormFieldEmits<BentoInputFieldValue>(emit); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const hasSlot = useHasSlot(slots); const inputFieldElement = ref<HTMLInputElement>(null); const inputDropdownElement = ref(null); const inputFieldId = generateUid('input'); const dropdownId = generateUid('input-dropdown'); const staticValueId = generateUid('input-static-value'); const descriptionId = generateUid('input-description'); const errorId = generateUid('input-error'); const isFocused = ref(false); /** * Gets the overriden or the default data-testid attribute * @param attributes - the live attributes object */ function getDataTestId(attributes: Record<string, unknown>) { return (attributes['data-testid'] as string) ?? 'input-text-container'; } /** * Filters out all aria attributes for the root element. * @param attributes - the live attributes object */ function getRootAttributes(attributes: Record<string, unknown>) { return Object.fromEntries( Object.entries(attributes).filter(([key]) => !key.startsWith('aria-') && key !== 'data-testid') ); } // Provide DROPDOWN_SMALL_SIZE_INJECTION_KEY as true, to set the input only dropdown size variant in the BentoDropdown comp provide(DROPDOWN_SMALL_SIZE_INJECTION_KEY, true); // Renders the input with a hint if a string is provided const hint = inject<Ref<string | undefined>>(INPUT_FIELD_HINT_INJECTION_KEY, ref(undefined)); const onFocus = () => { isFocused.value = true; emit('focus'); }; const onBlur = () => { isFocused.value = false; emit('blur'); }; const { isReadOnly } = useFormLayoutFieldLoading(toRef(props, 'readonly')); const isDropdownReadOnly = computed<BentoInputDropdownProps['readonly']>( () => isReadOnly.value || props.dropdown?.readonly ); const shouldShowStaticValue = computed( () => props.variant === BentoInputFieldVariant.STATIC_VALUE && !!slots.staticValue ); const isDefaultVariant = computed(() => props.variant === BentoInputFieldVariant.DEFAULT); const isPaymentMethodVariant = computed(() => props.variant === BentoInputFieldVariant.PAYMENT_METHOD); const shouldDisplayStaticValueAtStart = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.START ); const shouldDisplayStaticValueAtEnd = computed( () => shouldShowStaticValue.value && props.staticValuePosition === BentoInputFieldElementPosition.END ); const shouldShowDropdown = computed(() => props.variant === BentoInputFieldVariant.DROPDOWN && !!props.dropdown); const shouldDisplayDropdownAtStart = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.START ); const shouldDisplayDropdownAtEnd = computed( () => shouldShowDropdown.value && props.dropdownPosition === BentoInputFieldElementPosition.END ); const shouldShowError = computed(() => !props.disabled && !isReadOnly.value && !!props.errorMessage); const computedLabel = computed(() => (getSlotText(slots)('default') as string) || props.label); const computedDescription = computed(() => (getSlotText(slots)('description') as string) || props.description); const computedAriaOwns = computed(() => { if (shouldShowDropdown.value === true && shouldShowStaticValue.value === true) { return `${staticValueId} ${dropdownId}`; } if (shouldShowDropdown.value === true) { return `${dropdownId}`; } if (shouldShowStaticValue.value === true) { return `${staticValueId}`; } return null; }); // vue-i18n issue with t() used in script - direct usage in template works // requires either createI18n({ legacy: false }) or to be wrapped in computed() const computedAriaLabelFallbackMessage = computed(() => t('ariaLabelFallback')); const getAriaLabel = (attributes: Record<string, unknown>) => (attributes['aria-label'] as string | undefined) ?? (attributes['aria-labelledby'] ? undefined : computedLabel.value || computedAriaLabelFallbackMessage.value); const computedAriaDescribedBy = computed( () => `${computedDescription.value ? descriptionId : ''} ${ shouldShowError.value && !!props.errorMessage ? errorId : '' }` ); const inputFieldConditionalClasses = computed(() => ({ [`b-input-field--${BentoInputFieldStateClass.CONDENSED}`]: props.condensed, [`b-input-field--${BentoInputFieldStateClass.DISABLED}`]: props.disabled, [`b-input-field--${BentoInputFieldStateClass.READONLY}`]: isReadOnly.value, [`b-input-field--${BentoInputFieldStateClass.ERROR}`]: shouldShowError.value, [`b-input-field--slashed-zero`]: props.slashedZero, })); const getInputFieldDropdownProps = (attributes: Record<string, unknown>) => ({ 'aria-label': t('ariaLabelDropdownFallback', { inputLabel: getAriaLabel(attributes) }), ...props.dropdown, }); const onInput = (event: Event) => { const inputValue = (event.target as HTMLInputElement).value; if (!props.disabled && !isReadOnly.value) { emitValue(inputValue); } }; const onDropdownInput = (selectedValue: BentoListboxSelectedValue) => { emit('dropdown-input', selectedValue); emit('update:dropdown', { ...props?.dropdown, modelValue: selectedValue }); }; const focusInput = () => { inputFieldElement.value.focus(); }; const focus = () => { if (props.variant === 'dropdown') { inputDropdownElement.value.focus(); } else { focusInput(); } }; // Expose focus methods for parent components in Vue 3. defineExpose({ /** * Focuses the input field. */ focusInput, /** * Focuses the dropdown when using the dropdown variant; otherwise focuses the input field. */ focus, /** * Reference to the native input element. */ inputFieldElement, }); const inputFieldParentComponent = inject(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, ''); if (!Object.values(BentoInputFieldType).includes(props.type as BentoInputFieldType) && !inputFieldParentComponent) { if (props.type === 'tel') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-phone-number" instead.` ); } else if (props.type === 'password') { printDevelopmentWarning( `"bento-input-field" dropdown does not support the type '${props.type}', use "bento-input-field-password" instead.` ); } else { printDevelopmentWarning(`"bento-input-field" dropdown does not support the type '${props.type}'`); } } /** * Deprecations */ if (hasSlot('defaultIconBefore')) { deprecate( 'BentoInputField "defaultIconBefore" slot', `Use the BentoInputField "iconBefore" slot instead to add an icon at the left side of the input field <bento-input-field> \t<template #iconBefore> \t\t<my-icon-goes-here /> \t</template> </bento-input-field>`, '3.0.0' ); } </script> <script lang="ts"> /** * Input field component is used to create interactive controls for web-based * forms in order to accept data from the user. * * @example * import { BentoInputField } from '@adyen/bento-vue2' * * const inputValue = ref(''); * * export default { * components: { BentoInputField }, * tempate: ` * <bento-input-field * :model-value="inputValue" * :@update:model-value="newValue => inputValue.value = newValue" * > * } */ export default { name: 'bento-input-field', inheritAttrs: false, model: { prop: 'modelValue', event: 'update:model-value' }, i18n: { messages }, }; </script> <style lang="scss" scoped src="./input-field.scss" />
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export enum BentoInputFieldPasswordType { TEXT = 'text', PASSWORD = 'password', } export enum BentoInputFieldPasswordDefaultRule { MIN_LENGTH = 'minLength', REQUIRE_LOWERCASE = 'requireLowercase', REQUIRE_NUMERIC = 'requireNumeric', REQUIRE_SPECIAL_CHARACTER = 'requireSpecialCharacter', REQUIRE_UPPERCASE = 'requireUppercase', }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoInputFieldPassword from './input-field-password.vue'; import type { Meta, StoryObj } from '@storybook/vue'; import { minLength } from '@/components/input-field-password/utilities/input-field-password.validation'; import { type BentoInputFieldPasswordValidation } from './input-field-password.types'; const meta: Meta = { title: 'Input Field Password', component: BentoInputFieldPassword, argTypes: { condensed: { table: { type: { summary: 'boolean' }, }, }, disabled: { table: { type: { summary: 'boolean' }, }, }, errorMessage: { table: { type: { summary: 'string' }, }, }, placeholder: { table: { type: { summary: 'string' }, }, }, readonly: { table: { type: { summary: 'boolean' }, }, }, description: { name: 'description', description: 'If set, displays a description text for the input.', table: { category: 'props', type: { summary: 'string' }, }, control: { type: 'text', }, }, 'slot:description': { name: 'description', defaultValue: '', description: 'If set, displays a description text for the input. Overrides the `description` prop when provided.', table: { category: 'slots', type: { summary: 'VNode[]' }, }, control: { type: 'text', }, }, modelValue: { control: { type: 'text' }, table: { type: { summary: 'string | number' }, }, }, label: { table: { type: { summary: 'string' }, }, }, // Events 'input:valid': { table: { type: { summary: '() => void' }, }, control: { disable: true }, },
|
|
1
|
+
import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoInputFieldPassword from './input-field-password.vue'; import type { Meta, StoryObj } from '@storybook/vue'; import { minLength } from '@/components/input-field-password/utilities/input-field-password.validation'; import { type BentoInputFieldPasswordValidation } from './input-field-password.types'; const InputFieldPasswordCustomValidation = "<template>\n <bento-input-field-password\n v-model=\"password\"\n description=\"Enter your password\"\n :error-message=\"errorMessage\"\n label=\"Password\"\n :validation=\"validation\"\n @input:error=\"handleInputError\"\n @input:valid=\"handleInputValid\"\n />\n</template>\n\n<script setup lang=\"ts\">\n import { BentoInputFieldPassword, type BentoInputFieldPasswordValidation, minLength } from '@adyen/bento-vue2';\n import { ref } from 'vue';\n\n const errorMessage = ref('');\n const password = ref('');\n const validation: BentoInputFieldPasswordValidation = {\n extendDefaultValidation: true,\n list: [\n {\n label: 'Always true',\n key: 'alwaysTrue',\n validate: () => true,\n },\n {\n additionalArgs: [18],\n key: 'minLength',\n label: '18 characters',\n validate: minLength,\n },\n {\n disabled: true,\n key: 'requireSpecialCharacter',\n },\n ],\n };\n const handleInputError = () => {\n errorMessage.value = 'Password field has an error';\n };\n const handleInputValid = () => {\n errorMessage.value = '';\n };\n</script>\n"; const InputFieldPasswordDefault = "<template>\n <bento-input-field-password\n v-model=\"password\"\n description=\"Enter your password\"\n :error-message=\"errorMessage\"\n label=\"Password\"\n @input:error=\"handleInputError\"\n @input:valid=\"handleInputValid\"\n />\n</template>\n\n<script setup lang=\"ts\">\n import { BentoInputFieldPassword } from '@adyen/bento-vue2';\n import { ref } from 'vue';\n\n const errorMessage = ref('');\n const password = ref('');\n const handleInputError = () => {\n errorMessage.value = 'Password field has an error';\n };\n const handleInputValid = () => {\n errorMessage.value = '';\n };\n</script>\n"; const meta: Meta = { title: 'Input Field Password', component: BentoInputFieldPassword, argTypes: { condensed: { table: { type: { summary: 'boolean' }, }, }, disabled: { table: { type: { summary: 'boolean' }, }, }, errorMessage: { table: { type: { summary: 'string' }, }, }, placeholder: { table: { type: { summary: 'string' }, }, }, readonly: { table: { type: { summary: 'boolean' }, }, }, description: { name: 'description', description: 'If set, displays a description text for the input.', table: { category: 'props', type: { summary: 'string' }, }, control: { type: 'text', }, }, 'slot:description': { name: 'description', defaultValue: '', description: 'If set, displays a description text for the input. Overrides the `description` prop when provided.', table: { category: 'slots', type: { summary: 'VNode[]' }, }, control: { type: 'text', }, }, modelValue: { control: { type: 'text' }, table: { type: { summary: 'string | number' }, }, }, label: { table: { type: { summary: 'string' }, }, }, // Events 'input:valid': { table: { type: { summary: '() => void' }, }, control: { disable: true }, }, }, }; export default meta; type Story = StoryObj<typeof BentoInputFieldPassword>; const DEFAULT_PROPS = { label: 'Password', withHint: true, condensed: false, disabled: false, readonly: false, description: 'Enter your password', }; const CUSTOM_VALIDATION: BentoInputFieldPasswordValidation = { extendDefaultValidation: true, list: [ { label: 'Always true', key: 'alwaysTrue', validate: () => true, }, { label: '18 characters', key: 'minLength', validate: minLength, additionalArgs: [18], }, { key: 'requireSpecialCharacter', disabled: true, }, ], }; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoInputFieldPassword }, props: Object.keys(argTypes), template: ` <bento-input-field-password v-bind="args" @update:model-value="updateModelValue" @input:valid="inputValidAction" @input:error="inputErrorAction" > <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-input-field-password> `, setup(props) { return { // Values args: isVue2 ? props : _args, // Events updateModelValue: (value: string) => action('update:model-value')(value), inputErrorAction: (value: string) => action('input:error')(value), inputValidAction: action('input:valid'), }; }, }), args: DEFAULT_PROPS, parameters: storybookDocsParameter(InputFieldPasswordDefault), }; export const CustomValidation: Story = { render: (_args, { argTypes }) => ({ components: { BentoInputFieldPassword }, props: Object.keys(argTypes), template: ` <bento-input-field-password v-bind="args" @update:model-value="updateModelValue" @input:valid="inputValidAction" @input:error="inputErrorAction" > <template #description v-if="args['slot:description']">{{ args['slot:description'] }}</template> </bento-input-field-password> `, setup(props) { return { // Values args: isVue2 ? props : _args, // Events updateModelValue: (value: string) => action('update:model-value')(value), inputErrorAction: (value: string) => action('input:error')(value), inputValidAction: action('input:valid'), }; }, }), args: { ...DEFAULT_PROPS, validation: CUSTOM_VALIDATION, }, parameters: storybookDocsParameter(InputFieldPasswordCustomValidation), };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import type { BentoInputFieldProps, BentoInputFieldValue } from '@/components/input-field/input-field.types'; import { type FormField } from '../../types/form-field';
|
|
1
|
+
import type { BentoInputFieldProps, BentoInputFieldValue } from '@/components/input-field/input-field.types'; import { type FormField } from '../../types/form-field'; export interface BentoInputFieldPasswordValidationListItem { label?: string; key?: BentoInputFieldPasswordDefaultRuleKeys | string; // eslint-disable-next-line @typescript-eslint/no-explicit-any validate?: (value: BentoInputFieldValue, ...args: Array<any>) => boolean; // eslint-disable-next-line @typescript-eslint/no-explicit-any additionalArgs?: Array<any>; disabled?: boolean; } export type BentoInputFieldPasswordDefaultRuleKeys = | 'minLength' | 'requireLowercase' | 'requireNumeric' | 'requireSpecialCharacter' | 'requireUppercase'; export type BentoInputFieldPasswordErrorsList = Record<BentoInputFieldPasswordDefaultRuleKeys | string, string>; export interface BentoInputFieldPasswordValidation { title?: string; suggestion?: string; extendDefaultValidation?: boolean; list: Array<BentoInputFieldPasswordValidationListItem>; } export interface BentoInputFieldPasswordProps extends FormField { /** * Reduces height of the input */ condensed?: BentoInputFieldProps['condensed']; /** * The debounce time in milliseconds for running validation and emitting the `input` event after the user has stopped typing. * This helps to avoid excessive validation checks on every keystroke. Defaults to `500`. */ debounceTime?: number; /** * Performs real-time validations over the input value. Default: `false`. * Disabling the option (set to `true`) will not run pre-defined validations or custom validations defined in the `validation` field. */ disableValidation?: boolean; /** * Text displayed as the value of the input-field */ modelValue?: BentoInputFieldProps['modelValue']; /** * Configuration object for password field validation. * * This object defines the validation criteria and associated messages for a password field. * It includes the following properties: * * - `title`: The title displayed at the top of the validation hint. * - `suggestion`: The message displayed at the bottom of the validation hint, providing additional guidance. * - `extendDefaultValidation`: Set to true if you want to add your custom rules to the default validation instead of overriding the list. * - `list`: An array of callback functions to be executed on the value after each user input, validating the password against the specified rules. * * By default, the validation checks that: * * - The value has a minimum length of 12 characters. * - The value contains at least one lowercase letter. * - The value contains at least one uppercase letter. * - The value contains at least one numeric digit. * - The value contains at least one special character. * * */ validation?: BentoInputFieldPasswordValidation; /** * Whether the password validation hint is displayed when the input field is activated via focus or input */ withHint?: boolean; }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div ref="inputFieldPasswordRef" @focusout="onFocusOut"> <bento-input-field ref="inputFieldPasswordInputRef" v-bind="inputFieldArgs" class="b-input-field-password__input" autocapitalize="none" autocorrect="off" spellcheck="false" @blur="onBlur" @focus="onFocus" @update:model-value="onUpdateModelValue" @keydown="checkCapsLock" @keyup="checkCapsLock" > <template v-if="label">{{ label }}</template> <template v-else><slot /></template> <template v-if="description" #description> <slot name="description">{{ description }}</slot> </template> <template v-if="isCapsLockOn" #iconBefore> <div class="b-input-field-password__caps-lock-indicator" @mouseenter="handleCapsLockMouseEnter" @mouseleave="handleCapsLockMouseLeave" > <bento-image ref="inputFieldPasswordCapsLockIconRef" :src="CapsLockIcon" :alt="t('capsLockEnabled')" /> </div> <tooltip v-if="inputFieldPasswordCapsLockIconRef" :class="tooltipConditionalClass" :content="t('capsLockEnabled')" :disabled-focus-trap="true" :fallback-position="['top', 'bottom']" :is-shown="isTooltipDisplayed" :target-element="inputFieldPasswordCapsLockIconRef" /> </template> <template v-if="!disabled" #iconAfter> <bento-button variant="tertiary" @click="toggleInputType" @mousedown.native.prevent> <template #iconLeft> <show-icon v-if="!isPasswordReadable" :svg-title="t('showPassword')" /> <hide-icon v-if="isPasswordReadable" :svg-title="t('hidePassword')" /> </template> </bento-button> </template> </bento-input-field> <bento-popover v-if="hasValidations" :open="isHintDisplayed" :target-element="inputFieldPasswordInputRef" :disable-focus-trap="true" :aria-label="computedValidations.title" :aria-describedby="validationListId" position="right-start" > <div :id="validationPopoverId"> <bento-typography v-if="computedValidations.title" class="b-input-field-password__validation-title"> {{ computedValidations.title }} </bento-typography> <p v-if="!hasErrors" class="b-input-field-password__validation-status--valid" role="alert"> {{ t('validationValid') }} </p> <ul :id="validationListId" class="b-input-field-password__validation-list"> <li v-for="(rule, index) in computedValidationList" :key="index" :class="passwordValidityConditionalClass[index]" aria-live="polite" aria-atomic="true" > <span class="b-input-field-password__validation-list-icon"> <checkmark-circle-fill-icon v-if="passwordValidity[index]" :svg-title="t('validationChecked')" /> <dot-icon v-else :svg-title="t('validationUnchecked')" /> </span> <bento-typography class="b-input-field-password__validation-list-text" el="span"> {{ rule.label }} </bento-typography> </li> </ul> <bento-typography v-if="computedValidations.suggestion" class="b-input-field-password__validation-suggestion" > {{ computedValidations.suggestion }} </bento-typography> </div> </bento-popover> </div> </template> <script setup lang="ts"> import { BentoButton } from '@/components/button'; import { BentoImage } from '@/components/image'; import { BentoInputField } from '@/components/input-field'; import { BentoPopover } from '@/components/popover'; import { BentoTypography } from '@/components/typography'; import { Tooltip } from '@/internal'; import type { BentoInputFieldProps } from '@/components/input-field/input-field.types'; import { BentoInputFieldPasswordDefaultRule, type BentoInputFieldPasswordErrorsList, type BentoInputFieldPasswordProps, BentoInputFieldPasswordType, type BentoInputFieldPasswordValidation, type BentoInputFieldPasswordValidationListItem, } from './input-field-password.types'; import CheckmarkCircleFillIcon from '@adyen/ui-assets-icons-16/vue/checkmark-circle-fill'; import DotIcon from '@adyen/ui-assets-icons-16/vue/dot'; import HideIcon from '@adyen/ui-assets-icons-16/vue/hide'; import ShowIcon from '@adyen/ui-assets-icons-16/vue/show'; import CapsLockIcon from './assets/capslock.svg'; import { watchDebounced } from '@vueuse/core'; import { computed, provide, ref, type SetupContext, useAttrs, watch } from 'vue'; import { useI18n } from '@/utils/ts/i18n'; import { minLength, requireLowercase, requireNumeric, requireSpecialCharacter, requireUppercase, } from '@/components/input-field-password/utilities/input-field-password.validation'; import { generateUid } from '@/core/utils/ts'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { deprecate } from '@/utils/ts/deprecate'; import messages from './messages.json'; import { INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY } from '@/components/input-field/input-field.keys'; type MessageSchema = (typeof messages)['en-US']; const { t, n } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoInputFieldPasswordProps>(), { condensed: false, debounceTime: DEBOUNCE_DURATION, description: null, disabled: false, disableValidation: false, errorMessage: null, label: null, optional: false, placeholder: '', readonly: false, required: false, tooltipText: null, validation: undefined, value: undefined, modelValue: undefined, withHint: true, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emits an `input:valid` event when the value is validated against the provided validations. * This emit has a debounce and is emitted after an `input` event. */ (e: 'input:valid'): void; /** * Emits an `input:error` event when the value fails the validation. * This emit has a debounce and is emitted after an `input` event. */ (e: 'input:error', errors: BentoInputFieldPasswordErrorsList): void; /** * Emits an `input` event whenever the value in the input is changed. * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', value: BentoInputFieldPasswordProps['value']): void; /** * Emits an `update:model-value` event whenever the value in the input is changed. */ (e: 'update:model-value', value: BentoInputFieldPasswordProps['modelValue']): void; }>(); const attrs = useAttrs(); const inputType = ref(BentoInputFieldPasswordType.PASSWORD); const inputValue = ref(props.modelValue ?? props.value); const isHintDisplayed = ref(false); const isTouched = ref(false); const passwordValidity = ref<Array<boolean>>([]); const errorsList = ref<BentoInputFieldPasswordErrorsList>({}); const capsLockTooltipTimeoutID = ref<ReturnType<typeof setTimeout>>(null); const isTooltipDisplayed = ref(false); const inputFieldPasswordRef = ref(null); const inputFieldPasswordInputRef = ref(null); const inputFieldPasswordCapsLockIconRef = ref(null); const isCapsLockOn = ref(false); provide(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, 'bento-input-field-password'); const onFocusOut = (event: FocusEvent) => { const container = inputFieldPasswordRef.value as HTMLElement | null; // If focus moved to another element still inside the container, keep the popover open. if (container && event.relatedTarget instanceof Node && container.contains(event.relatedTarget)) { return; } isTouched.value = true; if (!props.disableValidation) { if (props.withHint) { isHintDisplayed.value = false; } checkValidity(inputValue.value); } }; const { emitValue } = useFormFieldEmits<BentoInputFieldPasswordProps['modelValue']>(emit); const validationListId = generateUid('validationListId'); const validationPopoverId = generateUid('validationPopoverId'); const isPasswordReadable = computed(() => inputType.value === BentoInputFieldPasswordType.TEXT); const passwordValidityConditionalClass = computed(() => passwordValidity.value.map(validity => validity ? 'b-input-field-password__validation-list--valid' : 'b-input-field-password__validation-list--invalid' ) ); const tooltipConditionalClass = computed(() => capsLockTooltipTimeoutID.value ? 'b-input-field-password__input-tooltip--timed' : '' ); const inputFieldArgs = computed<BentoInputFieldProps & SetupContext['attrs']>(() => ({ ariaHidden: false, condensed: props.condensed, disabled: props.disabled, errorMessage: props.errorMessage, optional: props.optional, placeholder: props.placeholder, readonly: props.readonly, required: props.required, tooltipText: props.tooltipText, type: inputType.value, modelValue: inputValue.value, 'aria-haspopup': hasValidations.value ? 'dialog' : false, 'aria-describedby': hasValidations.value ? validationPopoverId : '', ...attrs, })); const computedValidations = computed<BentoInputFieldPasswordValidation>(() => { if (props.validation && !props.validation.extendDefaultValidation) { return props.validation; } const defaultValidationList = [ { label: t('validationMinLength', { numberOfCharacters: n(MIN_PASSWORD_LENGTH) }), key: BentoInputFieldPasswordDefaultRule.MIN_LENGTH, validate: minLength, }, { label: t('validationRequireLowercase', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_LOWERCASE, validate: requireLowercase, }, { label: t('validationRequireUppercase', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_UPPERCASE, validate: requireUppercase, }, { label: t('validationRequireNumeric', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_NUMERIC, validate: requireNumeric, }, { label: t('validationRequireSpecialCharacter', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_SPECIAL_CHARACTER, validate: requireSpecialCharacter, }, ]; let validationList: Array<BentoInputFieldPasswordValidationListItem> = defaultValidationList; if (props.validation) { const defaultValidationKeys: Array<string> = defaultValidationList.map(rule => rule.key); const customRuleKeys = props.validation?.list?.map(rule => rule.key); // remove from the custom list any rules with default keys (rules that are overriding existent rules) const customValidationList = props.validation?.list?.filter(rule => !defaultValidationKeys.includes(rule.key)) ?? []; // loop through the default list and replace default rule with the custom rule if it exists const validationListWithOverrides = defaultValidationList.map(rule => { if (customRuleKeys?.includes(rule.key)) { const customRule = props.validation?.list?.find(customRule => customRule.key === rule.key); return customRule; } else { return rule; } }); validationList = [...validationListWithOverrides, ...customValidationList]; } return { title: props.validation?.title ?? t('validationTitle'), suggestion: props.validation?.suggestion ?? t('validationSuggestion'), list: validationList, }; }); const computedValidationList = computed(() => computedValidations.value?.list?.filter(rule => !rule.disabled)); const hasErrors = computed(() => passwordValidity.value?.some(validation => !validation)); const hasValidations = computed( () => inputFieldPasswordInputRef.value && computedValidations.value?.list?.length > 0 ); const checkCapsLock = (e: KeyboardEvent) => { // Check if getModifierState exists and is a function in case the user's browser is autofilling if (typeof e.getModifierState === 'function') { isCapsLockOn.value = e.getModifierState('CapsLock'); } }; const toggleInputType = () => { if (isPasswordReadable.value) { inputType.value = BentoInputFieldPasswordType.PASSWORD; } else { inputType.value = BentoInputFieldPasswordType.TEXT; } }; const onUpdateModelValue = (value: string) => { if (props.disabled) { return; } inputValue.value = value; emitValue(inputValue.value); }; const onFocus = () => { emit('focus'); if (!props.disableValidation) { if (props.withHint) { isHintDisplayed.value = true; } checkValidity(inputValue.value); } }; const onBlur = () => { emit('blur'); }; const checkValidity = (value: BentoInputFieldPasswordProps['value']) => { const rulesValidityObj = computedValidationList.value?.map(validationItem => ({ key: validationItem.key, label: validationItem.label, isValid: validationItem.validate(value, ...(validationItem.additionalArgs ?? [])), })); let customRuleIndex = 1; passwordValidity.value = rulesValidityObj?.map(error => error.isValid); errorsList.value = rulesValidityObj ?.filter(rule => !rule.isValid) .reduce((errorsObject, error) => { const key = error.key ?? `custom-rule-${customRuleIndex++}`; return { ...errorsObject, [key]: error.label, }; }, {}); }; const handleCapsLockMouseLeave = () => { isTooltipDisplayed.value = false; capsLockTooltipTimeoutID.value = null; }; const handleCapsLockMouseEnter = () => { isTooltipDisplayed.value = true; }; watch( () => [props.value, props.modelValue], () => { inputValue.value = props.modelValue ?? props.value; } ); watch(isCapsLockOn, isCapsLockOn => { if (!isCapsLockOn) { clearTimeout(capsLockTooltipTimeoutID.value); capsLockTooltipTimeoutID.value = null; isTooltipDisplayed.value = false; } else { isTooltipDisplayed.value = true; capsLockTooltipTimeoutID.value = setTimeout(() => { capsLockTooltipTimeoutID.value = null; isTooltipDisplayed.value = false; }, TOOLTIP_TIMEOUT); } }); watchDebounced( inputValue, inputValue => { checkValidity(inputValue); if (!hasErrors.value) { emit('input:valid'); } else { emit('input:error', errorsList.value); } }, // wait for 0.5s before checking validity OR 2s of continuous typing { debounce: props.debounceTime, maxWait: DEBOUNCE_MAX_WAIT } ); if (props.value) { deprecate( 'BentoInputFieldPassword "value" property', `The use of "value" prop in "BentoInputFieldPassword" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> const MIN_PASSWORD_LENGTH = 12; const DEBOUNCE_DURATION = 500; const DEBOUNCE_MAX_WAIT = 2000; const TOOLTIP_TIMEOUT = 2000; /** * The input field password is a component designed to support users' preference for entering values securely. * This is an extension of the `bento-input-field` component. * * @example * import { BentoInputFieldPassword } from '@adyen/bento-vue2'; * * export default { * components: { BentoInputFieldPassword }, * template: ` * <bento-input-field-password * label="Label" * error-message="Error" * :validation="{ * extendDefaultValidation: true, * list: [ * { label: '18 characters', key: 'minLength', validate: minLength, additionalArgs: [18] }, * { label: 'Always true', key: 'alwaysTrue', validate: () => true }, * { key: 'requireSpecialCharacter', disabled: true } * ], * }" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-input-field-password', model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./input-field-password.scss" />
|
|
1
|
+
<template> <div ref="inputFieldPasswordRef" @focusout="onFocusOut"> <bento-input-field ref="inputFieldPasswordInputRef" v-bind="inputFieldArgs" class="b-input-field-password__input" autocapitalize="none" autocorrect="off" spellcheck="false" @blur="onBlur" @focus="onFocus" @update:model-value="onUpdateModelValue" @keydown="checkCapsLock" @keyup="checkCapsLock" > <template v-if="label">{{ label }}</template> <template v-else><slot /></template> <template v-if="description" #description> <slot name="description">{{ description }}</slot> </template> <template v-if="isCapsLockOn" #iconBefore> <div class="b-input-field-password__caps-lock-indicator" @mouseenter="handleCapsLockMouseEnter" @mouseleave="handleCapsLockMouseLeave" > <bento-image ref="inputFieldPasswordCapsLockIconRef" :src="CapsLockIcon" :alt="t('capsLockEnabled')" /> </div> <tooltip v-if="inputFieldPasswordCapsLockIconRef" :class="tooltipConditionalClass" :content="t('capsLockEnabled')" :disabled-focus-trap="true" :fallback-position="['top', 'bottom']" :is-shown="isTooltipDisplayed" :target-element="inputFieldPasswordCapsLockIconRef" /> </template> <template v-if="!disabled" #iconAfter> <bento-button variant="tertiary" @click="toggleInputType" @mousedown.native.prevent> <template #iconLeft> <show-icon v-if="!isPasswordReadable" :svg-title="t('showPassword')" /> <hide-icon v-if="isPasswordReadable" :svg-title="t('hidePassword')" /> </template> </bento-button> </template> </bento-input-field> <bento-popover v-if="hasValidations" :open="isHintDisplayed" :target-element="inputFieldPasswordInputRef" :disable-focus-trap="true" :aria-label="computedValidations.title" :aria-describedby="validationListId" position="right-start" > <div :id="validationPopoverId"> <bento-typography v-if="computedValidations.title" class="b-input-field-password__validation-title"> {{ computedValidations.title }} </bento-typography> <p v-if="!hasErrors" class="b-input-field-password__validation-status--valid" role="alert"> {{ t('validationValid') }} </p> <ul :id="validationListId" class="b-input-field-password__validation-list"> <li v-for="(rule, index) in computedValidationList" :key="index" :class="passwordValidityConditionalClass[index]" aria-live="polite" aria-atomic="true" > <span class="b-input-field-password__validation-list-icon"> <checkmark-circle-fill-icon v-if="passwordValidity[index]" :svg-title="t('validationChecked')" /> <dot-icon v-else :svg-title="t('validationUnchecked')" /> </span> <bento-typography class="b-input-field-password__validation-list-text" el="span"> {{ rule.label }} </bento-typography> </li> </ul> <bento-typography v-if="computedValidations.suggestion" class="b-input-field-password__validation-suggestion" > {{ computedValidations.suggestion }} </bento-typography> </div> </bento-popover> </div> </template> <script setup lang="ts"> import { BentoButton } from '@/components/button'; import { BentoImage } from '@/components/image'; import { BentoInputField } from '@/components/input-field'; import { BentoPopover } from '@/components/popover'; import { BentoTypography } from '@/components/typography'; import { Tooltip } from '@/internal'; import type { BentoInputFieldProps } from '@/components/input-field/input-field.types'; import { BentoInputFieldPasswordDefaultRule, BentoInputFieldPasswordType, } from './input-field-password.internal.types'; import { type BentoInputFieldPasswordErrorsList, type BentoInputFieldPasswordProps, type BentoInputFieldPasswordValidation, type BentoInputFieldPasswordValidationListItem, } from './input-field-password.types'; import CheckmarkCircleFillIcon from '@adyen/ui-assets-icons-16/vue/checkmark-circle-fill'; import DotIcon from '@adyen/ui-assets-icons-16/vue/dot'; import HideIcon from '@adyen/ui-assets-icons-16/vue/hide'; import ShowIcon from '@adyen/ui-assets-icons-16/vue/show'; import CapsLockIcon from './assets/capslock.svg'; import { watchDebounced } from '@vueuse/core'; import { computed, provide, ref, type SetupContext, useAttrs, watch } from 'vue'; import { useI18n } from '@/utils/ts/i18n'; import { minLength, requireLowercase, requireNumeric, requireSpecialCharacter, requireUppercase, } from '@/components/input-field-password/utilities/input-field-password.validation'; import { generateUid } from '@/utils/ts/generate-uid'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import messages from './messages.json'; import { INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY } from '@/components/input-field/input-field.keys'; type MessageSchema = (typeof messages)['en-US']; const { t, n } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoInputFieldPasswordProps>(), { condensed: false, debounceTime: DEBOUNCE_DURATION, description: null, disabled: false, disableValidation: false, errorMessage: null, label: null, optional: false, placeholder: '', readonly: false, required: false, tooltipText: null, validation: undefined, modelValue: undefined, withHint: true, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the element has received focus */ (e: 'focus'): void; /** * Emits an `input:valid` event when the value is validated against the provided validations. * This emit has a debounce and is emitted after an `input` event. */ (e: 'input:valid'): void; /** * Emits an `input:error` event when the value fails the validation. * This emit has a debounce and is emitted after an `input` event. */ (e: 'input:error', errors: BentoInputFieldPasswordErrorsList): void; /** * Emits an `update:model-value` event whenever the value in the input is changed. */ (e: 'update:model-value', value: BentoInputFieldPasswordProps['modelValue']): void; }>(); const attrs = useAttrs(); const inputType = ref(BentoInputFieldPasswordType.PASSWORD); const inputValue = ref(props.modelValue); const isHintDisplayed = ref(false); const isTouched = ref(false); const passwordValidity = ref<Array<boolean>>([]); const errorsList = ref<BentoInputFieldPasswordErrorsList>({}); const capsLockTooltipTimeoutID = ref<ReturnType<typeof setTimeout>>(null); const isTooltipDisplayed = ref(false); const inputFieldPasswordRef = ref(null); const inputFieldPasswordInputRef = ref(null); const inputFieldPasswordCapsLockIconRef = ref(null); const isCapsLockOn = ref(false); provide(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, 'bento-input-field-password'); const onFocusOut = (event: FocusEvent) => { const container = inputFieldPasswordRef.value as HTMLElement | null; // If focus moved to another element still inside the container, keep the popover open. if (container && event.relatedTarget instanceof Node && container.contains(event.relatedTarget)) { return; } isTouched.value = true; if (!props.disableValidation) { if (props.withHint) { isHintDisplayed.value = false; } checkValidity(inputValue.value); } }; const { emitValue } = useFormFieldEmits<BentoInputFieldPasswordProps['modelValue']>(emit); const validationListId = generateUid('validationListId'); const validationPopoverId = generateUid('validationPopoverId'); const isPasswordReadable = computed(() => inputType.value === BentoInputFieldPasswordType.TEXT); const passwordValidityConditionalClass = computed(() => passwordValidity.value.map(validity => validity ? 'b-input-field-password__validation-list--valid' : 'b-input-field-password__validation-list--invalid' ) ); const tooltipConditionalClass = computed(() => capsLockTooltipTimeoutID.value ? 'b-input-field-password__input-tooltip--timed' : '' ); const inputFieldArgs = computed<BentoInputFieldProps & SetupContext['attrs']>(() => ({ ariaHidden: false, condensed: props.condensed, disabled: props.disabled, errorMessage: props.errorMessage, optional: props.optional, placeholder: props.placeholder, readonly: props.readonly, required: props.required, tooltipText: props.tooltipText, type: inputType.value, modelValue: inputValue.value, 'aria-haspopup': hasValidations.value ? 'dialog' : false, 'aria-describedby': hasValidations.value ? validationPopoverId : '', ...attrs, })); const computedValidations = computed<BentoInputFieldPasswordValidation>(() => { if (props.validation && !props.validation.extendDefaultValidation) { return props.validation; } const defaultValidationList = [ { label: t('validationMinLength', { numberOfCharacters: n(MIN_PASSWORD_LENGTH) }), key: BentoInputFieldPasswordDefaultRule.MIN_LENGTH, validate: minLength, }, { label: t('validationRequireLowercase', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_LOWERCASE, validate: requireLowercase, }, { label: t('validationRequireUppercase', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_UPPERCASE, validate: requireUppercase, }, { label: t('validationRequireNumeric', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_NUMERIC, validate: requireNumeric, }, { label: t('validationRequireSpecialCharacter', { numberOne: n(1) }), key: BentoInputFieldPasswordDefaultRule.REQUIRE_SPECIAL_CHARACTER, validate: requireSpecialCharacter, }, ]; let validationList: Array<BentoInputFieldPasswordValidationListItem> = defaultValidationList; if (props.validation) { const defaultValidationKeys: Array<string> = defaultValidationList.map(rule => rule.key); const customRuleKeys = props.validation?.list?.map(rule => rule.key); // remove from the custom list any rules with default keys (rules that are overriding existent rules) const customValidationList = props.validation?.list?.filter(rule => !defaultValidationKeys.includes(rule.key)) ?? []; // loop through the default list and replace default rule with the custom rule if it exists const validationListWithOverrides = defaultValidationList.map(rule => { if (customRuleKeys?.includes(rule.key)) { const customRule = props.validation?.list?.find(customRule => customRule.key === rule.key); return customRule; } else { return rule; } }); validationList = [...validationListWithOverrides, ...customValidationList]; } return { title: props.validation?.title ?? t('validationTitle'), suggestion: props.validation?.suggestion ?? t('validationSuggestion'), list: validationList, }; }); const computedValidationList = computed(() => computedValidations.value?.list?.filter(rule => !rule.disabled)); const hasErrors = computed(() => passwordValidity.value?.some(validation => !validation)); const hasValidations = computed( () => inputFieldPasswordInputRef.value && computedValidations.value?.list?.length > 0 ); const checkCapsLock = (e: KeyboardEvent) => { // Check if getModifierState exists and is a function in case the user's browser is autofilling if (typeof e.getModifierState === 'function') { isCapsLockOn.value = e.getModifierState('CapsLock'); } }; const toggleInputType = () => { if (isPasswordReadable.value) { inputType.value = BentoInputFieldPasswordType.PASSWORD; } else { inputType.value = BentoInputFieldPasswordType.TEXT; } }; const onUpdateModelValue = (value: string) => { if (props.disabled) { return; } inputValue.value = value; emitValue(inputValue.value); }; const onFocus = () => { emit('focus'); if (!props.disableValidation) { if (props.withHint) { isHintDisplayed.value = true; } checkValidity(inputValue.value); } }; const onBlur = () => { emit('blur'); }; const checkValidity = (value: BentoInputFieldPasswordProps['modelValue']) => { const rulesValidityObj = computedValidationList.value?.map(validationItem => ({ key: validationItem.key, label: validationItem.label, isValid: validationItem.validate(value, ...(validationItem.additionalArgs ?? [])), })); let customRuleIndex = 1; passwordValidity.value = rulesValidityObj?.map(error => error.isValid); errorsList.value = rulesValidityObj ?.filter(rule => !rule.isValid) .reduce((errorsObject, error) => { const key = error.key ?? `custom-rule-${customRuleIndex++}`; return { ...errorsObject, [key]: error.label, }; }, {}); }; const handleCapsLockMouseLeave = () => { isTooltipDisplayed.value = false; capsLockTooltipTimeoutID.value = null; }; const handleCapsLockMouseEnter = () => { isTooltipDisplayed.value = true; }; watch( () => props.modelValue, () => { inputValue.value = props.modelValue; } ); watch(isCapsLockOn, isCapsLockOn => { if (!isCapsLockOn) { clearTimeout(capsLockTooltipTimeoutID.value); capsLockTooltipTimeoutID.value = null; isTooltipDisplayed.value = false; } else { isTooltipDisplayed.value = true; capsLockTooltipTimeoutID.value = setTimeout(() => { capsLockTooltipTimeoutID.value = null; isTooltipDisplayed.value = false; }, TOOLTIP_TIMEOUT); } }); watchDebounced( inputValue, inputValue => { checkValidity(inputValue); if (!hasErrors.value) { emit('input:valid'); } else { emit('input:error', errorsList.value); } }, // wait for 0.5s before checking validity OR 2s of continuous typing { debounce: props.debounceTime, maxWait: DEBOUNCE_MAX_WAIT } ); </script> <script lang="ts"> const MIN_PASSWORD_LENGTH = 12; const DEBOUNCE_DURATION = 500; const DEBOUNCE_MAX_WAIT = 2000; const TOOLTIP_TIMEOUT = 2000; /** * The input field password is a component designed to support users' preference for entering values securely. * This is an extension of the `bento-input-field` component. * * @example * import { BentoInputFieldPassword } from '@adyen/bento-vue2'; * * export default { * components: { BentoInputFieldPassword }, * template: ` * <bento-input-field-password * label="Label" * error-message="Error" * :validation="{ * extendDefaultValidation: true, * list: [ * { label: '18 characters', key: 'minLength', validate: minLength, additionalArgs: [18] }, * { label: 'Always true', key: 'alwaysTrue', validate: () => true }, * { key: 'requireSpecialCharacter', disabled: true } * ], * }" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-input-field-password', model: { prop: 'modelValue', event: 'update:model-value' }, }; </script> <style lang="scss" scoped src="./input-field-password.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { isVue2 } from 'vue-demi'; import { action } from '@storybook/addon-actions'; import { nextTick, onMounted, ref } from 'vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoInputFieldPhoneNumber from './input-field-phone-number.vue'; import type { Meta, StoryObj } from '@storybook/vue'; import type { BentoInputFieldPhoneNumberDropdownProps } from './input-field-phone-number.types';
|
|
1
|
+
import { isVue2 } from 'vue-demi'; import { action } from '@storybook/addon-actions'; import { nextTick, onMounted, ref } from 'vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoInputFieldPhoneNumber from './input-field-phone-number.vue'; import type { Meta, StoryObj } from '@storybook/vue'; import type { BentoInputFieldPhoneNumberDropdownProps } from './input-field-phone-number.types'; const InputFieldPhoneNumberDefaultExample = "<template>\n <bento-input-field-phone-number\n v-model=\"value\"\n label=\"Phone number\"\n :selected-country=\"country\"\n :error-message=\"errorMessage\"\n :countries=\"['DE', 'BE', 'NL', 'US']\"\n @update:selected-country=\"onSelectedCountry\"\n @blur=\"onBlur\"\n @input:valid=\"validate\"\n >\n </bento-input-field-phone-number>\n</template>\n<script lang=\"ts\" setup>\n import { nextTick, onMounted, ref } from 'vue';\n import { BentoInputFieldPhoneNumber } from '../input-field-phone-number';\n import { type CountryCode } from 'libphonenumber-js';\n\n const value = ref('+12025550123');\n const country = ref<CountryCode>('US');\n const errorMessage = ref('');\n const isValid = ref(false);\n\n const updateErrorMessage = () => {\n if (!isValid.value && value.value) {\n errorMessage.value = 'This number is not valid.';\n } else {\n errorMessage.value = '';\n }\n };\n\n const onSelectedCountry = async updatedCountry => {\n country.value = updatedCountry;\n await nextTick();\n updateErrorMessage();\n };\n\n const validate = (validity: boolean) => {\n isValid.value = validity;\n if (validity) {\n updateErrorMessage();\n }\n };\n\n const onBlur = () => {\n updateErrorMessage();\n };\n\n onMounted(() => {\n updateErrorMessage();\n });\n</script>\n"; const meta: Meta = { title: 'Input field phone number', component: BentoInputFieldPhoneNumber, argTypes: { description: { name: 'description', description: 'If set, displays a description text for the input.', table: { category: 'props', type: { summary: 'string' }, }, control: { type: 'text', }, }, 'slot:description': { name: 'description', defaultValue: '', description: 'If set, displays a description text for the input. Overrides the `description` prop when provided.', table: { category: 'slots', type: { summary: 'VNode[]' }, }, control: { type: 'text', }, }, }, }; export default meta; type Story = StoryObj<typeof BentoInputFieldPhoneNumber>; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoInputFieldPhoneNumber }, props: Object.keys(argTypes), template: ` <bento-input-field-phone-number v-bind="args" :model-value="value" :selected-country="country" :error-message="errorMessage" @update:model-value="updateModelValue" @update:selected-country="onSelectedCountry" @blur="onBlur" @input:valid="validate" > <template #default v-if='args.default'> {{ args.default }} </template> <template #description v-if="args['slot:description']"> {{ args['slot:description'] }} </template> </bento-input-field-phone-number> `, setup(props) { const value = ref('+12025550123'); const country = ref('US'); const errorMessage = ref(''); const isValid = ref(false); const updateErrorMessage = () => { if (!isValid.value && value.value) { errorMessage.value = 'This number is not valid.'; } else { errorMessage.value = ''; } }; const onSelectedCountry = async updatedCountry => { country.value = updatedCountry; action('update:selected-country')(updatedCountry); await nextTick(); updateErrorMessage(); }; const updateModelValue = input => { value.value = input; action('update:model-value')(input); }; const validate = (validity: boolean) => { isValid.value = validity; if (validity) { updateErrorMessage(); } action('input:valid')(validity); }; const onBlur = () => { updateErrorMessage(); action('blur')(); }; onMounted(() => { updateErrorMessage(); }); return { // Values args: isVue2 ? props : _args, value, country, errorMessage, // Methods onSelectedCountry, updateModelValue, onBlur, validate, }; }, }), args: { label: 'Phone number', description: 'Select your country and input the phone number', dropdown: { readonly: false, } satisfies BentoInputFieldPhoneNumberDropdownProps, }, parameters: storybookDocsParameter(InputFieldPhoneNumberDefaultExample), };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { type CountryCode } from 'libphonenumber-js'; import { type FormField } from '../../types/form-field'; import { type BentoInputDropdownProps } from '@/components/input-field/input-field.types'; export type BentoInputFieldPhoneNumberDropdownProps = Pick<BentoInputDropdownProps, 'readonly'>; /** * String literal with all the available country codes. */ export type BentoInputFieldPhoneNumberCountryCode = CountryCode | any; export interface BentoInputFieldPhoneNumberProps extends FormField { /** * Array of country codes to show on the dropdown */ countries?: Array<BentoInputFieldPhoneNumberCountryCode>; /** * Dropdown props for the small selectable dropdown menu. * Properties supported: * * - `readonly`: Sets the dropdown in read-only mode */ dropdown?: BentoInputFieldPhoneNumberDropdownProps; /** * If set, allows filtering on the countries dropdown */ dynamicFiltering?: boolean; /** *
|
|
1
|
+
import { type CountryCode } from 'libphonenumber-js'; import { type FormField } from '../../types/form-field'; import { type BentoInputDropdownProps } from '@/components/input-field/input-field.types'; export type BentoInputFieldPhoneNumberDropdownProps = Pick<BentoInputDropdownProps, 'readonly'>; /** * String literal with all the available country codes. */ export type BentoInputFieldPhoneNumberCountryCode = CountryCode | any; export interface BentoInputFieldPhoneNumberProps extends FormField { /** * Array of country codes to show on the dropdown */ countries?: Array<BentoInputFieldPhoneNumberCountryCode>; /** * Dropdown props for the small selectable dropdown menu. * Properties supported: * * - `readonly`: Sets the dropdown in read-only mode */ dropdown?: BentoInputFieldPhoneNumberDropdownProps; /** * If set, allows filtering on the countries dropdown */ dynamicFiltering?: boolean; /** * Selected country code */ selectedCountry?: BentoInputFieldPhoneNumberCountryCode; /** * The current value of the field. */ modelValue?: string | number; }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <bento-input-field v-bind="computedProps" ref="inputField" variant="dropdown" :dropdown="dropdown" class="b-input-field-phone-number" :model-value="displayValue" :placeholder="computedPlaceholder" type="tel" @blur="onBlur" @update:model-value="onUpdateModelValue" @keydown="onKeydown" @dropdown-input="onCountryChange" > <template v-if="label">{{ label }}</template> <template v-else><slot /></template> <template #description> <slot name="description">{{ description }}</slot> </template> </bento-input-field> </template> <script setup lang="ts"> import { computed, nextTick, provide, ref, watch } from 'vue'; import { type BentoInputDropdownProps, BentoInputField } from '@/components/input-field'; import { AsYouType, type CountryCode } from 'libphonenumber-js'; import { useI18n } from '@/utils/ts/i18n'; import { appendRegionCode, BentoInputFieldPhoneNumberCountries, getCountriesDropdownItems, getFormattedPhoneCode, getPhoneNumberPlaceholder, handleInputCursor, hasDefaultRegionCode, parsePhoneAndCountry, preventRegionCodeRepetition, stripRegionCode, } from './utils'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import { deprecate } from '@/utils/ts/deprecate'; import type { BentoInputFieldPhoneNumberProps } from './input-field-phone-number.types'; import messages from './messages.json'; import { INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY } from '@/components/input-field/input-field.keys'; type MessageSchema = (typeof messages)['en-US']; const { t, locale } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoInputFieldPhoneNumberProps>(), { countries: () => BentoInputFieldPhoneNumberCountries, description: undefined, dynamicFiltering: true, }); /** * Avoid passing `value` to `bento-input-field`. * This component uses `displayValue` instead. */ const computedProps = computed(() => { const { value, ...allProps } = props; return allProps; }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the value changes. The value is the unformatted phone number in E.164 format. * @deprecated Since v2.0.0. Use `v-model` or `update:model-value` instead. */ (e: 'input', value: string): void; /** * Emitted when the value changes. The value is the unformatted phone number in E.164 format. */ (e: 'update:model-value', value: string): void; /** * Emitted when the selected country is updated. */ (e: 'update:selected-country', country: CountryCode): void; /** * Emitted when the phone number's validity changes. * The payload is a boolean indicating whether the number is valid. */ (e: 'input:valid', value: boolean): void; }>(); const inputField = ref(null); const displayValue = ref(''); const internalValue = ref(''); const insertedChar = ref(''); const { emitValue } = useFormFieldEmits<string>(emit); provide(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, 'bento-input-field-phone-number'); const computedPlaceholder = computed(() => { if (props.placeholder) { return props.placeholder; } if (props.selectedCountry) { return getPhoneNumberPlaceholder(props.selectedCountry); } return ''; }); const isDropdownReadonly = computed(() => props.dropdown?.readonly); const countryItems = computed(() => getCountriesDropdownItems(props.countries, locale.value)); const dropdown = computed<BentoInputDropdownProps>(() => ({ items: countryItems.value, modelValue: props.selectedCountry, readonly: isDropdownReadonly.value, dynamicFiltering: props.dynamicFiltering, 'aria-label': t('countrySelectorDropdown'), })); const onBlur = () => emit('blur'); const onUpdateModelValue = async (value: string) => { const inputEl = inputField.value.$refs.inputFieldElement; let formatter: AsYouType; // Check if the input value starts with '+' or '00', indicating it includes a country code. if (value.startsWith('+') || value.startsWith('00')) { // Reset displayValue to ensure changes are always reflected. // For example, if you paste '+1 264 123 4567' multiple times, the display value should always be '1234567'. // Without this, the displayValue will be '1234567' the first time but subsequently stay as '+1 264 123 4567'. displayValue.value = ''; // Normalize the value by replacing '00' with '+'. const normalizedValue = value.startsWith('00') ? `+${value.slice(2)}` : value; // Create a new AsYouType formatter to format the number as it's typed. formatter = new AsYouType(); formatter.input(normalizedValue); // Get the country code from the formatted number. const country = formatter.getCountry(); // Check if the extracted country code is valid and included in the allowed countries. const isValidCountry = country && props.countries.includes(country); // If the dropdown is readonly, determine whether to strip the region code based on the selected country. if (isDropdownReadonly.value) { displayValue.value = props.selectedCountry === country ? stripRegionCode(formatter.getNationalNumber(), country) : value; } else { // Emit an event to update the selected country, if valid. Otherwise, set it to null. emit('update:selected-country', isValidCountry ? country : null); // Strip the region code from the formatted number if the country is valid. displayValue.value = isValidCountry ? stripRegionCode(formatter.getNationalNumber(), country) : value; } const numberValue = formatter.getNumberValue() ?? ''; const formattedPhoneCode = getFormattedPhoneCode(country); const shouldClearInput = isValidCountry && numberValue.startsWith(formattedPhoneCode) && numberValue.length <= formattedPhoneCode.length; internalValue.value = shouldClearInput ? '' : numberValue; emitValue(internalValue.value); } else { // If the input doesn't start with a country code, format the number based on the selected country. formatter = new AsYouType(props.selectedCountry); const formatted = formatter.input(appendRegionCode(value, props.selectedCountry)); // Prevent repetition of the region code and get the unformatted number. const numberValue = formatter.getNumberValue(); const unformattedNumber = numberValue ? preventRegionCodeRepetition(value, numberValue, props.selectedCountry) : value; // Emit raw input when parsing fails i.e. number do not have country code // Emit the unformatted number value and update the display value with the stripped region code. internalValue.value = unformattedNumber; emitValue(internalValue.value); displayValue.value = stripRegionCode(formatted, props.selectedCountry); } await nextTick(); // Re-parse the number to ensure correct formatting, e.g., adding a leading '0' for NL numbers if valid and `hasDefaultRegionCode` returns false. if (formatter.isValid() && props.selectedCountry && !hasDefaultRegionCode(props.selectedCountry)) { const { displayNumber, isValid } = parsePhoneAndCountry( displayValue.value?.toString(), props.selectedCountry, props.countries ); displayValue.value = displayNumber; emitValidation(isValid); } else { emitValidation(formatter.isValid()); } handleInputCursor(inputEl, displayValue.value.toString(), insertedChar.value); }; const emitValidation = (isValid: boolean) => { // Numbers shorter than 5 digits are never valid phone numbers. // `libphonenumber-js` rejects numbers under 5 digits (`TOO_SHORT`). // We don’t call that check directly here, but use the same value for consistency. const MIN_PHONE_NUMBER_LENGTH_FOR_VALIDATION = 5; const numberLength = displayValue.value?.toString().replace(/\D/g, '')?.length ?? 0; const isValidNumberLength = numberLength >= MIN_PHONE_NUMBER_LENGTH_FOR_VALIDATION; emit('input:valid', isValidNumberLength && isValid); }; const onKeydown = event => { if (/^[a-zA-Z]$/.test(event.key) && !event.metaKey && !event.ctrlKey) { event.preventDefault(); return; } insertedChar.value = event.key; }; const onCountryChange = (country: CountryCode) => { emit('update:selected-country', country); const isUnformatted = displayValue.value?.toString().startsWith('+'); if (hasDefaultRegionCode(country) && !isUnformatted) { let formatter = new AsYouType(country); const formatted = formatter.input(appendRegionCode(displayValue.value?.toString(), country)); const unformattedNumber = preventRegionCodeRepetition(displayValue.value?.toString(), formatter.getNumberValue(), country) ?? ''; internalValue.value = unformattedNumber; emitValue(internalValue.value); displayValue.value = stripRegionCode(formatted, country); emitValidation(formatter.isValid()); } else { const { displayNumber, unformattedNumber, isValid } = parsePhoneAndCountry( displayValue.value?.toString(), country, props.countries ); internalValue.value = unformattedNumber ?? ''; emitValue(internalValue.value); displayValue.value = displayNumber; emitValidation(isValid); } }; watch( () => [props.value, props.modelValue], () => { const newValue = props.modelValue ?? props.value; if (newValue === internalValue.value) { return; } const { displayNumber, country, isValid } = parsePhoneAndCountry( newValue?.toString(), props.selectedCountry, props.countries ); if (country && country !== props.selectedCountry && !isDropdownReadonly.value) { emit('update:selected-country', country); } displayValue.value = stripRegionCode(displayNumber, country); emitValidation(isValid); }, { immediate: true } ); if (props.value) { deprecate( 'BentoInputFieldPhoneNumber "value" property', `The use of "value" prop in "BentoInputFieldPhoneNumber" is no longer supported. Use the "v-model" or "model-value" property instead.`, '2.0.0' ); } </script> <script lang="ts"> /** * Input field phone number can be used to display phone numbers that come with a country code. * * @example * import { BentoInputFieldPhoneNumber } from '@adyen/bento-vue2'; * * export default { * components: { BentoInputFieldPhoneNumber }, * template: ` * <bento-input-field-phone-number * label="Phone number" * :countries="['NL', 'BE']" * model-value="0612345678" * selected-country="NL" * /> * ` * } */ export default { i18n: { messages }, model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./input-field-phone-number.scss" />
|
|
1
|
+
<template> <bento-input-field v-bind="props" ref="inputField" variant="dropdown" :dropdown="dropdown" class="b-input-field-phone-number" :model-value="displayValue" :placeholder="computedPlaceholder" type="tel" @blur="onBlur" @update:model-value="onUpdateModelValue" @keydown="onKeydown" @dropdown-input="onCountryChange" > <template v-if="label">{{ label }}</template> <template v-else><slot /></template> <template #description> <slot name="description">{{ description }}</slot> </template> </bento-input-field> </template> <script setup lang="ts"> import { computed, nextTick, provide, ref, watch } from 'vue'; import { type BentoInputDropdownProps, BentoInputField } from '@/components/input-field'; import { AsYouType, type CountryCode } from 'libphonenumber-js'; import { useI18n } from '@/utils/ts/i18n'; import { appendRegionCode, BentoInputFieldPhoneNumberCountries, getCountriesDropdownItems, getFormattedPhoneCode, getPhoneNumberPlaceholder, handleInputCursor, hasDefaultRegionCode, parsePhoneAndCountry, preventRegionCodeRepetition, stripRegionCode, } from './utils'; import { useFormFieldEmits } from '@/utils/ts/form-field/form-field'; import type { BentoInputFieldPhoneNumberProps } from './input-field-phone-number.types'; import messages from './messages.json'; import { INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY } from '@/components/input-field/input-field.keys'; type MessageSchema = (typeof messages)['en-US']; const { t, locale } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoInputFieldPhoneNumberProps>(), { countries: () => BentoInputFieldPhoneNumberCountries, description: undefined, dynamicFiltering: true, }); const emit = defineEmits<{ /** * Emitted when the element has lost focus */ (e: 'blur'): void; /** * Emitted when the value changes. The value is the unformatted phone number in E.164 format. */ (e: 'update:model-value', value: string): void; /** * Emitted when the selected country is updated. */ (e: 'update:selected-country', country: CountryCode): void; /** * Emitted when the phone number's validity changes. * The payload is a boolean indicating whether the number is valid. */ (e: 'input:valid', value: boolean): void; }>(); const inputField = ref(null); const displayValue = ref(''); const internalValue = ref(''); const insertedChar = ref(''); const { emitValue } = useFormFieldEmits<string>(emit); provide(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, 'bento-input-field-phone-number'); const computedPlaceholder = computed(() => { if (props.placeholder) { return props.placeholder; } if (props.selectedCountry) { return getPhoneNumberPlaceholder(props.selectedCountry); } return ''; }); const isDropdownReadonly = computed(() => props.dropdown?.readonly); const countryItems = computed(() => getCountriesDropdownItems(props.countries, locale.value)); const dropdown = computed<BentoInputDropdownProps>(() => ({ items: countryItems.value, modelValue: props.selectedCountry, readonly: isDropdownReadonly.value, dynamicFiltering: props.dynamicFiltering, 'aria-label': t('countrySelectorDropdown'), })); const onBlur = () => emit('blur'); const onUpdateModelValue = async (value: string) => { const inputEl = inputField.value.$refs.inputFieldElement; let formatter: AsYouType; // Check if the input value starts with '+' or '00', indicating it includes a country code. if (value.startsWith('+') || value.startsWith('00')) { // Reset displayValue to ensure changes are always reflected. // For example, if you paste '+1 264 123 4567' multiple times, the display value should always be '1234567'. // Without this, the displayValue will be '1234567' the first time but subsequently stay as '+1 264 123 4567'. displayValue.value = ''; // Normalize the value by replacing '00' with '+'. const normalizedValue = value.startsWith('00') ? `+${value.slice(2)}` : value; // Create a new AsYouType formatter to format the number as it's typed. formatter = new AsYouType(); formatter.input(normalizedValue); // Get the country code from the formatted number. const country = formatter.getCountry(); // Check if the extracted country code is valid and included in the allowed countries. const isValidCountry = country && props.countries.includes(country); // If the dropdown is readonly, determine whether to strip the region code based on the selected country. if (isDropdownReadonly.value) { displayValue.value = props.selectedCountry === country ? stripRegionCode(formatter.getNationalNumber(), country) : value; } else { // Emit an event to update the selected country, if valid. Otherwise, set it to null. emit('update:selected-country', isValidCountry ? country : null); // Strip the region code from the formatted number if the country is valid. displayValue.value = isValidCountry ? stripRegionCode(formatter.getNationalNumber(), country) : value; } const numberValue = formatter.getNumberValue() ?? ''; const formattedPhoneCode = getFormattedPhoneCode(country); const shouldClearInput = isValidCountry && numberValue.startsWith(formattedPhoneCode) && numberValue.length <= formattedPhoneCode.length; internalValue.value = shouldClearInput ? '' : numberValue; emitValue(internalValue.value); } else { // If the input doesn't start with a country code, format the number based on the selected country. formatter = new AsYouType(props.selectedCountry); const formatted = formatter.input(appendRegionCode(value, props.selectedCountry)); // Prevent repetition of the region code and get the unformatted number. const numberValue = formatter.getNumberValue(); const unformattedNumber = numberValue ? preventRegionCodeRepetition(value, numberValue, props.selectedCountry) : value; // Emit raw input when parsing fails i.e. number do not have country code // Emit the unformatted number value and update the display value with the stripped region code. internalValue.value = unformattedNumber; emitValue(internalValue.value); displayValue.value = stripRegionCode(formatted, props.selectedCountry); } await nextTick(); // Re-parse the number to ensure correct formatting, e.g., adding a leading '0' for NL numbers if valid and `hasDefaultRegionCode` returns false. if (formatter.isValid() && props.selectedCountry && !hasDefaultRegionCode(props.selectedCountry)) { const { displayNumber, isValid } = parsePhoneAndCountry( displayValue.value?.toString(), props.selectedCountry, props.countries ); displayValue.value = displayNumber; emitValidation(isValid); } else { emitValidation(formatter.isValid()); } handleInputCursor(inputEl, displayValue.value.toString(), insertedChar.value); }; const emitValidation = (isValid: boolean) => { // Numbers shorter than 5 digits are never valid phone numbers. // `libphonenumber-js` rejects numbers under 5 digits (`TOO_SHORT`). // We don’t call that check directly here, but use the same value for consistency. const MIN_PHONE_NUMBER_LENGTH_FOR_VALIDATION = 5; const numberLength = displayValue.value?.toString().replace(/\D/g, '')?.length ?? 0; const isValidNumberLength = numberLength >= MIN_PHONE_NUMBER_LENGTH_FOR_VALIDATION; emit('input:valid', isValidNumberLength && isValid); }; const onKeydown = event => { if (/^[a-zA-Z]$/.test(event.key) && !event.metaKey && !event.ctrlKey) { event.preventDefault(); return; } insertedChar.value = event.key; }; const onCountryChange = (country: CountryCode) => { emit('update:selected-country', country); const isUnformatted = displayValue.value?.toString().startsWith('+'); if (hasDefaultRegionCode(country) && !isUnformatted) { let formatter = new AsYouType(country); const formatted = formatter.input(appendRegionCode(displayValue.value?.toString(), country)); const unformattedNumber = preventRegionCodeRepetition(displayValue.value?.toString(), formatter.getNumberValue(), country) ?? ''; internalValue.value = unformattedNumber; emitValue(internalValue.value); displayValue.value = stripRegionCode(formatted, country); emitValidation(formatter.isValid()); } else { const { displayNumber, unformattedNumber, isValid } = parsePhoneAndCountry( displayValue.value?.toString(), country, props.countries ); internalValue.value = unformattedNumber ?? ''; emitValue(internalValue.value); displayValue.value = displayNumber; emitValidation(isValid); } }; watch( () => props.modelValue, () => { const newValue = props.modelValue; if (newValue === internalValue.value) { return; } const { displayNumber, country, isValid } = parsePhoneAndCountry( newValue?.toString(), props.selectedCountry, props.countries ); if (country && country !== props.selectedCountry && !isDropdownReadonly.value) { emit('update:selected-country', country); } displayValue.value = stripRegionCode(displayNumber, country); emitValidation(isValid); }, { immediate: true } ); </script> <script lang="ts"> /** * Input field phone number can be used to display phone numbers that come with a country code. * * @example * import { BentoInputFieldPhoneNumber } from '@adyen/bento-vue2'; * * export default { * components: { BentoInputFieldPhoneNumber }, * template: ` * <bento-input-field-phone-number * label="Phone number" * :countries="['NL', 'BE']" * model-value="0612345678" * selected-country="NL" * /> * ` * } */ export default { i18n: { messages }, model: { prop: 'modelValue', event: 'update:model-value' }, }; </script> <style lang="scss" scoped src="./input-field-phone-number.scss" />
|
|
@@ -35,6 +35,23 @@ and `Tab`) are silently blocked via the `keydown` handler.
|
|
|
35
35
|
|
|
36
36
|
## Modifiers
|
|
37
37
|
|
|
38
|
+
### Predefined times
|
|
39
|
+
|
|
40
|
+
Use the `interval` prop, in minutes, when the time has to be limited to fixed steps such as full hours or 15 minute
|
|
41
|
+
periods. The input then becomes a searchable dropdown listing every time of the day matching the interval, starting at
|
|
42
|
+
midnight, and the user selects one of those times instead of typing a free time. Typing filters the list.
|
|
43
|
+
|
|
44
|
+
<Canvas of={InputTimeStories.Interval} />
|
|
45
|
+
|
|
46
|
+
```html
|
|
47
|
+
<bento-input-time label="Start time" :interval="15" />
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The interval must be a whole number of at least `5` minutes that divides a full day, e.g. `5`, `15`, `30` or `60`. Any
|
|
51
|
+
other value is ignored, logs a development warning and keeps the free text time input.
|
|
52
|
+
|
|
53
|
+
On touch devices the native time input is kept and the interval is applied through its `step` attribute.
|
|
54
|
+
|
|
38
55
|
### Error state
|
|
39
56
|
|
|
40
57
|
Use the `error-message` prop to display an inline error message and apply error styling:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import { ref } from 'vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoInputTime from './input-time.vue'; import type { Meta, StoryObj } from '@storybook/vue';
|
|
1
|
+
import { action } from '@storybook/addon-actions'; import { isVue2 } from 'vue-demi'; import { ref } from 'vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import BentoInputTime from './input-time.vue'; import type { Meta, StoryObj } from '@storybook/vue'; const defaultCode = "<template>\n <bento-input-time\n label=\"Time input\"\n :model-value=\"modelValue\"\n @update:model-value=\"updateModelValue\"\n @input:valid=\"inputValidAction\"\n @input:error=\"inputErrorAction\"\n />\n</template>\n\n<script setup lang=\"ts\">\n import { BentoInputTime } from '@adyen/bento-vue2';\n import { ref } from 'vue';\n\n const modelValue = ref();\n\n const updateModelValue = (value: string | null) => {\n modelValue.value = value;\n };\n\n const inputValidAction = () => {\n // Your action here\n };\n\n const inputErrorAction = () => {\n // Your error action here\n };\n</script>\n"; const intervalCode = "<template>\n <bento-input-time\n label=\"Time input\"\n :interval=\"15\"\n :model-value=\"modelValue\"\n @update:model-value=\"updateModelValue\"\n />\n</template>\n\n<script setup lang=\"ts\">\n import { BentoInputTime } from '@adyen/bento-vue2';\n import { ref } from 'vue';\n\n const modelValue = ref('09:00:00');\n\n const updateModelValue = (value: string | null) => {\n modelValue.value = value;\n };\n</script>\n"; const meta: Meta = { title: 'Input Time', component: BentoInputTime, }; const DEFAULT_PROPS = { label: 'Input time', }; export default meta; type Story = StoryObj<typeof BentoInputTime>; export const Default: Story = { args: DEFAULT_PROPS, render: (_args, { argTypes }) => ({ components: { BentoInputTime }, props: Object.keys(argTypes), setup(props) { const modelValue = ref(''); const onUpdateModelValue = (value: string) => { modelValue.value = value; action('update:model-value')(value); }; return { args: isVue2 ? props : _args, modelValue, onUpdateModelValue, onInputValid: action('input:valid'), onInputError: action('input:error'), onInputKeydown: action('keydown'), }; }, template: ` <bento-input-time v-bind="args" :model-value="modelValue" @update:model-value="onUpdateModelValue" @input:valid="onInputValid" @input:error="onInputError" @keydown="onInputKeydown" /> `, }), parameters: storybookDocsParameter(defaultCode), }; export const Interval: Story = { args: { ...DEFAULT_PROPS, interval: 15 }, render: (_args, { argTypes }) => ({ components: { BentoInputTime }, props: Object.keys(argTypes), setup(props) { const modelValue = ref('09:00:00'); const onUpdateModelValue = (value: string) => { modelValue.value = value; action('update:model-value')(value); }; return { args: isVue2 ? props : _args, modelValue, onUpdateModelValue, }; }, template: ` <bento-input-time v-bind="args" :model-value="modelValue" @update:model-value="onUpdateModelValue" /> `, }), parameters: storybookDocsParameter(intervalCode), };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import type { BentoInputFieldValue } from '@/components/input-field/input-field.types'; import { type FormField } from '../../types/form-field'; export interface BentoInputTimeProps extends FormField { /** * Reduces height of the input */ condensed?: boolean; /** * Text displayed as the value of the input field */ modelValue?: BentoInputFieldValue; }
|
|
1
|
+
import type { BentoInputFieldValue } from '@/components/input-field/input-field.types'; import { type FormField } from '../../types/form-field'; export interface BentoInputTimeProps extends FormField { /** * Reduces height of the input */ condensed?: boolean; /** * Limits the selectable times to a fixed interval, expressed in minutes. * When set, the input becomes a searchable dropdown listing every time of the day * matching the interval, starting at midnight, e.g. `15` lists `00:00:00`, `00:15:00`, ... * Must be a whole number of at least 5 minutes that divides a full day. * @default undefined */ interval?: number; /** * Text displayed as the value of the input field */ modelValue?: BentoInputFieldValue; }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <bento-
|
|
1
|
+
<template> <bento-dropdown v-if="isTimeDropdown" class="b-input-time" dynamic-filtering :condensed="props.condensed" :disabled="props.disabled" :error-message="computedErrorMessage" :items="timeItems" :label="props.label" :model-value="props.modelValue ?? ''" :optional="props.optional" :placeholder="props.placeholder" :readonly="props.readonly" :required="props.required" :tooltip-text="props.tooltipText" v-bind="$attrs" @click.native="emit('click', $event)" @update:model-value="onTimeSelected" > <template #description> <slot name="description">{{ timeDescription }}</slot> </template> </bento-dropdown> <bento-input-field v-else class="b-input-time" :type="inputType" :step="nativeStep" :condensed="props.condensed" :disabled="props.disabled" :error-message="inputErrorMessage" :label="props.label" :model-value="inputModelValue" :optional="props.optional" :placeholder="computedPlaceholder" :readonly="props.readonly" :required="props.required" :tooltip-text="props.tooltipText" v-bind="$attrs" @click="emit('click', $event)" @update:model-value="onInput" @keydown="onKeyDown" > <template v-if="hasSlot('default')" #default> <slot name="default"></slot> </template> <template v-if="!isTouchDevice" #description> <slot name="description">{{ timeDescription }}</slot> </template> </bento-input-field> </template> <script setup lang="ts"> // Components import { BentoDropdown } from '@/components/dropdown'; import { BentoInputField } from '@/components/input-field'; import { INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY } from '@/components/input-field/input-field.keys'; // Utils import { debounce } from '@/utils/ts/debounce'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { formatTimeInputValue, generateTimeOptions, isValidTimeInterval, isValidTimeString, validateTimeInput, } from '@/utils/ts/time-input'; // Types import type { BentoInputFieldValue } from '@/components/input-field/input-field.types'; import { type BentoInputTimeProps } from './input-time.types'; // Localization import messages from './messages.json'; import { computed, provide, ref, useSlots, watch } from 'vue'; import { useHasSlot, useTouchDevice } from '@/composables'; const VALIDATION_DEBOUNCE_TIME = 300; provide(INPUT_FIELD_PARENT_COMPONENT_INJECTION_KEY, 'bento-input-time'); const slots = useSlots(); const hasSlot = useHasSlot(slots); const { isTouchDevice } = useTouchDevice(); type MessageSchema = (typeof messages)['en-US']; const { t } = useI18n<{ message: MessageSchema }>({ messages }); const props = withDefaults(defineProps<BentoInputTimeProps>(), { condensed: false, description: undefined, disabled: false, errorMessage: null, interval: undefined, label: '', modelValue: undefined, optional: false, placeholder: '', readonly: false, required: false, tooltipText: null, }); const emit = defineEmits<{ /** * Emitted when the input is clicked */ (e: 'click', event: MouseEvent): void; /** * Emitted when the component's model changes */ (e: 'update:model-value', value: BentoInputFieldValue): void; /** * Emitted when a key is pressed down */ (e: 'keydown', event: KeyboardEvent): void; /** * Emitted when the time value passes validation (complete HH:MM:SS format) */ (e: 'input:valid'): void; /** * Emitted when the time value fails validation */ (e: 'input:error'): void; }>(); const internalError = ref(false); const computedErrorMessage = computed(() => { if (props.errorMessage) { return props.errorMessage; } return internalError.value ? t('invalidTimeFormat') : null; }); const inputErrorMessage = computed(() => (isTouchDevice.value ? props.errorMessage : computedErrorMessage.value)); const inputType = computed(() => (isTouchDevice.value ? 'time' : undefined)); const inputModelValue = computed(() => props.modelValue); const computedPlaceholder = computed(() => (isTouchDevice ? undefined : props.placeholder)); const timeDescription = computed(() => props.description ? `${props.description} ${t('timeFormatDescription')}` : t('timeFormatDescription') ); const hasTimeInterval = computed(() => isValidTimeInterval(props.interval)); // On touch devices the native time input already restricts the selectable times through `step` const isTimeDropdown = computed(() => hasTimeInterval.value && !isTouchDevice.value); const timeItems = computed(() => generateTimeOptions(props.interval).map(timeOption => ({ label: timeOption, value: timeOption })) ); const nativeStep = computed(() => { if (!isTouchDevice.value) { return undefined; } return hasTimeInterval.value ? props.interval * 60 : 1; }); watch( () => props.interval, interval => { if (interval != null && !isValidTimeInterval(interval)) { printDevelopmentWarning( `[bento-input-time] The "interval" prop must be a whole number of minutes of at least 5 that divides a full day, e.g. 5, 15, 30 or 60. Received "${interval}", falling back to a free text time input.` ); } }, { immediate: true } ); const validateTime = (timeText: string) => { if (!timeText) { internalError.value = false; return; } const isValid = isValidTimeString(timeText); internalError.value = !isValid; if (isValid) { emit('input:valid'); } else { emit('input:error'); } }; const validateTimeWithDebounce = debounce(validateTime, VALIDATION_DEBOUNCE_TIME); // Watch for external modelValue changes (e.g., when parent resets the value) watch( () => props.modelValue, newValue => { const timeText = String(newValue ?? ''); // Clear the error only when the new value is a complete, valid time string if (timeText.length === 8 && isValidTimeString(timeText)) { internalError.value = false; } } ); const onInput = (value: BentoInputFieldValue) => { if (props.disabled || props.readonly) { return; } if (isTouchDevice.value) { // A `step` of whole minutes hides the seconds in the native input, so they are added back const nativeValue = String(value ?? ''); const shouldAppendSeconds = hasTimeInterval.value && nativeValue.length === 5; emit('update:model-value', shouldAppendSeconds ? `${nativeValue}:00` : nativeValue); emit('input:valid'); } else { const formatted = formatTimeInputValue(String(value ?? '')); emit('update:model-value', formatted); validateTimeWithDebounce(formatted); } }; const onKeyDown = (event: KeyboardEvent) => { if (!isTouchDevice.value) { validateTimeInput(event); } emit('keydown', event); }; const onTimeSelected = (value: BentoInputFieldValue) => { if (props.disabled || props.readonly) { return; } internalError.value = false; emit('update:model-value', String(value ?? '')); emit('input:valid'); }; </script> <script lang="ts"> /** * Input time component for entering time values in HH:MM:SS format. * This is an extension of the `bento-input-field` component with built-in * time formatting and validation logic. * * @usage * import { BentoInputTime } from '@adyen/bento-vue2'; * * const timeValue = ref(''); * * export default { * components: { BentoInputTime }, * template: ` * <bento-input-time * label="Start time" * :model-value="timeValue" * @update:model-value="newValue => timeValue.value = newValue" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-input-time', inheritAttrs: false, model: { prop: 'modelValue' }, }; </script> <style lang="scss" scoped src="./input-time.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { isVue2 } from 'vue-demi'; import { computed, ref, watch } from 'vue'; import { BentoButton } from '@/components/button'; import { BentoDataGrid } from '@/components/data-grid'; import { BentoStructuredList, BentoStructuredListItem } from '@/components/structured-list'; import { BentoTypography } from '@/components/typography'; import { BentoHeader } from '@/components/header'; import { BentoInspectorPage } from './components/inspector-page/index.js'; import BentoInspector from './inspector.vue'; import InspectorWithMultiplePagesExample from './__tests__/inspector-with-multiple-pages-example.vue?raw'; import PlusIcon from '@adyen/ui-assets-icons-16/vue/plus'; import SearchIcon from '@adyen/ui-assets-icons-16/vue/search'; import ShieldIcon from '@adyen/ui-assets-icons-16/vue/shield'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import type { Meta, StoryObj } from '@storybook/vue'; import { BentoInspectorSizeOptions } from './inspector.types'; import './inspector.stories.scss?module'; const meta: Meta = { title: 'Inspector', component: BentoInspector, parameters: { layout: 'fullscreen', }, argTypes: { size: { options: BentoInspectorSizeOptions, control: { type: 'select', }, }, relativeLayout: { options: ['push-content', 'overlay-content'], control: { type: 'select', }, }, }, }; export default meta; type Story = StoryObj<typeof BentoInspector>; const DEFAULT_PROPS = { title: 'Title', size: 'large', isOpen: false, activePage: '', relativeLayout: 'push-content', }; const defaultCode = ` <template> <bento-inspector :is-open="isOpen" @update:is-open="isOpen = $event" :active-page="activePage" @update:active-page="activePage = $event" title="Inspector title" expandable > <template #page> // Your page content <bento-button :aria-expanded="isOpen" @click="openInspector">{{ inspectorButtonTitle }}</bento-button> </template> <template #content> // Your inspector content </template> </bento-inspector> </template> <script setup lang="ts"> import { ref } from 'vue'; import { BentoButton, BentoInspector } from '@adyen/bento-vue2'; const isOpen = ref(false); const activePage = ref('') const inspectorButtonTitle = computed(() => (isOpen.value ? 'Move to inspector' : 'Open inspector')); const openInspector = () => { isOpen.value = true; activePage.value = 'PageId1234'; }; </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoDataGrid, BentoHeader, BentoInspector, BentoStructuredList, BentoStructuredListItem, BentoTypography, SearchIcon, }, props: Object.keys(argTypes), template: ` <bento-inspector v-bind="args" :is-open="isOpen" :title="title" :active-page="activePage" @update:is-open="isOpen = $event" @update:active-page="activePage = $event" expandable > <template #page> <div style="padding:16px"> <bento-header v-bind="headerProps"></bento-header> <bento-data-grid v-bind="gridArgs"> <template #item-paymentMethods="{item}"> <div> {{ item.paymentMethods }} <bento-button variant="tertiary" @click="openInspector(item)" style="float:right" > <template #iconLeft> <search-icon :svg-title="inspectorButtonTitle" /> </template> </bento-button> </div> </template> </bento-data-grid> </div> </template> <template #content> <div style="display: flex; flex-direction: column; height: 100%"> <bento-structured-list style="flex-grow: 1"> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> </bento-structured-list> <div> <bento-button style="float: right">Action</bento-button> </div> </div> </template> </bento-inspector> `, setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(args.isOpen); const title = ref(args.title); const activePage = ref(''); const inspectorButtonTitle = computed(() => (isOpen.value ? 'Move to inspector' : 'Open inspector')); const gridData = [ { id: 1, paymentMethods: 'Visa', amount: '1,234 USD', numeric: '54,213' }, { id: 2, paymentMethods: 'MasterCard', amount: '1,234 USD', numeric: '54,213' }, { id: 3, paymentMethods: 'iDeal', amount: '1,234 USD', numeric: '54,213' }, { id: 4, paymentMethods: 'Klarna', amount: '1,234 USD', numeric: '54,213' }, { id: 5, paymentMethods: 'Apple Pay', amount: '1,234 USD', numeric: '54,213' }, ]; const openInspector = value => { title.value = value.paymentMethods; isOpen.value = true; activePage.value = value.paymentMethods; }; const gridColumns = [ { field: 'paymentMethods', label: 'Payment methods', minWidth: 200, mandatory: true, }, { field: 'amount', label: 'Amount', numeric: true, minWidth: 300 }, { field: 'numeric', label: 'Numeric', numeric: true, minWidth: 300 }, ]; const gridArgs = { data: gridData, columns: gridColumns, pagination: {}, }; const headerProps = { title: 'Payment methods list', actions: [ { title: 'Primary action', event: () => undefined }, { title: 'Secondary action', icon: PlusIcon, event: () => undefined }, { title: 'Tertiary action', icon: PlusIcon, event: () => undefined }, { title: 'Another action', event: () => undefined }, ], lastUpdated: new Date(), }; watch( () => args.isOpen, () => { isOpen.value = args.isOpen; } ); watch( () => args.title, () => { title.value = args.title; } ); watch( () => args.activePage, () => { activePage.value = args.activePage; } ); return { args, isOpen, gridArgs, title, headerProps, activePage, openInspector, inspectorButtonTitle, }; }, }), args: DEFAULT_PROPS, parameters: storybookDocsParameter(defaultCode), }; export const MultiplePages: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoDataGrid, BentoHeader, BentoInspector, BentoInspectorPage, BentoStructuredList, BentoStructuredListItem, BentoTypography, SearchIcon, ShieldIcon, }, props: Object.keys(argTypes), template: ` <bento-inspector v-bind="args" :is-open="isOpen" :active-page="activePage" title="What if this title is really really long? So long that it should overflow the content on the right where the buttons are" @update:is-open="isOpen = $event" @update:active-page="onUpdateActivePage" expandable > <template #page> <div style="padding:16px"> <bento-header v-bind="headerProps"></bento-header> <bento-data-grid v-bind="gridArgs"> <template #item-paymentMethods="{item}"> <div> {{ item.paymentMethods }} <bento-button variant="tertiary" @click="openInspector(item)" style="float:right" > <template #iconLeft> <search-icon :svg-title="inspectorButtonTitle" /> </template> </bento-button> <bento-button v-if="item.paymentMethods === 'Visa'" variant="tertiary" @click="openInspector(item, true)" style="float:right; margin-right: 10px;" > <template #iconLeft> <shield-icon :svg-title="inspectorButtonTitle" /> </template> </bento-button> </div> </template> </bento-data-grid> </div> </template> <template #content> <bento-inspector-page page-id="visa-secure" title="Visa secure with a wrap text title" :actions="actions"> <p style="margin-top: 0;">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus accumsan dui sed nulla tincidunt fermentum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec tristique erat ut tincidunt aliquet. Nunc placerat aliquam consequat. Suspendisse non leo ut sem rutrum aliquet et at ex. Mauris facilisis arcu nec ipsum auctor, imperdiet fringilla tortor placerat. Aenean bibendum dui id velit ornare, eget porta tortor euismod.</p> <p>Phasellus posuere nec enim sed sodales. Mauris aliquet tristique nisl vitae sollicitudin. Aliquam nec diam lorem. Pellentesque eu ex condimentum, rhoncus ipsum et, varius elit. In lacinia elit eros, vel euismod nisl vestibulum et. Vivamus quis dui vel nibh facilisis venenatis nec molestie mi. Praesent porta suscipit elit, ut porta risus. Nunc gravida euismod convallis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris in ornare lorem.</p> <p>Ut nec nisl erat. Nunc arcu ante, lobortis eu lorem eget, maximus cursus nunc. Etiam tortor tellus, condimentum a libero in, pellentesque dignissim turpis. Phasellus ac sodales dui. Proin at auctor leo. Nunc vehicula ut lorem eget tempus. Quisque lectus dui, auctor vitae ipsum nec, dapibus mollis justo. Donec purus lectus, malesuada non laoreet eu, sodales ac turpis.</p> <p>Mauris venenatis, eros et fringilla vulputate, ante velit euismod nisi, blandit volutpat eros elit nec mi. Donec rutrum felis vel nibh aliquet finibus. Mauris gravida dapibus dui, ut consectetur risus. Sed sagittis dignissim lacus nec ornare. Fusce ullamcorper pretium metus id congue. Vestibulum pellentesque sem orci, eget bibendum magna maximus vel. Sed id lectus ullamcorper, imperdiet velit ac, porttitor est. Duis risus nisi, facilisis at pharetra pellentesque, hendrerit malesuada sapien. Aenean tellus nisl, rhoncus sed nisl quis, sagittis fermentum mauris. Etiam rutrum leo et ligula varius volutpat. Fusce semper hendrerit accumsan. Nam efficitur vitae velit nec condimentum. Vestibulum pellentesque nec purus nec tristique. In libero ante, dictum at urna in, interdum pretium felis. Ut scelerisque sed sem a malesuada.</p> <p>Vestibulum elementum malesuada arcu, id aliquet nisi blandit eu. Proin vel sagittis odio. Maecenas sollicitudin neque non urna malesuada, at convallis sem vulputate. Ut volutpat leo at tincidunt malesuada. Quisque pharetra turpis eget velit commodo, eu scelerisque risus ultrices. Etiam non faucibus libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Mauris in odio massa.</p> </bento-inspector-page> <bento-inspector-page page-id="visa-secure-sub" title="Sub Visa secure" previous-page="visa-secure" > I'm a sub page of visa </bento-inspector-page> <bento-inspector-page page-id="payment-details" :title="title"> <div style="display: flex; flex-direction: column; height: 100%"> <bento-structured-list style="flex-grow: 1"> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> </bento-structured-list> </div> </bento-inspector-page> </template> </bento-inspector> `, setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(args.isOpen); const title = ref(args.title); const activePage = ref(); const inspectorButtonTitle = computed(() => (isOpen.value ? 'Move to inspector' : 'Open inspector')); const gridData = [ { id: 1, paymentMethods: 'Visa', amount: '1,234 USD', numeric: '54,213' }, { id: 2, paymentMethods: 'MasterCard', amount: '1,234 USD', numeric: '54,213' }, { id: 3, paymentMethods: 'iDeal', amount: '1,234 USD', numeric: '54,213' }, { id: 4, paymentMethods: 'Klarna', amount: '1,234 USD', numeric: '54,213' }, { id: 5, paymentMethods: 'Apple Pay', amount: '1,234 USD', numeric: '54,213' }, ]; const onUpdateActivePage = (newActivePage: string) => { activePage.value = newActivePage; }; const openInspector = (value, isSecureView) => { if (isSecureView) { activePage.value = 'visa-secure'; } else { activePage.value = 'payment-details'; title.value = value.paymentMethods; } isOpen.value = true; }; const gridColumns = [ { field: 'paymentMethods', label: 'Payment methods', minWidth: 200, mandatory: true, }, { field: 'amount', label: 'Amount', numeric: true, minWidth: 300 }, { field: 'numeric', label: 'Numeric', numeric: true, minWidth: 300 }, ]; const gridArgs = { data: gridData, columns: gridColumns, pagination: {}, }; const actions = [ { title: 'Visa sub', event: () => { activePage.value = 'visa-secure-sub'; }, }, ]; const headerProps = { title: 'Payment methods list', actions: [ { title: 'Primary action', event: () => undefined }, { title: 'Secondary action', icon: PlusIcon, event: () => undefined }, { title: 'Tertiary action', icon: PlusIcon, event: () => undefined }, { title: 'Another action', event: () => undefined }, ], lastUpdated: new Date(), }; watch( () => args.isOpen, () => { isOpen.value = args.isOpen; } ); watch( () => args.title, () => { title.value = args.title; } ); return { args, isOpen, actions, gridArgs, title, headerProps, activePage, openInspector, onUpdateActivePage, inspectorButtonTitle, }; }, }), args: DEFAULT_PROPS, parameters: storybookDocsParameter(InspectorWithMultiplePagesExample), };
|
|
1
|
+
import { isVue2 } from 'vue-demi'; import { computed, ref, watch } from 'vue'; import { BentoButton } from '@/components/button'; import { BentoDataGrid, type BentoDatagridDataItem } from '@/components/data-grid'; import { BentoStructuredList, BentoStructuredListItem } from '@/components/structured-list'; import { BentoTypography } from '@/components/typography'; import { BentoHeader } from '@/components/header'; import { BentoInspectorPage } from './components/inspector-page/index.js'; import BentoInspector from './inspector.vue'; const InspectorDefaultExample = "<template>\n <bento-inspector\n :is-open=\"isOpen\"\n :title=\"title\"\n :active-page=\"activePage\"\n :relative-layout=\"props.relativeLayout\"\n size=\"large\"\n expandable\n @update:is-open=\"isOpen = $event\"\n @update:active-page=\"activePage = $event\"\n >\n <template #page>\n <div :style=\"{ padding: '16px' }\">\n <bento-header v-bind=\"headerProps\" />\n <bento-data-grid :data=\"gridData\" :columns=\"gridColumns\" :pagination=\"{}\">\n <template #item-paymentMethods=\"{ item }\">\n <div>\n {{ item.paymentMethods }}\n <bento-button\n :aria-label=\"inspectorButtonTitle\"\n variant=\"tertiary\"\n :style=\"{ float: 'right' }\"\n @click=\"openInspector(item)\"\n >\n <template #iconLeft>\n <search-icon aria-hidden=\"true\" />\n </template>\n </bento-button>\n </div>\n </template>\n </bento-data-grid>\n </div>\n </template>\n <template #content>\n <div :style=\"{ display: 'flex', flexDirection: 'column', height: '100%' }\">\n <bento-structured-list :style=\"{ flexGrow: 1 }\">\n <bento-structured-list-item v-for=\"item in inspectorContent\" :key=\"item.label\" :label=\"item.label\">\n <bento-typography>{{ item.value }}</bento-typography>\n </bento-structured-list-item>\n </bento-structured-list>\n <div>\n <bento-button :style=\"{ float: 'right' }\">Action</bento-button>\n </div>\n </div>\n </template>\n </bento-inspector>\n</template>\n\n<script setup lang=\"ts\">\n import {\n BentoButton,\n BentoDataGrid,\n type BentoDatagridDataItem,\n BentoHeader,\n BentoInspector,\n type BentoInspectorRelativeLayout,\n BentoStructuredList,\n BentoStructuredListItem,\n BentoTypography,\n } from '@adyen/bento-vue2';\n import PlusIcon from '@adyen/ui-assets-icons-16/vue/plus';\n import SearchIcon from '@adyen/ui-assets-icons-16/vue/search';\n import { computed, ref } from 'vue';\n\n const props = withDefaults(defineProps<{ relativeLayout?: BentoInspectorRelativeLayout }>(), {\n relativeLayout: 'push-content',\n });\n\n const isOpen = ref(false);\n const title = ref('Title');\n const activePage = ref('');\n\n const inspectorButtonTitle = computed(() => (isOpen.value ? 'Move to inspector' : 'Open inspector'));\n\n const gridData = Array.from({ length: 20 }, (_, index) => ({\n id: index + 1,\n paymentMethods: `Payment method ${index + 1}`,\n amount: '1,234 USD',\n numeric: '54,213',\n }));\n\n const inspectorContent = Array.from({ length: 20 }, (_, index) => ({\n label: `Amount ${index + 1}`,\n value: '1,234 USD',\n }));\n\n const openInspector = (value: BentoDatagridDataItem) => {\n title.value = value.paymentMethods as string;\n isOpen.value = true;\n activePage.value = value.paymentMethods as string;\n };\n\n const gridColumns = [\n {\n field: 'paymentMethods',\n label: 'Payment methods',\n minWidth: 200,\n mandatory: true,\n },\n { field: 'amount', label: 'Amount', numeric: true, minWidth: 300 },\n { field: 'numeric', label: 'Numeric', numeric: true, minWidth: 300 },\n ];\n\n const headerProps = {\n title: 'Payment methods list',\n actions: [\n { title: 'Primary action', event: () => undefined },\n { title: 'Secondary action', icon: PlusIcon, event: () => undefined },\n { title: 'Tertiary action', icon: PlusIcon, event: () => undefined },\n { title: 'Another action', event: () => undefined },\n ],\n lastUpdated: new Date('2023-11-27T12:00:00Z'),\n };\n</script>\n"; const InspectorWithMultiplePagesExample = "<template>\n <bento-inspector\n title=\"Example inspector with multiple pages\"\n :is-open=\"isOpen\"\n :active-page=\"activePage\"\n expandable\n @update:is-open=\"isOpen = $event\"\n @update:active-page=\"onUpdateActivePage\"\n >\n <template #page>\n <div :style=\"{ padding: '16px' }\">\n <bento-header v-bind=\"headerProps\"></bento-header>\n <bento-data-grid v-bind=\"gridArgs\">\n <template #item-paymentMethods=\"{ item }\">\n <div>\n {{ item.paymentMethods }}\n <bento-button\n :aria-label=\"inspectorButtonTitle\"\n variant=\"tertiary\"\n :style=\"{ float: 'right' }\"\n @click=\"openInspector(item)\"\n >\n <template #iconLeft>\n <search-icon aria-hidden=\"true\" />\n </template>\n </bento-button>\n\n <bento-button\n v-if=\"item.paymentMethods === 'Visa'\"\n :aria-label=\"visaSecureButtonTitle\"\n tabindex=\"1\"\n variant=\"tertiary\"\n :style=\"{ float: 'right', marginRight: '10px' }\"\n @click=\"openInspector(item, true)\"\n >\n <template #iconLeft>\n <shield-icon aria-hidden=\"true\" />\n </template>\n </bento-button>\n </div>\n </template>\n </bento-data-grid>\n </div>\n </template>\n <template #content>\n <bento-inspector-page page-id=\"visa-secure\" title=\"Visa secure with a wrap text title\" :actions=\"actions\">\n <p :style=\"{ marginTop: 0 }\">\n Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus accumsan dui sed nulla tincidunt\n fermentum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec tristique erat ut\n tincidunt aliquet. Nunc placerat aliquam consequat. Suspendisse non leo ut sem rutrum aliquet et at\n ex. Mauris facilisis arcu nec ipsum auctor, imperdiet fringilla tortor placerat. Aenean bibendum dui\n id velit ornare, eget porta tortor euismod.\n </p>\n <p>\n Phasellus posuere nec enim sed sodales. Mauris aliquet tristique nisl vitae sollicitudin. Aliquam\n nec diam lorem. Pellentesque eu ex condimentum, rhoncus ipsum et, varius elit. In lacinia elit eros,\n vel euismod nisl vestibulum et. Vivamus quis dui vel nibh facilisis venenatis nec molestie mi.\n Praesent porta suscipit elit, ut porta risus. Nunc gravida euismod convallis. Lorem ipsum dolor sit\n amet, consectetur adipiscing elit. Mauris in ornare lorem.\n </p>\n <p>\n Ut nec nisl erat. Nunc arcu ante, lobortis eu lorem eget, maximus cursus nunc. Etiam tortor tellus,\n condimentum a libero in, pellentesque dignissim turpis. Phasellus ac sodales dui. Proin at auctor\n leo. Nunc vehicula ut lorem eget tempus. Quisque lectus dui, auctor vitae ipsum nec, dapibus mollis\n justo. Donec purus lectus, malesuada non laoreet eu, sodales ac turpis.\n </p>\n <p>\n Mauris venenatis, eros et fringilla vulputate, ante velit euismod nisi, blandit volutpat eros elit\n nec mi. Donec rutrum felis vel nibh aliquet finibus. Mauris gravida dapibus dui, ut consectetur\n risus. Sed sagittis dignissim lacus nec ornare. Fusce ullamcorper pretium metus id congue.\n Vestibulum pellentesque sem orci, eget bibendum magna maximus vel. Sed id lectus ullamcorper,\n imperdiet velit ac, porttitor est. Duis risus nisi, facilisis at pharetra pellentesque, hendrerit\n malesuada sapien. Aenean tellus nisl, rhoncus sed nisl quis, sagittis fermentum mauris. Etiam rutrum\n leo et ligula varius volutpat. Fusce semper hendrerit accumsan. Nam efficitur vitae velit nec\n condimentum. Vestibulum pellentesque nec purus nec tristique. In libero ante, dictum at urna in,\n interdum pretium felis. Ut scelerisque sed sem a malesuada.\n </p>\n <p>\n Vestibulum elementum malesuada arcu, id aliquet nisi blandit eu. Proin vel sagittis odio. Maecenas\n sollicitudin neque non urna malesuada, at convallis sem vulputate. Ut volutpat leo at tincidunt\n malesuada. Quisque pharetra turpis eget velit commodo, eu scelerisque risus ultrices. Etiam non\n faucibus libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos\n himenaeos. Mauris in odio massa.\n </p>\n </bento-inspector-page>\n\n <bento-inspector-page page-id=\"visa-secure-sub\" title=\"Sub Visa secure\" previous-page=\"visa-secure\">\n I'm a sub page of visa\n </bento-inspector-page>\n\n <bento-inspector-page page-id=\"payment-details\" :title=\"title\">\n <div :style=\"{ display: 'flex', flexDirection: 'column', height: '100%' }\">\n <bento-structured-list :style=\"{ flexGrow: 1 }\">\n <bento-structured-list-item label=\"Amount\">\n <bento-typography>1,234 USD</bento-typography>\n </bento-structured-list-item>\n <bento-structured-list-item label=\"Amount\">\n <bento-typography>1,234 USD</bento-typography>\n </bento-structured-list-item>\n <bento-structured-list-item label=\"Amount\">\n <bento-typography>1,234 USD</bento-typography>\n </bento-structured-list-item>\n <bento-structured-list-item label=\"Amount\">\n <bento-typography>1,234 USD</bento-typography>\n </bento-structured-list-item>\n <bento-structured-list-item label=\"Amount\">\n <bento-typography>1,234 USD</bento-typography>\n </bento-structured-list-item>\n </bento-structured-list>\n </div>\n </bento-inspector-page>\n </template>\n </bento-inspector>\n</template>\n\n<script setup lang=\"ts\">\n import { computed, ref } from 'vue';\n import {\n BentoButton,\n BentoDataGrid,\n BentoHeader,\n BentoInspectorPage,\n BentoStructuredList,\n BentoStructuredListItem,\n BentoTypography,\n } from '@adyen/bento-vue2';\n\n import PlusIcon from '@adyen/ui-assets-icons-16/vue/plus';\n import SearchIcon from '@adyen/ui-assets-icons-16/vue/search';\n import ShieldIcon from '@adyen/ui-assets-icons-16/vue/shield';\n import BentoInspector from '../inspector.vue';\n\n const isOpen = ref<boolean>(false);\n // Title only used in the `bento-inspector-page` component\n const title = ref('');\n const activePage = ref();\n\n const inspectorButtonTitle = computed(() => (isOpen.value ? 'Move to simple details' : 'Open simple details'));\n const visaSecureButtonTitle = computed(() =>\n isOpen.value ? 'Move to visa secure details' : 'Open visa secure details'\n );\n\n const gridData = [\n { id: 1, paymentMethods: 'Visa', amount: '1,234 USD', numeric: '54,213' },\n { id: 2, paymentMethods: 'MasterCard', amount: '1,234 USD', numeric: '54,213' },\n { id: 3, paymentMethods: 'iDeal', amount: '1,234 USD', numeric: '54,213' },\n { id: 4, paymentMethods: 'Klarna', amount: '1,234 USD', numeric: '54,213' },\n { id: 5, paymentMethods: 'Apple Pay', amount: '1,234 USD', numeric: '54,213' },\n ];\n\n const onUpdateActivePage = (newActivePage: string) => {\n activePage.value = newActivePage;\n };\n\n const openInspector = (value, isSecureView?: boolean) => {\n if (isSecureView) {\n activePage.value = 'visa-secure';\n } else {\n activePage.value = 'payment-details';\n title.value = value.paymentMethods;\n }\n isOpen.value = true;\n };\n\n const gridColumns = [\n {\n field: 'paymentMethods',\n label: 'Payment methods',\n minWidth: 200,\n mandatory: true,\n },\n { field: 'amount', label: 'Amount', numeric: true, minWidth: 300 },\n { field: 'numeric', label: 'Numeric', numeric: true, minWidth: 300 },\n ];\n\n const gridArgs = {\n data: gridData,\n columns: gridColumns,\n pagination: {},\n };\n\n const actions = [\n {\n title: 'Visa sub-page',\n event: () => {\n activePage.value = 'visa-secure-sub';\n },\n },\n ];\n\n const headerProps = {\n title: 'Payment methods list',\n actions: [\n { title: 'Primary action', event: () => undefined },\n { title: 'Secondary action', icon: PlusIcon, event: () => undefined },\n { title: 'Tertiary action', icon: PlusIcon, event: () => undefined },\n { title: 'Another action', event: () => undefined },\n ],\n lastUpdated: new Date(),\n };\n</script>\n"; import PlusIcon from '@adyen/ui-assets-icons-16/vue/plus'; import SearchIcon from '@adyen/ui-assets-icons-16/vue/search'; import ShieldIcon from '@adyen/ui-assets-icons-16/vue/shield'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import type { Meta, StoryObj } from '@storybook/vue'; import { BentoInspectorSizeOptions } from './inspector.types'; import './inspector.stories.scss?module'; const meta: Meta = { title: 'Inspector', component: BentoInspector, parameters: { layout: 'fullscreen', }, argTypes: { size: { options: BentoInspectorSizeOptions, control: { type: 'select', }, }, relativeLayout: { options: ['push-content', 'overlay-content'], control: { type: 'select', }, }, }, }; export default meta; type Story = StoryObj<typeof BentoInspector>; const DEFAULT_PROPS = { title: 'Title', size: 'large', isOpen: false, activePage: '', relativeLayout: 'push-content', }; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoDataGrid, BentoHeader, BentoInspector, BentoStructuredList, BentoStructuredListItem, BentoTypography, SearchIcon, }, props: Object.keys(argTypes), template: ` <bento-inspector v-bind="args" :is-open="isOpen" :title="title" :active-page="activePage" @update:is-open="isOpen = $event" @update:active-page="activePage = $event" expandable > <template #page> <div style="padding:16px"> <bento-header v-bind="headerProps"></bento-header> <bento-data-grid v-bind="gridArgs"> <template #item-paymentMethods="{item}"> <div> {{ item.paymentMethods }} <bento-button variant="tertiary" @click="openInspector(item)" style="float:right" > <template #iconLeft> <search-icon :svg-title="inspectorButtonTitle" /> </template> </bento-button> </div> </template> </bento-data-grid> </div> </template> <template #content> <div style="display: flex; flex-direction: column; height: 100%"> <bento-structured-list style="flex-grow: 1"> <bento-structured-list-item v-for="item in inspectorContent" :key="item.label" :label="item.label" > <bento-typography>{{ item.value }}</bento-typography> </bento-structured-list-item> </bento-structured-list> <div> <bento-button style="float: right">Action</bento-button> </div> </div> </template> </bento-inspector> `, setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(args.isOpen); const title = ref(args.title); const activePage = ref(''); const inspectorButtonTitle = computed(() => (isOpen.value ? 'Move to inspector' : 'Open inspector')); const gridData = Array.from({ length: 20 }, (_, index) => ({ id: index + 1, paymentMethods: `Payment method ${index + 1}`, amount: '1,234 USD', numeric: '54,213', })); const inspectorContent = Array.from({ length: 20 }, (_, index) => ({ label: `Amount ${index + 1}`, value: '1,234 USD', })); const openInspector = (value: BentoDatagridDataItem) => { title.value = value.paymentMethods as string; isOpen.value = true; activePage.value = value.paymentMethods as string; }; const gridColumns = [ { field: 'paymentMethods', label: 'Payment methods', minWidth: 200, mandatory: true, }, { field: 'amount', label: 'Amount', numeric: true, minWidth: 300 }, { field: 'numeric', label: 'Numeric', numeric: true, minWidth: 300 }, ]; const gridArgs = { data: gridData, columns: gridColumns, pagination: {}, }; const headerProps = { title: 'Payment methods list', actions: [ { title: 'Primary action', event: () => undefined }, { title: 'Secondary action', icon: PlusIcon, event: () => undefined }, { title: 'Tertiary action', icon: PlusIcon, event: () => undefined }, { title: 'Another action', event: () => undefined }, ], lastUpdated: new Date('2023-11-27T12:00:00Z'), }; watch( () => args.isOpen, () => { isOpen.value = args.isOpen; } ); watch( () => args.title, () => { title.value = args.title; } ); watch( () => args.activePage, () => { activePage.value = args.activePage; } ); return { args, isOpen, gridArgs, title, headerProps, inspectorContent, activePage, openInspector, inspectorButtonTitle, }; }, }), args: DEFAULT_PROPS, parameters: storybookDocsParameter(InspectorDefaultExample), }; export const MultiplePages: Story = { render: (_args, { argTypes }) => ({ components: { BentoButton, BentoDataGrid, BentoHeader, BentoInspector, BentoInspectorPage, BentoStructuredList, BentoStructuredListItem, BentoTypography, SearchIcon, ShieldIcon, }, props: Object.keys(argTypes), template: ` <bento-inspector v-bind="args" :is-open="isOpen" :active-page="activePage" title="What if this title is really really long? So long that it should overflow the content on the right where the buttons are" @update:is-open="isOpen = $event" @update:active-page="onUpdateActivePage" expandable > <template #page> <div style="padding:16px"> <bento-header v-bind="headerProps"></bento-header> <bento-data-grid v-bind="gridArgs"> <template #item-paymentMethods="{item}"> <div> {{ item.paymentMethods }} <bento-button variant="tertiary" @click="openInspector(item)" style="float:right" > <template #iconLeft> <search-icon :svg-title="inspectorButtonTitle" /> </template> </bento-button> <bento-button v-if="item.paymentMethods === 'Visa'" variant="tertiary" @click="openInspector(item, true)" style="float:right; margin-right: 10px;" > <template #iconLeft> <shield-icon :svg-title="inspectorButtonTitle" /> </template> </bento-button> </div> </template> </bento-data-grid> </div> </template> <template #content> <bento-inspector-page page-id="visa-secure" title="Visa secure with a wrap text title" :actions="actions"> <p style="margin-top: 0;">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus accumsan dui sed nulla tincidunt fermentum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec tristique erat ut tincidunt aliquet. Nunc placerat aliquam consequat. Suspendisse non leo ut sem rutrum aliquet et at ex. Mauris facilisis arcu nec ipsum auctor, imperdiet fringilla tortor placerat. Aenean bibendum dui id velit ornare, eget porta tortor euismod.</p> <p>Phasellus posuere nec enim sed sodales. Mauris aliquet tristique nisl vitae sollicitudin. Aliquam nec diam lorem. Pellentesque eu ex condimentum, rhoncus ipsum et, varius elit. In lacinia elit eros, vel euismod nisl vestibulum et. Vivamus quis dui vel nibh facilisis venenatis nec molestie mi. Praesent porta suscipit elit, ut porta risus. Nunc gravida euismod convallis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris in ornare lorem.</p> <p>Ut nec nisl erat. Nunc arcu ante, lobortis eu lorem eget, maximus cursus nunc. Etiam tortor tellus, condimentum a libero in, pellentesque dignissim turpis. Phasellus ac sodales dui. Proin at auctor leo. Nunc vehicula ut lorem eget tempus. Quisque lectus dui, auctor vitae ipsum nec, dapibus mollis justo. Donec purus lectus, malesuada non laoreet eu, sodales ac turpis.</p> <p>Mauris venenatis, eros et fringilla vulputate, ante velit euismod nisi, blandit volutpat eros elit nec mi. Donec rutrum felis vel nibh aliquet finibus. Mauris gravida dapibus dui, ut consectetur risus. Sed sagittis dignissim lacus nec ornare. Fusce ullamcorper pretium metus id congue. Vestibulum pellentesque sem orci, eget bibendum magna maximus vel. Sed id lectus ullamcorper, imperdiet velit ac, porttitor est. Duis risus nisi, facilisis at pharetra pellentesque, hendrerit malesuada sapien. Aenean tellus nisl, rhoncus sed nisl quis, sagittis fermentum mauris. Etiam rutrum leo et ligula varius volutpat. Fusce semper hendrerit accumsan. Nam efficitur vitae velit nec condimentum. Vestibulum pellentesque nec purus nec tristique. In libero ante, dictum at urna in, interdum pretium felis. Ut scelerisque sed sem a malesuada.</p> <p>Vestibulum elementum malesuada arcu, id aliquet nisi blandit eu. Proin vel sagittis odio. Maecenas sollicitudin neque non urna malesuada, at convallis sem vulputate. Ut volutpat leo at tincidunt malesuada. Quisque pharetra turpis eget velit commodo, eu scelerisque risus ultrices. Etiam non faucibus libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Mauris in odio massa.</p> </bento-inspector-page> <bento-inspector-page page-id="visa-secure-sub" title="Sub Visa secure" previous-page="visa-secure" > I'm a sub page of visa </bento-inspector-page> <bento-inspector-page page-id="payment-details" :title="title"> <div style="display: flex; flex-direction: column; height: 100%"> <bento-structured-list style="flex-grow: 1"> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> <bento-structured-list-item label="Amount"> <bento-typography>1,234 USD</bento-typography> </bento-structured-list-item> </bento-structured-list> </div> </bento-inspector-page> </template> </bento-inspector> `, setup(props) { const args = isVue2 ? props : _args; const isOpen = ref(args.isOpen); const title = ref(args.title); const activePage = ref(); const inspectorButtonTitle = computed(() => (isOpen.value ? 'Move to inspector' : 'Open inspector')); const gridData = [ { id: 1, paymentMethods: 'Visa', amount: '1,234 USD', numeric: '54,213' }, { id: 2, paymentMethods: 'MasterCard', amount: '1,234 USD', numeric: '54,213' }, { id: 3, paymentMethods: 'iDeal', amount: '1,234 USD', numeric: '54,213' }, { id: 4, paymentMethods: 'Klarna', amount: '1,234 USD', numeric: '54,213' }, { id: 5, paymentMethods: 'Apple Pay', amount: '1,234 USD', numeric: '54,213' }, ]; const onUpdateActivePage = (newActivePage: string) => { activePage.value = newActivePage; }; const openInspector = (value, isSecureView) => { if (isSecureView) { activePage.value = 'visa-secure'; } else { activePage.value = 'payment-details'; title.value = value.paymentMethods; } isOpen.value = true; }; const gridColumns = [ { field: 'paymentMethods', label: 'Payment methods', minWidth: 200, mandatory: true, }, { field: 'amount', label: 'Amount', numeric: true, minWidth: 300 }, { field: 'numeric', label: 'Numeric', numeric: true, minWidth: 300 }, ]; const gridArgs = { data: gridData, columns: gridColumns, pagination: {}, }; const actions = [ { title: 'Visa sub', event: () => { activePage.value = 'visa-secure-sub'; }, }, ]; const headerProps = { title: 'Payment methods list', actions: [ { title: 'Primary action', event: () => undefined }, { title: 'Secondary action', icon: PlusIcon, event: () => undefined }, { title: 'Tertiary action', icon: PlusIcon, event: () => undefined }, { title: 'Another action', event: () => undefined }, ], lastUpdated: new Date(), }; watch( () => args.isOpen, () => { isOpen.value = args.isOpen; } ); watch( () => args.title, () => { title.value = args.title; } ); return { args, isOpen, actions, gridArgs, title, headerProps, activePage, openInspector, onUpdateActivePage, inspectorButtonTitle, }; }, }), args: DEFAULT_PROPS, parameters: storybookDocsParameter(InspectorWithMultiplePagesExample), };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div ref="wrapper" class="b-inspector" :class="conditionalClass" @keydown="handleSectionFocus"> <div ref="page" class="b-inspector__page-wrapper"> <div class="b-inspector__page"> <slot name="page"></slot> </div> </div> <Transition name="b-inspector__animation" mode="out-in" @before-enter="setFocus" @after-leave="setFocus"> <aside v-if="isOpen" ref="inspector" class="b-inspector__container-wrapper" :aria-label="inspectorAriaLabel" @keydown.tab="handleTabNavigation" > <bento-focus-trap :disabled="!isCompact" class="b-inspector__focus-trap"> <div class="b-inspector__container" :class="computedContainerClasses"> <template v-if="!isCompact"> <bento-button class="b-inspector__return-content" variant="secondary" condensed :aria-describedby="returnToMainContentDescriptionId" @click="moveFocusBetweenSections" > {{ t('returnToMainContent') }} </bento-button> <span :id="returnToMainContentDescriptionId" class="b-inspector__return-content-description" > {{ t('returnToMainContentDescription') }} </span> </template> <div v-if="pages.length === 0" class="b-inspector__header"> <bento-typography el="h2" variant="title" class="b-inspector__title"> {{ title }} </bento-typography> </div> <div class="b-inspector__icons"> <bento-button v-if="expandable" variant="tertiary" @click="emit('expand')"> <template #iconLeft> <expand-icon :svg-title="t('expand')" /> </template> </bento-button> <bento-button variant="tertiary" @click="emit('update:is-open', false)"> <template #iconLeft> <cross-icon :svg-title="t('close')" /> </template> </bento-button> </div> <div class="b-inspector__content"> <bento-loading-indicator v-if="loading" large class="b-inspector__loading-indicator" /> <slot v-else name="content"></slot> </div> </div> </bento-focus-trap> </aside> </Transition> </div> </template> <script setup lang="ts"> import { computed, nextTick, provide, ref, toRef, watch } from 'vue'; import { BentoTypography } from '@/components/typography'; import { BentoButton } from '@/components/button'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoFocusTrap } from '@/components/focus-trap'; import ExpandIcon from '@adyen/ui-assets-icons-16/vue/expand'; import CrossIcon from '@adyen/ui-assets-icons-16/vue/cross'; import { generateUid } from '@/core/utils/ts'; import { useI18n } from '@/utils/ts/i18n'; import { deprecate } from '@/utils/ts/deprecate'; import { getNextFocusableElement } from '@/utils/ts/focus-trap.utils'; import { useElementSize } from '@vueuse/core'; import moveFocusInside, { focusInside, getActiveElement } from 'focus-lock'; import { type BentoInspectorProps } from './inspector.types'; import { useFocusHistory } from './composables/use-focus-history/use-focus-history'; import { DIALOG_PAGE_CONFIG_INJECTION_KEY, DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY, } from '@/internal/dialog-page/dialog-page.keys'; import { BentoDialogPageAnimation, type BentoDialogPageConfigData, type BentoDialogPageRegisterPage, } from '@/internal/dialog-page/dialog-page.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoInspectorProps>(), { activePage: '', expandable: false, isOpen: false, loading: false, size: 'small', relativeLayout: 'push-content', pageFocusAnchor: null, }); const emit = defineEmits<{ /** * Emits update to keep isOpen prop in sync */ (e: 'update:is-open', value: boolean): void; /** * Emits update to keep activePage prop in sync */ (e: 'update:active-page', value: string): void; /** * Emits event when user click expand button */ (e: 'expand'): void; }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const returnToMainContentDescriptionId = generateUid('returnToMainContentDescription'); const wrapper = ref<HTMLElement>(null); const inspector = ref<HTMLElement>(null); const page = ref<HTMLElement>(null); const inspectorLastFocusedElement = ref<HTMLElement>(null); const internalActivePage = ref(props.activePage); // Multiple page navigation const pages = ref<Array<string>>([]); const previousPages = ref([]); const animation = ref(BentoDialogPageAnimation.TRANSITION_LEVEL_DEEPER); const isAnimationActive = ref(false); const { width: wrapperWidth } = useElementSize(wrapper); const computedContainerClasses = computed(() => { return { 'b-inspector__container--padded': pages.value.length === 0, // No pages are registered }; }); const isCompact = computed(() => { const COMPACT_THRESHOLD_SMALL = 740; const COMPACT_THRESHOLD_LARGE = 915; // small inspector to occupy full screen if < 740px // large inspector to occupy full screen if < 915px const threshold = props.size === 'small' ? COMPACT_THRESHOLD_SMALL : COMPACT_THRESHOLD_LARGE; return wrapperWidth.value > 0 && wrapperWidth.value < threshold; }); const computedPageLastFocusedElement = computed(() => props.pageFocusAnchor); const conditionalClass = computed(() => ({ [`b-inspector--${props.size}`]: true, [`b-inspector--${props.relativeLayout}`]: true, [`b-inspector--is-compact`]: isCompact.value, [`b-inspector--multi-paged`]: pages.value.length, })); const inspectorAriaLabel = computed(() => `${t('inspector')}, ${props.title}`); const { findValidFocusFallback } = useFocusHistory(page); const setFocus = async () => { if (props.isOpen) { await nextTick(); moveFocusInside(inspector.value, computedPageLastFocusedElement.value); } else { // This doesn't work with data grid cell actions, as the button gets destroyed once is not being hovered. // To be reviewed after LevelAccess revision focusElement(computedPageLastFocusedElement.value ?? findValidFocusFallback()); } }; const handleTabNavigation = event => { if (isCompact.value) { return; } const direction = event.shiftKey ? -1 : 1; const focusableEl = getNextFocusableElement(direction, inspector.value); // If the nextfocusableEl is the same as the activeElement that means it's the last focusable element // inside the inspector, so the focus is moved back to the last focused element on the page if (getActiveElement() === focusableEl) { event.preventDefault(); inspectorLastFocusedElement.value = document.activeElement as HTMLElement; focusElement(computedPageLastFocusedElement.value ?? findValidFocusFallback()); // Set active-page as undefined when focus is not on page emit('update:active-page', undefined); } }; const handleSectionFocus = event => { const sectionShortcut = event.shiftKey && event.ctrlKey && event.key === 'S'; if (props.isOpen && !isCompact.value && sectionShortcut) { moveFocusBetweenSections(); } }; const moveFocusBetweenSections = () => { if (focusInside(inspector.value)) { inspectorLastFocusedElement.value = document.activeElement as HTMLElement; focusElement(computedPageLastFocusedElement.value ?? findValidFocusFallback()); // Set active-page as undefined when focus is not on page emit('update:active-page', undefined); } else { focusElement(inspectorLastFocusedElement.value); } }; const focusElement = (element: HTMLElement) => { if (!element) { return; } element?.focus(); if (document.activeElement !== element) { focusElement(element.parentElement); } }; /** * Multiple page navigation and page registration */ const updatePage = page => emit('update:active-page', page); // unregister page if removed / destroyed const unregisterPage = (pageIdToRemove: string) => () => { pages.value = pages.value?.filter(pageId => pageId !== pageIdToRemove); }; const registerPage: BentoDialogPageRegisterPage = pageId => { pages.value.push(pageId); return { unregisterPage: unregisterPage(pageId) }; }; provide<BentoDialogPageConfigData>(DIALOG_PAGE_CONFIG_INJECTION_KEY, { activePage: internalActivePage, animation, isAnimationActive, isOpen: toRef(props, 'isOpen'), pages, previousPages, updatePage, }); provide(DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY, registerPage); watch( () => props.activePage, () => { if (props.activePage) { internalActivePage.value = props.activePage; setFocus(); } } ); if (!props.activePage) { deprecate( 'BentoInspector "active-page" as optional', `Make sure to set the "active-page" prop and keep it in sync with "update:active-page". "active-page" prop will be required in the next version`, '2.0.0' ); } </script> <script lang="ts"> /** * Inspector allows users to access additional information or details of an object/item. The inspector opens adjacent to the main content, pushing it aside to maintain visibility and accessibility to both the main content and inspector. * * @example * import { BentoInspector } from '@adyen/bento-vue2'; * * export default { * components: { BentoInspector }, * template: ` * <bento-inspector * title="Inspector" * size="large" * :is-open="isOpen" * :active-page="activePage" * expandable * @update:is-open="isOpen = $event" * @update:active-page="activePage = $event" * > * <template #page>Content of the page</template> * <template #content>Content of the inspector</template> * </bento-inspector> * ` * } */ export default { i18n: { messages } }; </script> <style lang="scss" scoped src="./inspector.scss" />
|
|
1
|
+
<template> <div ref="wrapper" class="b-inspector" :class="conditionalClass" @keydown="handleSectionFocus"> <div ref="page" class="b-inspector__page-wrapper"> <div class="b-inspector__page"> <slot name="page"></slot> </div> </div> <Transition name="b-inspector__animation" mode="out-in" @before-enter="setFocus" @after-leave="setFocus"> <aside v-if="isOpen" ref="inspector" class="b-inspector__container-wrapper" :aria-label="inspectorAriaLabel" @keydown.tab="handleTabNavigation" > <bento-focus-trap :disabled="!isCompact" class="b-inspector__focus-trap"> <div class="b-inspector__container" :class="computedContainerClasses"> <template v-if="!isCompact"> <bento-button ref="returnContentButton" class="b-inspector__return-content" variant="secondary" condensed :aria-describedby="returnToMainContentDescriptionId" @click="moveFocusBetweenSections" > {{ t('returnToMainContent') }} </bento-button> <span :id="returnToMainContentDescriptionId" class="b-inspector__return-content-description" > {{ t('returnToMainContentDescription') }} </span> </template> <div v-if="pages.length === 0" class="b-inspector__header"> <bento-typography el="h2" variant="title" class="b-inspector__title"> {{ title }} </bento-typography> </div> <div class="b-inspector__content"> <bento-loading-indicator v-if="loading" large class="b-inspector__loading-indicator" /> <slot v-else name="content"></slot> </div> <div class="b-inspector__icons"> <bento-button v-if="expandable" ref="expandButton" variant="tertiary" :aria-label="t('expand')" @click="emit('expand')" > <template #iconLeft> <expand-icon aria-hidden="true" /> </template> </bento-button> <bento-button ref="closeButton" :aria-label="t('close')" variant="tertiary" @click="emit('update:is-open', false)" > <template #iconLeft> <cross-icon aria-hidden="true" /> </template> </bento-button> </div> </div> </bento-focus-trap> </aside> </Transition> </div> </template> <script setup lang="ts"> import { computed, nextTick, provide, ref, toRef, watch } from 'vue'; import { BentoTypography } from '@/components/typography'; import { BentoButton } from '@/components/button'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoFocusTrap } from '@/components/focus-trap'; import ExpandIcon from '@adyen/ui-assets-icons-16/vue/expand'; import CrossIcon from '@adyen/ui-assets-icons-16/vue/cross'; import { generateUid } from '@/utils/ts/generate-uid'; import { useI18n } from '@/utils/ts/i18n'; import { deprecate } from '@/utils/ts/deprecate'; import { useElementSize } from '@vueuse/core'; import moveFocusInside, { focusInside, getActiveElement, getFocusableIn } from 'focus-lock'; import { type BentoInspectorProps } from './inspector.types'; import { useFocusHistory } from './composables/use-focus-history/use-focus-history'; import { DIALOG_PAGE_CONFIG_INJECTION_KEY, DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY, } from '@/internal/dialog-page/dialog-page.keys'; import { BentoDialogPageAnimation, type BentoDialogPageConfigData, type BentoDialogPageRegisterPage, } from '@/internal/dialog-page/dialog-page.types'; import messages from './messages.json'; type MessageSchema = (typeof messages)['en-US']; const props = withDefaults(defineProps<BentoInspectorProps>(), { activePage: '', expandable: false, isOpen: false, loading: false, size: 'small', relativeLayout: 'push-content', pageFocusAnchor: null, }); const emit = defineEmits<{ /** * Emits update to keep isOpen prop in sync */ (e: 'update:is-open', value: boolean): void; /** * Emits update to keep activePage prop in sync */ (e: 'update:active-page', value: string): void; /** * Emits event when user click expand button */ (e: 'expand'): void; }>(); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const returnToMainContentDescriptionId = generateUid('returnToMainContentDescription'); const wrapper = ref<HTMLElement>(null); const inspector = ref<HTMLElement>(null); const page = ref<HTMLElement>(null); // These refs allow keyboard navigation to follow the intended order independently of the DOM order. const returnContentButton = ref(null); const expandButton = ref(null); const closeButton = ref(null); const inspectorLastFocusedElement = ref<HTMLElement>(null); const internalActivePage = ref(props.activePage); // Multiple page navigation const pages = ref<Array<string>>([]); const previousPages = ref([]); const animation = ref(BentoDialogPageAnimation.TRANSITION_LEVEL_DEEPER); const isAnimationActive = ref(false); const { width: wrapperWidth } = useElementSize(wrapper); const computedContainerClasses = computed(() => { return { 'b-inspector__container--padded': pages.value.length === 0, // No pages are registered }; }); const isCompact = computed(() => { const COMPACT_THRESHOLD_SMALL = 740; const COMPACT_THRESHOLD_LARGE = 915; // small inspector to occupy full screen if < 740px // large inspector to occupy full screen if < 915px const threshold = props.size === 'small' ? COMPACT_THRESHOLD_SMALL : COMPACT_THRESHOLD_LARGE; return wrapperWidth.value > 0 && wrapperWidth.value < threshold; }); const computedPageLastFocusedElement = computed(() => props.pageFocusAnchor); const conditionalClass = computed(() => ({ [`b-inspector--${props.size}`]: true, [`b-inspector--${props.relativeLayout}`]: true, [`b-inspector--is-compact`]: isCompact.value, [`b-inspector--multi-paged`]: pages.value.length, })); const inspectorAriaLabel = computed(() => `${t('inspector')}, ${props.title}`); const { findValidFocusFallback } = useFocusHistory(page); /** * Returns the Inspector's intended tab order. * * The content must remain before the icon buttons in the DOM for the Inspector animations, * so the keyboard order is managed separately. The return button is omitted automatically in compact mode. */ const getOrderedFocusableElements = () => { const navigationButtons = [ returnContentButton.value?.$el, expandButton.value?.$el, closeButton.value?.$el, ].filter(Boolean) as Array<HTMLElement>; const contentFocusableElements = getFocusableIn(inspector.value) .map(({ node }) => node) .filter(element => !navigationButtons.includes(element)); return [...navigationButtons, ...contentFocusableElements]; }; const setFocus = async () => { if (props.isOpen) { await nextTick(); if (isCompact.value) { // The compact focus trap uses DOM order when it initially moves focus. // Explicitly focus the first ordered control so content does not skip the icon buttons. focusElement(getOrderedFocusableElements()[0]); } else { moveFocusInside(inspector.value, computedPageLastFocusedElement.value); } } else { // This doesn't work with data grid cell actions, as the button gets destroyed once is not being hovered. // To be reviewed after LevelAccess revision focusElement(computedPageLastFocusedElement.value ?? findValidFocusFallback()); } }; const handleTabNavigation = (event: KeyboardEvent) => { // Prevent the browser's DOM-based tab order and use the Inspector-specific order instead. const direction = event.shiftKey ? -1 : 1; const focusableElements = getOrderedFocusableElements(); const currentIndex = focusableElements.indexOf(getActiveElement()); const nextIndex = currentIndex + direction; // Compact inspectors trap focus, so navigation should wrap around instead of returning focus to the page. if (isCompact.value && focusableElements.length > 0) { event.preventDefault(); focusableElements[(nextIndex + focusableElements.length) % focusableElements.length].focus(); return; } const focusableEl = focusableElements[nextIndex]; // At either end of the Inspector, return focus to the page. if (!focusableEl) { event.preventDefault(); inspectorLastFocusedElement.value = document.activeElement as HTMLElement; focusElement(computedPageLastFocusedElement.value ?? findValidFocusFallback()); // Set active-page as undefined when focus is not on page emit('update:active-page', undefined); return; } event.preventDefault(); focusableEl.focus(); }; const handleSectionFocus = event => { const sectionShortcut = event.shiftKey && event.ctrlKey && event.key === 'S'; if (props.isOpen && !isCompact.value && sectionShortcut) { moveFocusBetweenSections(); } }; const moveFocusBetweenSections = () => { if (focusInside(inspector.value)) { inspectorLastFocusedElement.value = document.activeElement as HTMLElement; focusElement(computedPageLastFocusedElement.value ?? findValidFocusFallback()); // Set active-page as undefined when focus is not on page emit('update:active-page', undefined); } else { focusElement(inspectorLastFocusedElement.value); } }; const focusElement = (element: HTMLElement) => { if (!element) { return; } element?.focus(); if (document.activeElement !== element) { focusElement(element.parentElement); } }; /** * Multiple page navigation and page registration */ const updatePage = page => emit('update:active-page', page); // unregister page if removed / destroyed const unregisterPage = (pageIdToRemove: string) => () => { pages.value = pages.value?.filter(pageId => pageId !== pageIdToRemove); }; const registerPage: BentoDialogPageRegisterPage = pageId => { pages.value.push(pageId); return { unregisterPage: unregisterPage(pageId) }; }; provide<BentoDialogPageConfigData>(DIALOG_PAGE_CONFIG_INJECTION_KEY, { activePage: internalActivePage, animation, isAnimationActive, isOpen: toRef(props, 'isOpen'), pages, previousPages, updatePage, }); provide(DIALOG_PAGE_REGISTER_PAGE_INJECTION_KEY, registerPage); watch( () => props.activePage, () => { if (props.activePage) { internalActivePage.value = props.activePage; setFocus(); } } ); if (!props.activePage) { deprecate( 'BentoInspector "active-page" as optional', `Make sure to set the "active-page" prop and keep it in sync with "update:active-page". "active-page" prop will be required in the next version`, '3.0.0' ); } </script> <script lang="ts"> /** * Inspector allows users to access additional information or details of an object/item. The inspector opens adjacent to the main content, pushing it aside to maintain visibility and accessibility to both the main content and inspector. * * @example * import { BentoInspector } from '@adyen/bento-vue2'; * * export default { * components: { BentoInspector }, * template: ` * <bento-inspector * title="Inspector" * size="large" * :is-open="isOpen" * :active-page="activePage" * expandable * @update:is-open="isOpen = $event" * @update:active-page="activePage = $event" * > * <template #page>Content of the page</template> * <template #content>Content of the inspector</template> * </bento-inspector> * ` * } */ export default { i18n: { messages } }; </script> <style lang="scss" scoped src="./inspector.scss" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { isVue2 } from 'vue-demi'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import type { Meta, StoryObj } from '@storybook/vue'; import { BentoLayout, BentoLayoutRow } from '@/components/layout'; import { BentoLayoutRowColumnLayoutOptions } from '@/components/layout/layout.types'; import './layout-row.stories.scss?module'; const meta: Meta = { title: 'Layout/Layout row', component: BentoLayoutRow, argTypes: { columnsLayout: { options: BentoLayoutRowColumnLayoutOptions, control: { type: 'select', }, }, default: { name: 'default', type: { name: 'string' }, description: 'Slot for a single column row, does not use layout.', control: 'text', table: { category: 'slots' }, }, // Need to set slots as col1 and col2 and hide col-1 and col-2 because args don't support args['col-1'] col1: { name: 'col-1', description: 'Slot for the content in the first column', control: 'text', table: { category: 'slots' }, }, col2: { name: 'col-2', description: 'Slot for the content in the second column', control: 'text', table: { category: 'slots' }, }, ['col-1']: { table: { disable: true }, }, ['col-2']: { table: { disable: true }, }, }, parameters: { layout: 'fullscreen', }, }; export default meta; type Story = StoryObj<typeof BentoLayout>; const defaultCode = ` <template> <bento-layout size="full-width"> <bento-layout-row columns-layout="3-9"> <template #col-1>3 cols</template> <template #col-2>9 cols</template> </bento-layout-row> </bento-layout> </template> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoLayout, BentoLayoutRow, }, props: Object.keys(argTypes), template: ` <bento-layout size="full-width"> <bento-layout-row v-bind="args"> <template #default v-if="args.default"><div class="b-layout-row-story__cell">{{ args.default }}</div></template> <template #col-1 v-if="args.col1"><div class="b-layout-row-story__cell">{{ args.col1 }}</div></template> <template #col-2 v-if="args.col2"><div class="b-layout-row-story__cell">{{ args.col2 }}</div></template> </bento-layout-row> </bento-layout> `, setup(props) { const args = isVue2 ? props : _args; return { args, }; }, }), args: { columnsLayout: '3-9', col1: 'Col 1', col2: 'Col 2', }, parameters: storybookDocsParameter(defaultCode), };
|
|
1
|
+
import { isVue2 } from 'vue-demi'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import type { Meta, StoryObj } from '@storybook/vue'; import { BentoDataGrid } from '@/components/data-grid'; import { BentoLayout, BentoLayoutRow } from '@/components/layout'; import { BentoLayoutRowColumnLayoutOptions } from '@/components/layout/layout.internal.types'; import './layout-row.stories.scss?module'; const FullHeightExample = "<template>\n <bento-layout variant=\"component\" size=\"full-width\" style=\"height: 100vh\">\n <bento-layout-row full-height>\n <bento-data-grid :columns=\"columns\" :data=\"data\" :pagination=\"pagination\" fit-content sticky-pagination />\n </bento-layout-row>\n </bento-layout>\n</template>\n\n<script setup lang=\"ts\">\n import { ref } from 'vue';\n import { type BentoColumn, BentoDataGrid, BentoLayout, BentoLayoutRow } from '@adyen/bento-vue2';\n\n const columns = ref<Array<BentoColumn>>([\n { field: 'name', label: 'Name', minWidth: 200, mandatory: true, sortable: true },\n { field: 'company', label: 'Company', minWidth: 150 },\n { field: 'amount', label: 'Amount', minWidth: 120 },\n ]);\n\n const data = ref([\n { id: '1', name: 'Shepherd Kane', company: 'VURBO', amount: 1604 },\n { id: '2', name: 'Santos Palmer', company: 'POLARAX', amount: 1635 },\n { id: '3', name: 'Joyce Johns', company: 'ZENTIA', amount: 785 },\n ]);\n\n const pagination = ref({ hasNext: true, size: 20, page: 1, description: 'Data grid pagination' });\n</script>\n"; import { Default as DefaultDataGrid } from '@/components/data-grid/data-grid.stories'; const meta: Meta = { title: 'Layout/Layout row', component: BentoLayoutRow, argTypes: { columnsLayout: { options: BentoLayoutRowColumnLayoutOptions, control: { type: 'select', }, }, default: { name: 'default', type: { name: 'string' }, description: 'Slot for a single column row, does not use layout.', control: 'text', table: { category: 'slots' }, }, // Need to set slots as col1 and col2 and hide col-1 and col-2 because args don't support args['col-1'] col1: { name: 'col-1', description: 'Slot for the content in the first column', control: 'text', table: { category: 'slots' }, }, col2: { name: 'col-2', description: 'Slot for the content in the second column', control: 'text', table: { category: 'slots' }, }, ['col-1']: { table: { disable: true }, }, ['col-2']: { table: { disable: true }, }, }, parameters: { layout: 'fullscreen', }, }; export default meta; type Story = StoryObj<typeof BentoLayout>; const defaultCode = ` <template> <bento-layout size="full-width"> <bento-layout-row columns-layout="3-9"> <template #col-1>3 cols</template> <template #col-2>9 cols</template> </bento-layout-row> </bento-layout> </template> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoLayout, BentoLayoutRow, }, props: Object.keys(argTypes), template: ` <bento-layout size="full-width"> <bento-layout-row v-bind="args"> <template #default v-if="args.default"><div class="b-layout-row-story__cell">{{ args.default }}</div></template> <template #col-1 v-if="args.col1"><div class="b-layout-row-story__cell">{{ args.col1 }}</div></template> <template #col-2 v-if="args.col2"><div class="b-layout-row-story__cell">{{ args.col2 }}</div></template> </bento-layout-row> </bento-layout> `, setup(props) { const args = isVue2 ? props : _args; return { args, }; }, }), args: { columnsLayout: '3-9', col1: 'Col 1', col2: 'Col 2', }, parameters: storybookDocsParameter(defaultCode), }; export const FullHeight: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLayout, BentoLayoutRow, }, props: Object.keys(argTypes), template: ` <bento-layout variant="component" size="full-width" style="height: 100vh"> <bento-layout-row v-bind="args"> <bento-data-grid v-bind="dataGridArgs" fit-content sticky-pagination /> </bento-layout-row> </bento-layout> `, setup(props) { const args = isVue2 ? props : _args; return { args, dataGridArgs: DefaultDataGrid.args, }; }, }), parameters: storybookDocsParameter(FullHeightExample), args: { fullHeight: true, }, };
|