@lifesg/react-design-system 4.0.0-alpha.12 → 4.0.0-alpha.14
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/breadcrumb/breadcrumb.js +1 -1
- package/breadcrumb/breadcrumb.js.map +1 -1
- package/breadcrumb/breadcrumb.styles.d.ts +0 -8
- package/breadcrumb/breadcrumb.styles.js +1 -1
- package/breadcrumb/breadcrumb.styles.js.map +1 -1
- package/breadcrumb/breadcrumb.styles_1blzn8n.css +7 -0
- package/breadcrumb/types.d.ts +2 -13
- package/cjs/breadcrumb/breadcrumb.js +1 -1
- package/cjs/breadcrumb/breadcrumb.js.map +1 -1
- package/cjs/breadcrumb/breadcrumb.styles.js +1 -1
- package/cjs/breadcrumb/breadcrumb.styles.js.map +1 -1
- package/cjs/breadcrumb/breadcrumb.styles_1blzn8n.css +7 -0
- package/cjs/drawer/drawer.js +1 -1
- package/cjs/drawer/drawer.js.map +1 -1
- package/cjs/drawer/drawer.styles.js +1 -1
- package/cjs/drawer/drawer.styles.js.map +1 -1
- package/{drawer/drawer.styles_94hx3y.css → cjs/drawer/drawer.styles_a4bwe0.css} +6 -3
- package/cjs/external/dompurify/dist/purify.es.js +4 -2
- package/cjs/external/dompurify/dist/purify.es.js.map +1 -1
- package/cjs/file-upload/file-list/file-list.js +1 -1
- package/cjs/file-upload/file-list/file-list.js.map +1 -1
- package/cjs/file-upload/file-list/file-list.styles.js +1 -1
- package/cjs/file-upload/file-list/file-list.styles.js.map +1 -1
- package/cjs/file-upload/file-list/file-list.styles_1xbypnh.css +2 -0
- package/cjs/file-upload/file-list-item/file-item-actions.js +2 -0
- package/cjs/file-upload/file-list-item/file-item-actions.js.map +1 -0
- package/cjs/file-upload/file-list-item/file-item-actions.styles.js +2 -0
- package/cjs/file-upload/file-list-item/file-item-actions.styles.js.map +1 -0
- package/cjs/file-upload/file-list-item/file-item-actions.styles_1quulgy.css +8 -0
- package/cjs/file-upload/file-list-item/file-item-details.js +2 -0
- package/cjs/file-upload/file-list-item/file-item-details.js.map +1 -0
- package/cjs/file-upload/file-list-item/file-item-details.styles.js +2 -0
- package/cjs/file-upload/file-list-item/file-item-details.styles.js.map +1 -0
- package/cjs/file-upload/file-list-item/file-item-details.styles_1ce9akx.css +7 -0
- package/cjs/file-upload/file-list-item/file-list-item.js +1 -1
- package/cjs/file-upload/file-list-item/file-list-item.js.map +1 -1
- package/cjs/file-upload/file-list-item/file-list-item.styles.js +1 -1
- package/cjs/file-upload/file-list-item/file-list-item.styles.js.map +1 -1
- package/cjs/file-upload/file-list-item/file-list-item.styles_x3a500.css +10 -0
- package/cjs/file-upload/file-list-item/use-truncated-name.js +2 -0
- package/cjs/file-upload/file-list-item/use-truncated-name.js.map +1 -0
- package/cjs/local-nav/local-nav-dropdown/local-nav-dropdown.js +1 -1
- package/cjs/local-nav/local-nav-dropdown/local-nav-dropdown.js.map +1 -1
- package/cjs/local-nav/local-nav-dropdown/local-nav-dropdown.styles.js +1 -1
- package/cjs/local-nav/local-nav-dropdown/local-nav-dropdown.styles.js.map +1 -1
- package/{local-nav/local-nav-dropdown/local-nav-dropdown.styles_uhd4xl.css → cjs/local-nav/local-nav-dropdown/local-nav-dropdown.styles_kpel1m.css} +9 -6
- package/cjs/local-nav/local-nav-menu/local-nav-menu.js +1 -1
- package/cjs/local-nav/local-nav-menu/local-nav-menu.js.map +1 -1
- package/cjs/local-nav/local-nav-menu/local-nav-menu.styles.js +1 -1
- package/cjs/local-nav/local-nav-menu/local-nav-menu.styles.js.map +1 -1
- package/cjs/local-nav/local-nav-menu/local-nav-menu.styles_67fcyc.css +8 -0
- package/cjs/menu/menu-content.js +1 -1
- package/cjs/menu/menu-content.js.map +1 -1
- package/cjs/menu/menu-content.styles.js +1 -1
- package/cjs/menu/menu-content.styles.js.map +1 -1
- package/cjs/menu/menu-content.styles_1r8oxtk.css +5 -0
- package/cjs/menu/menu-item.styles.js +1 -1
- package/cjs/menu/menu-item.styles.js.map +1 -1
- package/cjs/menu/{menu-item.styles_liyed8.css → menu-item.styles_eut2j9.css} +1 -1
- package/cjs/menu/menu-link.styles.js +1 -1
- package/cjs/menu/menu-link.styles.js.map +1 -1
- package/cjs/menu/menu-link.styles_rng7n7.css +2 -0
- package/cjs/menu/menu-section.js +1 -1
- package/cjs/menu/menu-section.js.map +1 -1
- package/cjs/menu/menu-section.styles.js +1 -1
- package/cjs/menu/menu-section.styles.js.map +1 -1
- package/cjs/menu/menu-section.styles_1n0ppxl.css +5 -0
- package/cjs/modal-v2/slots/slot-styles.js +1 -1
- package/cjs/modal-v2/slots/slot-styles.js.map +1 -1
- package/cjs/modal-v2/slots/{slot-styles_ayb9il.css → slot-styles_ygu6zc.css} +1 -1
- package/cjs/navbar/navbar-items.js +1 -1
- package/cjs/navbar/navbar-items.js.map +1 -1
- package/cjs/notification-banner/notification-banner-hoc.js +1 -1
- package/cjs/notification-banner/notification-banner-hoc.js.map +1 -1
- package/cjs/pagination/pagination.js +1 -1
- package/cjs/pagination/pagination.js.map +1 -1
- package/cjs/shared/dropdown-list/dropdown-label.js +1 -1
- package/cjs/shared/dropdown-list/dropdown-label.js.map +1 -1
- package/cjs/shared/fade-wrapper/fade-wrapper.js +1 -1
- package/cjs/shared/fade-wrapper/fade-wrapper.js.map +1 -1
- package/cjs/theme/tokens/colour.js +1 -1
- package/cjs/theme/tokens/colour.js.map +1 -1
- package/cjs/theme/tokens/radius.js +1 -1
- package/cjs/theme/tokens/radius.js.map +1 -1
- package/codemods/run-codemod.js +8 -2
- package/codemods/run-codemod.js.map +1 -1
- package/codemods/run-codemod.ts +13 -4
- package/drawer/drawer.d.ts +1 -1
- package/drawer/drawer.js +1 -1
- package/drawer/drawer.js.map +1 -1
- package/drawer/drawer.styles.d.ts +3 -0
- package/drawer/drawer.styles.js +1 -1
- package/drawer/drawer.styles.js.map +1 -1
- package/{cjs/drawer/drawer.styles_94hx3y.css → drawer/drawer.styles_a4bwe0.css} +6 -3
- package/drawer/types.d.ts +7 -0
- package/external/dompurify/dist/purify.es.js +4 -2
- package/external/dompurify/dist/purify.es.js.map +1 -1
- package/file-upload/file-list/file-list.js +1 -1
- package/file-upload/file-list/file-list.js.map +1 -1
- package/file-upload/file-list/file-list.styles.d.ts +0 -1
- package/file-upload/file-list/file-list.styles.js +1 -1
- package/file-upload/file-list/file-list.styles.js.map +1 -1
- package/file-upload/file-list/file-list.styles_1xbypnh.css +2 -0
- package/file-upload/file-list-item/file-item-actions.d.ts +30 -0
- package/file-upload/file-list-item/file-item-actions.js +2 -0
- package/file-upload/file-list-item/file-item-actions.js.map +1 -0
- package/file-upload/file-list-item/file-item-actions.styles.d.ts +6 -0
- package/file-upload/file-list-item/file-item-actions.styles.js +2 -0
- package/file-upload/file-list-item/file-item-actions.styles.js.map +1 -0
- package/file-upload/file-list-item/file-item-actions.styles_1quulgy.css +8 -0
- package/file-upload/file-list-item/file-item-details.d.ts +15 -0
- package/file-upload/file-list-item/file-item-details.js +2 -0
- package/file-upload/file-list-item/file-item-details.js.map +1 -0
- package/file-upload/file-list-item/file-item-details.styles.d.ts +7 -0
- package/file-upload/file-list-item/file-item-details.styles.js +2 -0
- package/file-upload/file-list-item/file-item-details.styles.js.map +1 -0
- package/file-upload/file-list-item/file-item-details.styles_1ce9akx.css +7 -0
- package/file-upload/file-list-item/file-list-item.d.ts +1 -7
- package/file-upload/file-list-item/file-list-item.js +1 -1
- package/file-upload/file-list-item/file-list-item.js.map +1 -1
- package/file-upload/file-list-item/file-list-item.styles.d.ts +6 -13
- package/file-upload/file-list-item/file-list-item.styles.js +1 -1
- package/file-upload/file-list-item/file-list-item.styles.js.map +1 -1
- package/file-upload/file-list-item/file-list-item.styles_x3a500.css +10 -0
- package/file-upload/file-list-item/types.d.ts +10 -3
- package/file-upload/file-list-item/use-truncated-name.d.ts +1 -0
- package/file-upload/file-list-item/use-truncated-name.js +2 -0
- package/file-upload/file-list-item/use-truncated-name.js.map +1 -0
- package/input-group/types.d.ts +4 -1
- package/input-select/types.d.ts +1 -0
- package/local-nav/local-nav-dropdown/local-nav-dropdown.js +1 -1
- package/local-nav/local-nav-dropdown/local-nav-dropdown.js.map +1 -1
- package/local-nav/local-nav-dropdown/local-nav-dropdown.styles.d.ts +3 -0
- package/local-nav/local-nav-dropdown/local-nav-dropdown.styles.js +1 -1
- package/local-nav/local-nav-dropdown/local-nav-dropdown.styles.js.map +1 -1
- package/{cjs/local-nav/local-nav-dropdown/local-nav-dropdown.styles_uhd4xl.css → local-nav/local-nav-dropdown/local-nav-dropdown.styles_kpel1m.css} +9 -6
- package/local-nav/local-nav-menu/local-nav-menu.js +1 -1
- package/local-nav/local-nav-menu/local-nav-menu.js.map +1 -1
- package/local-nav/local-nav-menu/local-nav-menu.styles.d.ts +3 -0
- package/local-nav/local-nav-menu/local-nav-menu.styles.js +1 -1
- package/local-nav/local-nav-menu/local-nav-menu.styles.js.map +1 -1
- package/local-nav/local-nav-menu/local-nav-menu.styles_67fcyc.css +8 -0
- package/local-nav/types.d.ts +17 -0
- package/masked-input/types.d.ts +1 -2
- package/menu/menu-content.js +1 -1
- package/menu/menu-content.js.map +1 -1
- package/menu/menu-content.styles.d.ts +1 -0
- package/menu/menu-content.styles.js +1 -1
- package/menu/menu-content.styles.js.map +1 -1
- package/menu/menu-content.styles_1r8oxtk.css +5 -0
- package/menu/menu-item.styles.js +1 -1
- package/menu/menu-item.styles.js.map +1 -1
- package/menu/{menu-item.styles_liyed8.css → menu-item.styles_eut2j9.css} +1 -1
- package/menu/menu-link.styles.js +1 -1
- package/menu/menu-link.styles.js.map +1 -1
- package/menu/menu-link.styles_rng7n7.css +2 -0
- package/menu/menu-section.d.ts +1 -1
- package/menu/menu-section.js +1 -1
- package/menu/menu-section.js.map +1 -1
- package/menu/menu-section.styles.d.ts +7 -0
- package/menu/menu-section.styles.js +1 -1
- package/menu/menu-section.styles.js.map +1 -1
- package/menu/menu-section.styles_1n0ppxl.css +5 -0
- package/menu/menu.d.ts +1 -1
- package/menu/types.d.ts +10 -2
- package/modal-v2/slots/slot-styles.js +1 -1
- package/modal-v2/slots/slot-styles.js.map +1 -1
- package/modal-v2/slots/{slot-styles_ayb9il.css → slot-styles_ygu6zc.css} +1 -1
- package/navbar/navbar-items.js +1 -1
- package/navbar/navbar-items.js.map +1 -1
- package/navbar/types.d.ts +9 -0
- package/notification-banner/notification-banner-hoc.js +1 -1
- package/notification-banner/notification-banner-hoc.js.map +1 -1
- package/package.json +3 -4
- package/pagination/pagination.js +1 -1
- package/pagination/pagination.js.map +1 -1
- package/pagination/types.d.ts +8 -1
- package/scripts/cli.js +18 -0
- package/select-histogram/types.d.ts +1 -0
- package/shared/dropdown-list/dropdown-label.d.ts +2 -2
- package/shared/dropdown-list/dropdown-label.js +1 -1
- package/shared/dropdown-list/dropdown-label.js.map +1 -1
- package/shared/dropdown-list/types.d.ts +6 -2
- package/shared/fade-wrapper/fade-wrapper.js +1 -1
- package/shared/fade-wrapper/fade-wrapper.js.map +1 -1
- package/shared/fade-wrapper/types.d.ts +1 -0
- package/shared/styles/index.d.ts +5 -0
- package/skills/fds-build/SKILL.md +167 -0
- package/skills/fds-build/catalogue.md +181 -0
- package/skills/fds-build/evals/evals.json +519 -0
- package/skills/fds-build/resources/components/accordion.md +128 -0
- package/skills/fds-build/resources/components/alert.md +54 -0
- package/skills/fds-build/resources/components/animation.md +18 -0
- package/skills/fds-build/resources/components/avatar.md +10 -0
- package/skills/fds-build/resources/components/badge.md +18 -0
- package/skills/fds-build/resources/components/box-container.md +61 -0
- package/skills/fds-build/resources/components/breadcrumb.md +60 -0
- package/skills/fds-build/resources/components/button.md +74 -0
- package/skills/fds-build/resources/components/calendar.md +113 -0
- package/skills/fds-build/resources/components/card.md +43 -0
- package/skills/fds-build/resources/components/checkbox.md +53 -0
- package/skills/fds-build/resources/components/countdown-timer.md +20 -0
- package/skills/fds-build/resources/components/data-table.md +142 -0
- package/skills/fds-build/resources/components/date-navigator.md +25 -0
- package/skills/fds-build/resources/components/divider.md +23 -0
- package/skills/fds-build/resources/components/drawer.md +78 -0
- package/skills/fds-build/resources/components/error-display.md +137 -0
- package/skills/fds-build/resources/components/feedback-rating.md +61 -0
- package/skills/fds-build/resources/components/file-download.md +54 -0
- package/skills/fds-build/resources/components/file-upload.md +105 -0
- package/skills/fds-build/resources/components/filter.md +228 -0
- package/skills/fds-build/resources/components/footer.md +94 -0
- package/skills/fds-build/resources/components/form-date-input.md +85 -0
- package/skills/fds-build/resources/components/form-date-range-input.md +146 -0
- package/skills/fds-build/resources/components/form-e-signature.md +55 -0
- package/skills/fds-build/resources/components/form-input.md +65 -0
- package/skills/fds-build/resources/components/form-masked-input.md +69 -0
- package/skills/fds-build/resources/components/form-multi-select.md +100 -0
- package/skills/fds-build/resources/components/form-nested-multi-select.md +159 -0
- package/skills/fds-build/resources/components/form-nested-select.md +151 -0
- package/skills/fds-build/resources/components/form-otp-verification.md +175 -0
- package/skills/fds-build/resources/components/form-phone-number-input.md +87 -0
- package/skills/fds-build/resources/components/form-predictive-text-input.md +108 -0
- package/skills/fds-build/resources/components/form-select.md +103 -0
- package/skills/fds-build/resources/components/form-slider.md +116 -0
- package/skills/fds-build/resources/components/form-textarea.md +116 -0
- package/skills/fds-build/resources/components/form-time-range-picker.md +98 -0
- package/skills/fds-build/resources/components/form-timepicker.md +77 -0
- package/skills/fds-build/resources/components/form-unit-number-input.md +138 -0
- package/skills/fds-build/resources/components/fullscreen-image-carousel.md +159 -0
- package/skills/fds-build/resources/components/histogram-slider.md +41 -0
- package/skills/fds-build/resources/components/icon.md +167 -0
- package/skills/fds-build/resources/components/image-button.md +12 -0
- package/skills/fds-build/resources/components/input.md +51 -0
- package/skills/fds-build/resources/components/language-switcher.md +15 -0
- package/skills/fds-build/resources/components/layout.md +156 -0
- package/skills/fds-build/resources/components/link-list.md +120 -0
- package/skills/fds-build/resources/components/local-nav.md +119 -0
- package/skills/fds-build/resources/components/masthead.md +14 -0
- package/skills/fds-build/resources/components/menu.md +109 -0
- package/skills/fds-build/resources/components/modal.md +150 -0
- package/skills/fds-build/resources/components/navbar.md +270 -0
- package/skills/fds-build/resources/components/notification-banner.md +106 -0
- package/skills/fds-build/resources/components/pagination.md +104 -0
- package/skills/fds-build/resources/components/pill.md +55 -0
- package/skills/fds-build/resources/components/popover.md +80 -0
- package/skills/fds-build/resources/components/progress-indicator.md +47 -0
- package/skills/fds-build/resources/components/radio-button.md +66 -0
- package/skills/fds-build/resources/components/schedule.md +78 -0
- package/skills/fds-build/resources/components/sidenav.md +183 -0
- package/skills/fds-build/resources/components/singpass-button.md +14 -0
- package/skills/fds-build/resources/components/smart-app-banner.md +28 -0
- package/skills/fds-build/resources/components/tab.md +107 -0
- package/skills/fds-build/resources/components/table.md +65 -0
- package/skills/fds-build/resources/components/tag.md +68 -0
- package/skills/fds-build/resources/components/text-list.md +44 -0
- package/skills/fds-build/resources/components/time-slot-bar-week.md +66 -0
- package/skills/fds-build/resources/components/time-slot-bar.md +57 -0
- package/skills/fds-build/resources/components/time-slot-week-view.md +70 -0
- package/skills/fds-build/resources/components/timeline.md +69 -0
- package/skills/fds-build/resources/components/timetable.md +123 -0
- package/skills/fds-build/resources/components/toast.md +72 -0
- package/skills/fds-build/resources/components/toggle.md +119 -0
- package/skills/fds-build/resources/components/typography.md +86 -0
- package/skills/fds-build/resources/components/uneditable-section.md +84 -0
- package/skills/fds-build/resources/foundations/layout-composition-patterns.md +53 -0
- package/skills/fds-build/resources/foundations/tokens.md +547 -0
- package/skills/fds-build/resources/patterns/states.md +300 -0
- package/skills/fds-build/resources/setup/setup.md +202 -0
- package/skills/fds-build/resources/templates/form.md +427 -0
- package/skills/fds-build/resources/templates/listing.md +435 -0
- package/theme/styles/a11y-playground.css +52 -40
- package/theme/styles/bookingsg.css +52 -40
- package/theme/styles/careercompass.css +52 -40
- package/theme/styles/ccube.css +52 -40
- package/theme/styles/imda.css +52 -40
- package/theme/styles/lifesg.css +52 -40
- package/theme/styles/mylegacy.css +52 -40
- package/theme/styles/oneservice.css +52 -40
- package/theme/styles/pa.css +28 -22
- package/theme/styles/rbs.css +52 -40
- package/theme/styles/sgw-digital-lobby.css +52 -40
- package/theme/styles/smgs.css +52 -40
- package/theme/styles/spf.css +28 -27
- package/theme/styles/sportsg-orange.css +52 -40
- package/theme/styles/supportgowhere.css +52 -40
- package/theme/styles/tote-board.css +52 -40
- package/theme/styles/vica.css +52 -40
- package/theme/styles/websg.css +52 -40
- package/theme/styles/wise.css +52 -40
- package/theme/styles/wogaa.css +52 -40
- package/theme/tokens/colour.d.ts +6 -0
- package/theme/tokens/colour.js +1 -1
- package/theme/tokens/colour.js.map +1 -1
- package/theme/tokens/radius.d.ts +3 -0
- package/theme/tokens/radius.js +1 -1
- package/theme/tokens/radius.js.map +1 -1
- package/breadcrumb/breadcrumb.styles_1nl4aua.css +0 -18
- package/cjs/breadcrumb/breadcrumb.styles_1nl4aua.css +0 -18
- package/cjs/file-upload/file-item-edit.js +0 -2
- package/cjs/file-upload/file-item-edit.js.map +0 -1
- package/cjs/file-upload/file-item-edit.styles.js +0 -2
- package/cjs/file-upload/file-item-edit.styles.js.map +0 -1
- package/cjs/file-upload/file-item-edit.styles_19bvztn.css +0 -11
- package/cjs/file-upload/file-list/file-list.styles_1w7so98.css +0 -3
- package/cjs/file-upload/file-list-item/file-list-item.styles_kgtfke.css +0 -17
- package/cjs/local-nav/local-nav-menu/local-nav-menu.styles_pq19dh.css +0 -5
- package/cjs/menu/menu-content.styles_p7ijll.css +0 -2
- package/cjs/menu/menu-link.styles_hfxoez.css +0 -2
- package/cjs/menu/menu-section.styles_g7my4d.css +0 -3
- package/file-upload/file-item-edit.d.ts +0 -14
- package/file-upload/file-item-edit.js +0 -2
- package/file-upload/file-item-edit.js.map +0 -1
- package/file-upload/file-item-edit.styles.d.ts +0 -9
- package/file-upload/file-item-edit.styles.js +0 -2
- package/file-upload/file-item-edit.styles.js.map +0 -1
- package/file-upload/file-item-edit.styles_19bvztn.css +0 -11
- package/file-upload/file-list/file-list.styles_1w7so98.css +0 -3
- package/file-upload/file-list-item/file-list-item.styles_kgtfke.css +0 -17
- package/local-nav/local-nav-menu/local-nav-menu.styles_pq19dh.css +0 -5
- package/menu/menu-content.styles_p7ijll.css +0 -2
- package/menu/menu-link.styles_hfxoez.css +0 -2
- package/menu/menu-section.styles_g7my4d.css +0 -3
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Form.NestedMultiSelect
|
|
2
|
+
|
|
3
|
+
Import: `import { Form } from "@lifesg/react-design-system/form"`
|
|
4
|
+
|
|
5
|
+
## When to use
|
|
6
|
+
|
|
7
|
+
Use when users can select multiple options from a hierarchically organised list of up to 3 levels. For hierarchical single-select use `Form.NestedSelect`. For flat multi-select use `Form.MultiSelect`.
|
|
8
|
+
|
|
9
|
+
## Variants
|
|
10
|
+
|
|
11
|
+
| `variant` | When to use |
|
|
12
|
+
| ----------- | --------------------- |
|
|
13
|
+
| `"default"` | Standard form context |
|
|
14
|
+
| `"small"` | Compact layouts |
|
|
15
|
+
|
|
16
|
+
| `mode` | When to use |
|
|
17
|
+
| ------------ | ---------------------------------------------------------------------------------------- |
|
|
18
|
+
| `"default"` | System decides initial expand state |
|
|
19
|
+
| `"expand"` | Expand all categories on open — for short, browsable lists |
|
|
20
|
+
| `"collapse"` | Collapse all categories on open — for long lists where search is the primary interaction |
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Multi-select with parallel keyPaths / values arrays
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { useState } from "react";
|
|
28
|
+
import { Form } from "@lifesg/react-design-system/form";
|
|
29
|
+
import { L1OptionProps } from "@lifesg/react-design-system/input-nested-select";
|
|
30
|
+
|
|
31
|
+
interface SkillValue {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const options: L1OptionProps<SkillValue, SkillValue, SkillValue>[] = [
|
|
37
|
+
{
|
|
38
|
+
key: "design",
|
|
39
|
+
label: "Design",
|
|
40
|
+
subItems: [
|
|
41
|
+
{
|
|
42
|
+
key: "design-ux",
|
|
43
|
+
label: "UX Design",
|
|
44
|
+
value: { id: "ux", name: "UX Design" },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
key: "design-ui",
|
|
48
|
+
label: "UI Design",
|
|
49
|
+
value: { id: "ui", name: "UI Design" },
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
key: "engineering",
|
|
55
|
+
label: "Engineering",
|
|
56
|
+
subItems: [
|
|
57
|
+
{
|
|
58
|
+
key: "eng-fe",
|
|
59
|
+
label: "Frontend",
|
|
60
|
+
value: { id: "fe", name: "Frontend" },
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
key: "eng-be",
|
|
64
|
+
label: "Backend",
|
|
65
|
+
value: { id: "be", name: "Backend" },
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const NestedMultiSelectExample = () => {
|
|
72
|
+
// keyPaths[i] and values[i] always refer to the same selection — keep them in sync
|
|
73
|
+
const [keyPaths, setKeyPaths] = useState<string[][]>([
|
|
74
|
+
["design", "design-ux"], // pre-selected: UX Design
|
|
75
|
+
]);
|
|
76
|
+
const [values, setValues] = useState<SkillValue[]>([
|
|
77
|
+
{ id: "ux", name: "UX Design" },
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Form.NestedMultiSelect<SkillValue, SkillValue, SkillValue>
|
|
82
|
+
label="Skills"
|
|
83
|
+
options={options}
|
|
84
|
+
selectedKeyPaths={keyPaths}
|
|
85
|
+
// onSelectOptions provides the NEXT full selection state — replace, don't merge
|
|
86
|
+
onSelectOptions={(nextKeyPaths, nextValues) => {
|
|
87
|
+
setKeyPaths(nextKeyPaths);
|
|
88
|
+
setValues(nextValues as SkillValue[]);
|
|
89
|
+
}}
|
|
90
|
+
valueToStringFunction={(val) => val.name}
|
|
91
|
+
enableSearch
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**The parallel-array contract:** index `i` in `keyPaths` and index `i` in `values` always describe the same selected item. `onSelectOptions` delivers the complete next state — replace both arrays together, never splice one independently. Initialise both arrays with matching entries when pre-populating selections.
|
|
98
|
+
|
|
99
|
+
## Props
|
|
100
|
+
|
|
101
|
+
| Prop | Type | Default | Description |
|
|
102
|
+
| ----------------------- | ----------------------------------------------------------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
103
|
+
| `options` \* | `L1OptionProps<V1, V2, V3>[]` | — | — |
|
|
104
|
+
| `optionsLoadState` | `ItemsLoadStateType` | "success" | Indicates the load state of options fetched asynchronously. Renders a loading spinner, error retry, or the list depending on the state. |
|
|
105
|
+
| `optionTruncationType` | `TruncateType` | "end" | Controls how long option labels are truncated when they overflow. Specify `"none"` to display option labels in full. |
|
|
106
|
+
| `onShowOptions` | `(() => void)` | — | Called when the dropdown list becomes visible. |
|
|
107
|
+
| `onHideOptions` | `(() => void)` | — | Called when the dropdown list is hidden. |
|
|
108
|
+
| `onRetry` | `(() => void)` | — | Called when the user activates the retry action after a load failure. |
|
|
109
|
+
| `name` | `string` | — | Name attribute forwarded to the underlying button element. |
|
|
110
|
+
| `disabled` | `boolean` | — | — |
|
|
111
|
+
| `selectedKeyPaths` | `string[][]` | — | The currently selected options expressed as an array of key paths. Each key path is an ordered array of `key` strings from the root option down to the selected item. |
|
|
112
|
+
| `onSelectOptions` | `\| (( keyPaths: string[][], values: InputNestedMultiSelectValues<V1, V2, V3> ) => void)` | — | Called after the selection changes. |
|
|
113
|
+
| `dropdownWidth` | `string` | — | Custom width for the dropdown. When specified, the dropdown uses this width instead of matching the trigger element width. |
|
|
114
|
+
| `onBlur` | `(() => void)` | — | Called when focus leaves both the trigger and dropdown. |
|
|
115
|
+
| `customLabels` | `DropdownCustomLabelProps` | — | Overrides the default labels shown inside the dropdown and on the trigger. |
|
|
116
|
+
| `readOnly` | `boolean` | — | — |
|
|
117
|
+
| `mode` | `ExpandMode` | — | Specifies if items are expanded or collapsed when the dropdown is opened |
|
|
118
|
+
| `variant` | `DropdownVariantType` | "default" | Controls the height of the trigger and dropdown items. |
|
|
119
|
+
| `alignment` | `DropdownAlignmentType` | — | Aligns the dropdown relative to the trigger element. |
|
|
120
|
+
| `dropdownZIndex` | `number` | — | CSS `z-index` applied to the dropdown overlay. |
|
|
121
|
+
| `dropdownRootNode` | `RefObject<HTMLElement>` | document.body | The root element that contains the dropdown element. |
|
|
122
|
+
| `valueToStringFunction` | `((value: V1 \| V2 \| V3) => string)` | — | Function to convert selected value into a string |
|
|
123
|
+
| `enableSearch` | `boolean` | — | Renders a search bar inside the dropdown when `true`. |
|
|
124
|
+
| `hideNoResultsDisplay` | `boolean` | — | Suppresses the built-in "no results" message when `true`. |
|
|
125
|
+
| `noResultsDescription` | `React.ReactNode` | — | — |
|
|
126
|
+
| `searchPlaceholder` | `string` | — | — |
|
|
127
|
+
| `searchFunction` | `((searchValue: string) => T[])` | — | Custom filter function invoked on each keystroke in the search input. |
|
|
128
|
+
| `onSearch` | `(() => void)` | — | Called when the user types in the search input. |
|
|
129
|
+
| `label` | `FormLabelProps \| string` | — | Label rendered above the input. Accepts a plain string or a `FormLabelProps` object to provide additional configuration. |
|
|
130
|
+
| `errorMessage` | `string \| React.ReactNode` | — | Inline error text rendered below the input. |
|
|
131
|
+
| `layoutType` | `FormElementLayoutType` | — | Controls whether the container uses flex layout or a responsive column grid. |
|
|
132
|
+
| `xxlCols` | `ColSpan` | — | Column span at the `xxl` breakpoint. |
|
|
133
|
+
| `xlCols` | `ColSpan` | — | Column span at the `xl` breakpoint. |
|
|
134
|
+
| `lgCols` | `ColSpan` | — | Column span at the `lg` breakpoint. |
|
|
135
|
+
| `mdCols` | `ColSpan` | — | Column span at the `md` breakpoint. |
|
|
136
|
+
| `smCols` | `ColSpan` | — | Column span at the `sm` breakpoint. |
|
|
137
|
+
| `xsCols` | `ColSpan` | — | Column span at the `xs` breakpoint. |
|
|
138
|
+
| `xxsCols` | `ColSpan` | — | Column span at the `xxs` (smallest) breakpoint. Acts as the base value. All larger breakpoints fall back to this when their own prop is not set. |
|
|
139
|
+
|
|
140
|
+
## Rules
|
|
141
|
+
|
|
142
|
+
- Every option node must have a unique `key` string — key paths are used for controlled state.
|
|
143
|
+
- Provide `valueToStringFunction` when values are not plain strings. The same function is applied to values at all nesting levels — ensure values are homogeneous across levels, or that the function handles all types.
|
|
144
|
+
- Use `enableSearch` when the total option count across all levels is large.
|
|
145
|
+
- Use `readOnly` on review screens — not `disabled`.
|
|
146
|
+
- Provide `customLabels` to localise "Select all", "Clear all", and count labels.
|
|
147
|
+
- `options` is the only mandatory component-specific prop.
|
|
148
|
+
- Options follow the same nested structure as `Form.NestedSelect`: `{ key: string, label: string, value: V, subItems?: L2OptionProps[] }`. Every option must have a unique `key`.
|
|
149
|
+
- `selectedKeyPaths` is an array of key-path arrays — one per selected item.
|
|
150
|
+
- `onSelectOptions` receives the complete updated `keyPaths` and `values` arrays on every change.
|
|
151
|
+
- `valueToStringFunction` uses a single callback for all nesting levels — values across levels must be the same type or handled uniformly.
|
|
152
|
+
|
|
153
|
+
## Anti-patterns
|
|
154
|
+
|
|
155
|
+
- Do not use `Form.NestedMultiSelect` for flat option lists — use `Form.MultiSelect`.
|
|
156
|
+
- Do not use when only one option can be selected — use `Form.NestedSelect`.
|
|
157
|
+
- Do not use duplicate `key` values across options — selection state will break.
|
|
158
|
+
- Do not set both `error` and `errorMessage`.
|
|
159
|
+
- Do not use the deprecated `searchPlaceholder` or `noResultsDescription` props directly — use `customLabels`.
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Form.NestedSelect
|
|
2
|
+
|
|
3
|
+
Import: `import { Form } from "@lifesg/react-design-system/form"`
|
|
4
|
+
|
|
5
|
+
## When to use
|
|
6
|
+
|
|
7
|
+
Use when options are organised in a hierarchy of up to 3 levels — e.g. industry → sector → role, or country → state → city. For flat single-select use `Form.Select`. For hierarchical multi-select use `Form.NestedMultiSelect`.
|
|
8
|
+
|
|
9
|
+
## Variants
|
|
10
|
+
|
|
11
|
+
| `variant` | When to use |
|
|
12
|
+
| ----------- | --------------------- |
|
|
13
|
+
| `"default"` | Standard form context |
|
|
14
|
+
| `"small"` | Compact layouts |
|
|
15
|
+
|
|
16
|
+
| `mode` | When to use |
|
|
17
|
+
| ------------ | -------------------------------------------------------------------------------------- |
|
|
18
|
+
| `"default"` | System decides initial expand state based on selected path |
|
|
19
|
+
| `"expand"` | Expand all categories on open — useful when the list is short and browsing is expected |
|
|
20
|
+
| `"collapse"` | Collapse all categories on open — useful for long lists where search is primary |
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Three-level hierarchy with controlled keyPath
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { useState } from "react";
|
|
28
|
+
import { Form } from "@lifesg/react-design-system/form";
|
|
29
|
+
import { L1OptionProps } from "@lifesg/react-design-system/input-nested-select";
|
|
30
|
+
|
|
31
|
+
interface RoleValue {
|
|
32
|
+
id: string;
|
|
33
|
+
label: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const options: L1OptionProps<RoleValue, RoleValue, RoleValue>[] = [
|
|
37
|
+
{
|
|
38
|
+
key: "engineering",
|
|
39
|
+
label: "Engineering",
|
|
40
|
+
subItems: [
|
|
41
|
+
{
|
|
42
|
+
key: "engineering-frontend",
|
|
43
|
+
label: "Frontend",
|
|
44
|
+
subItems: [
|
|
45
|
+
{
|
|
46
|
+
key: "engineering-frontend-react",
|
|
47
|
+
label: "React Developer",
|
|
48
|
+
value: { id: "fe-react", label: "React Developer" },
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: "engineering-frontend-vue",
|
|
52
|
+
label: "Vue Developer",
|
|
53
|
+
value: { id: "fe-vue", label: "Vue Developer" },
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
key: "engineering-backend",
|
|
59
|
+
label: "Backend",
|
|
60
|
+
value: { id: "be", label: "Backend Developer" },
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const NestedSelectExample = () => {
|
|
67
|
+
// keyPath is the path of keys from root to selected node
|
|
68
|
+
const [keyPath, setKeyPath] = useState<string[]>([]);
|
|
69
|
+
const [value, setValue] = useState<RoleValue | undefined>();
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Form.NestedSelect<RoleValue, RoleValue, RoleValue>
|
|
73
|
+
label="Job role"
|
|
74
|
+
options={options}
|
|
75
|
+
selectedKeyPath={keyPath}
|
|
76
|
+
// onChange receives the full key path and the node's value
|
|
77
|
+
onChange={(path, val) => {
|
|
78
|
+
setKeyPath(path);
|
|
79
|
+
setValue(val);
|
|
80
|
+
}}
|
|
81
|
+
valueToStringFunction={(val) => val.label}
|
|
82
|
+
selectableCategory // intermediate nodes (e.g. "Backend") are also selectable
|
|
83
|
+
enableSearch
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**keyPath explains the selection:** `["engineering", "engineering-frontend", "engineering-frontend-react"]` means the user drilled through Engineering → Frontend → React Developer. An empty array means nothing is selected.
|
|
90
|
+
|
|
91
|
+
## Props
|
|
92
|
+
|
|
93
|
+
| Prop | Type | Default | Description |
|
|
94
|
+
| ----------------------- | --------------------------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
95
|
+
| `options` \* | `L1OptionProps<V1, V2, V3>[]` | — | — |
|
|
96
|
+
| `optionsLoadState` | `ItemsLoadStateType` | "success" | Indicates the load state of options fetched asynchronously. Renders a loading spinner, error retry, or the list depending on the state. |
|
|
97
|
+
| `optionTruncationType` | `TruncateType` | "end" | Controls how long option labels are truncated when they overflow. Specify `"none"` to display option labels in full. |
|
|
98
|
+
| `onShowOptions` | `(() => void)` | — | Called when the dropdown list becomes visible. |
|
|
99
|
+
| `onHideOptions` | `(() => void)` | — | Called when the dropdown list is hidden. |
|
|
100
|
+
| `onRetry` | `(() => void)` | — | Called when the user activates the retry action after a load failure. |
|
|
101
|
+
| `name` | `string` | — | Name attribute forwarded to the underlying button element. |
|
|
102
|
+
| `disabled` | `boolean` | — | — |
|
|
103
|
+
| `selectedKeyPath` | `string[]` | — | Key path identifying the currently selected option. Each entry is the `key` of the option at that nesting level, ordered from level 1 to the selected level. Drives the controlled selection state. |
|
|
104
|
+
| `selectableCategory` | `boolean` | — | When `true`, parent category labels are themselves selectable as options, not just expand/collapse toggles. |
|
|
105
|
+
| `onSelectOption` | `\| ((keyPath: string[], value: V1 \| V2 \| V3) => void)` | — | Called when the user selects an option. |
|
|
106
|
+
| `dropdownWidth` | `string` | — | Custom width for the dropdown. When specified, the dropdown uses this width instead of matching the trigger element width. |
|
|
107
|
+
| `onBlur` | `(() => void)` | — | Called when focus leaves both the trigger and the dropdown. |
|
|
108
|
+
| `customLabels` | `DropdownCustomLabelProps` | — | Overrides the default labels shown inside the dropdown and on the trigger. |
|
|
109
|
+
| `readOnly` | `boolean` | — | — |
|
|
110
|
+
| `mode` | `ExpandMode` | — | Specifies if items are expanded or collapsed when the dropdown is opened |
|
|
111
|
+
| `variant` | `DropdownVariantType` | "default" | Controls the height of the trigger and dropdown items. |
|
|
112
|
+
| `alignment` | `DropdownAlignmentType` | — | Aligns the dropdown relative to the trigger element. |
|
|
113
|
+
| `dropdownZIndex` | `number` | — | CSS `z-index` applied to the dropdown overlay. |
|
|
114
|
+
| `dropdownRootNode` | `RefObject<HTMLElement>` | document.body | The root element that contains the dropdown element. |
|
|
115
|
+
| `valueToStringFunction` | `((value: V1 \| V2 \| V3) => string)` | — | Function to convert selected value into a string |
|
|
116
|
+
| `enableSearch` | `boolean` | — | Renders a search bar inside the dropdown when `true`. |
|
|
117
|
+
| `hideNoResultsDisplay` | `boolean` | — | Suppresses the built-in "no results" message when `true`. |
|
|
118
|
+
| `noResultsDescription` | `React.ReactNode` | — | — |
|
|
119
|
+
| `searchPlaceholder` | `string` | — | — |
|
|
120
|
+
| `searchFunction` | `((searchValue: string) => T[])` | — | Custom filter function invoked on each keystroke in the search input. |
|
|
121
|
+
| `onSearch` | `(() => void)` | — | Called when the user types in the search input. |
|
|
122
|
+
| `label` | `FormLabelProps \| string` | — | Label rendered above the input. Accepts a plain string or a `FormLabelProps` object to provide additional configuration. |
|
|
123
|
+
| `errorMessage` | `string \| React.ReactNode` | — | Inline error text rendered below the input. |
|
|
124
|
+
| `layoutType` | `FormElementLayoutType` | — | Controls whether the container uses flex layout or a responsive column grid. |
|
|
125
|
+
| `xxlCols` | `ColSpan` | — | Column span at the `xxl` breakpoint. |
|
|
126
|
+
| `xlCols` | `ColSpan` | — | Column span at the `xl` breakpoint. |
|
|
127
|
+
| `lgCols` | `ColSpan` | — | Column span at the `lg` breakpoint. |
|
|
128
|
+
| `mdCols` | `ColSpan` | — | Column span at the `md` breakpoint. |
|
|
129
|
+
| `smCols` | `ColSpan` | — | Column span at the `sm` breakpoint. |
|
|
130
|
+
| `xsCols` | `ColSpan` | — | Column span at the `xs` breakpoint. |
|
|
131
|
+
| `xxsCols` | `ColSpan` | — | Column span at the `xxs` (smallest) breakpoint. Acts as the base value. All larger breakpoints fall back to this when their own prop is not set. |
|
|
132
|
+
|
|
133
|
+
## Rules
|
|
134
|
+
|
|
135
|
+
- Every option node must have a unique `key` string — the key path is used for controlled state.
|
|
136
|
+
- Provide `valueToStringFunction` when values are not plain strings and the selected display needs to be derived from the value.
|
|
137
|
+
- Enable `selectableCategory` only when selecting a parent category is a valid business outcome — otherwise category nodes serve as navigational groupings only.
|
|
138
|
+
- Use `enableSearch` when the total option count across all levels is large.
|
|
139
|
+
- Use `readOnly` on review screens — not `disabled`.
|
|
140
|
+
- Provide `customLabels.searchPlaceholder` instead of the deprecated `searchPlaceholder` prop.
|
|
141
|
+
- `options` is the only mandatory component-specific prop.
|
|
142
|
+
- Options follow a nested structure: `{ key: string, label: string, value: V, subItems?: L2OptionProps[] }`. Every option must have a unique `key`.
|
|
143
|
+
- `selectedKeyPath` is an array of keys tracing the path to the selected leaf (e.g. `["parent-key", "child-key"]`).
|
|
144
|
+
- `selectableCategory` enables selection of non-leaf (parent category) nodes.
|
|
145
|
+
|
|
146
|
+
## Anti-patterns
|
|
147
|
+
|
|
148
|
+
- Do not use `Form.NestedSelect` for flat option lists — use `Form.Select`.
|
|
149
|
+
- Do not use when multiple selections are needed — use `Form.NestedMultiSelect`.
|
|
150
|
+
- Do not use duplicate `key` values across options — selection state will break.
|
|
151
|
+
- Do not set both `error` and `errorMessage`.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Form.OtpVerification
|
|
2
|
+
|
|
3
|
+
Import: `import { Form } from "@lifesg/react-design-system/form"`
|
|
4
|
+
|
|
5
|
+
## When to use
|
|
6
|
+
|
|
7
|
+
- Flows that require identity verification via a one-time password sent to a phone number or email address.
|
|
8
|
+
- The component manages the full multi-step UI: input contact → send OTP → enter OTP → verify.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
### Email variant
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { useState } from "react";
|
|
16
|
+
import { Form } from "@lifesg/react-design-system/form";
|
|
17
|
+
import { OtpVerificationState } from "@lifesg/react-design-system/otp-verification";
|
|
18
|
+
|
|
19
|
+
const EmailOtpExample = () => {
|
|
20
|
+
const [otpState, setOtpState] = useState<OtpVerificationState>("default");
|
|
21
|
+
const [email, setEmail] = useState("");
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Form.OtpVerification
|
|
25
|
+
label="Verify your email"
|
|
26
|
+
type="email"
|
|
27
|
+
emailValue={email}
|
|
28
|
+
onEmailChange={setEmail}
|
|
29
|
+
otpState={otpState}
|
|
30
|
+
onOtpStateChange={setOtpState}
|
|
31
|
+
onSendOtp={async () => {
|
|
32
|
+
// Throw to surface the error inline — do NOT return an error string
|
|
33
|
+
const res = await api.sendEmailOtp(email);
|
|
34
|
+
if (!res.ok) throw new Error("Invalid email address");
|
|
35
|
+
}}
|
|
36
|
+
onVerifyOtp={async (otpInput) => {
|
|
37
|
+
const res = await api.verifyOtp(otpInput);
|
|
38
|
+
if (!res.ok)
|
|
39
|
+
throw new Error("Incorrect OTP. Please try again.");
|
|
40
|
+
}}
|
|
41
|
+
onResendOtp={async () => {
|
|
42
|
+
const res = await api.sendEmailOtp(email);
|
|
43
|
+
if (!res.ok) throw new Error("Could not resend OTP");
|
|
44
|
+
}}
|
|
45
|
+
verifyOtpCountdownTimer={300}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Phone variant (`fixedCountry` required)
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import { useState } from "react";
|
|
55
|
+
import { Form } from "@lifesg/react-design-system/form";
|
|
56
|
+
import { OtpVerificationState } from "@lifesg/react-design-system/otp-verification";
|
|
57
|
+
import { PhoneNumberInputValue } from "@lifesg/react-design-system/phone-number-input";
|
|
58
|
+
|
|
59
|
+
const PhoneOtpExample = () => {
|
|
60
|
+
const [otpState, setOtpState] = useState<OtpVerificationState>("default");
|
|
61
|
+
const [phone, setPhone] = useState<PhoneNumberInputValue>({ number: "" });
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Form.OtpVerification
|
|
65
|
+
label="Verify your mobile number"
|
|
66
|
+
type="phone-number"
|
|
67
|
+
phoneNumberValue={phone}
|
|
68
|
+
onPhoneNumberChange={setPhone}
|
|
69
|
+
fixedCountry // required when type="phone-number"; hides country selector
|
|
70
|
+
otpState={otpState}
|
|
71
|
+
onOtpStateChange={setOtpState}
|
|
72
|
+
onSendOtp={async () => {
|
|
73
|
+
const res = await api.sendSmsOtp(phone.number);
|
|
74
|
+
if (!res.ok) throw new Error("Invalid phone number");
|
|
75
|
+
}}
|
|
76
|
+
onVerifyOtp={async (otpInput) => {
|
|
77
|
+
const res = await api.verifyOtp(otpInput);
|
|
78
|
+
if (!res.ok)
|
|
79
|
+
throw new Error("Incorrect OTP. Please try again.");
|
|
80
|
+
}}
|
|
81
|
+
onResendOtp={async () => {
|
|
82
|
+
const res = await api.sendSmsOtp(phone.number);
|
|
83
|
+
if (!res.ok) throw new Error("Could not resend OTP");
|
|
84
|
+
}}
|
|
85
|
+
verifyOtpCountdownTimer={180}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Key gotcha — callbacks must throw, not return error strings.**
|
|
92
|
+
The component catches the thrown error and displays `error.message` inline.
|
|
93
|
+
Returning a value (or nothing) always signals success and advances the state machine.
|
|
94
|
+
|
|
95
|
+
## Phone-number-specific props
|
|
96
|
+
|
|
97
|
+
| Prop | Type | Description |
|
|
98
|
+
| --------------------- | ---------------------------------------- | ------------------------------------------------------ |
|
|
99
|
+
| `phoneNumberValue` | `PhoneNumberInputValue` | Controlled phone number value |
|
|
100
|
+
| `onPhoneNumberChange` | `(value: PhoneNumberInputValue) => void` | Called when phone input changes |
|
|
101
|
+
| `fixedCountry` | `true` | Locks the country code; required for phone-number type |
|
|
102
|
+
|
|
103
|
+
## Email-specific props
|
|
104
|
+
|
|
105
|
+
| Prop | Type | Description |
|
|
106
|
+
| --------------- | ------------------------- | ------------------------------- |
|
|
107
|
+
| `emailValue` | `string` | Controlled email value |
|
|
108
|
+
| `onEmailChange` | `(value: string) => void` | Called when email input changes |
|
|
109
|
+
|
|
110
|
+
## PhoneNumberInputValue
|
|
111
|
+
|
|
112
|
+
| Prop | Type | Description |
|
|
113
|
+
| ------------- | -------- | --------------------------- |
|
|
114
|
+
| `number` | `string` | Phone number |
|
|
115
|
+
| `countryCode` | `string` | Country code (e.g. `"+65"`) |
|
|
116
|
+
|
|
117
|
+
## Form-specific props
|
|
118
|
+
|
|
119
|
+
| Prop | Type | Description |
|
|
120
|
+
| ------- | -------------------------- | ----------- |
|
|
121
|
+
| `label` | `string \| FormLabelProps` | Field label |
|
|
122
|
+
|
|
123
|
+
## Props
|
|
124
|
+
|
|
125
|
+
| Prop | Type | Default | Description |
|
|
126
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
127
|
+
| `id` | `string` | — | — |
|
|
128
|
+
| `disabled` | `boolean` | — | — |
|
|
129
|
+
| `readOnly` | `boolean` | — | — |
|
|
130
|
+
| `className` | `string` | — | — |
|
|
131
|
+
| `inputId` | `string` | — | The `id` attribute applied to the contact input field. |
|
|
132
|
+
| `otpState` \* | `OtpVerificationState` | — | The current step in the OTP verification flow. Controls which sections are rendered. |
|
|
133
|
+
| `onOtpStateChange` \* | `(state: OtpVerificationState) => void` | — | Called when the component transition to a new `otpState`. |
|
|
134
|
+
| `onSendOtp` | `(() => Promise<void>)` | — | Called when the user initiates sending the OTP. On resolution, the component transitions to `"sent"`. |
|
|
135
|
+
| `sendOtpError` | `string` | — | Error message displayed below the contact input after a failed attempt. |
|
|
136
|
+
| `sendOtpPlaceholder` | `string` | — | Placeholder text for the contact input field. |
|
|
137
|
+
| `otpReminderInterval` | `number` | 120 | Interval in seconds at which an accessibility announcement is made, reminding the user how many seconds remain before they can resend the OTP. |
|
|
138
|
+
| `type` \* | `"email"` | — | Discriminant that sets the contact input to email mode. |
|
|
139
|
+
| `otpValue` | `\| { /** * Static text displayed before the OTP input. */ prefix?: string; /** * Character displayed between the `prefix` and OTP input.. */ separator?: string; /** * The current OTP string entered by the user. */ value?: string; }` | — | Controlled state of the OTP input field. |
|
|
140
|
+
| `onOtpChange` | `((value: string) => void)` | — | Called when the OTP input value changes. |
|
|
141
|
+
| `onVerifyOtp` | `((input: string) => Promise<void>)` | — | Called when the user submits the OTP for verification. On resolution the component transitions to `"verified"`. |
|
|
142
|
+
| `verifyOtpCountdownTimer` | `number` | 60 | Duration in seconds of the resend cooldown after an OTP is sent. |
|
|
143
|
+
| `onResendOtp` | `(() => Promise<void>)` | — | Called when the user requests to resend the OTP from the verification step. |
|
|
144
|
+
| `showVerifyOtpThumbnail` | `boolean` | — | Whether to display the thumbnail image in the OTP verification section, visible in the `"sent"` state. |
|
|
145
|
+
| `verifyOtpIcon` | `React.ReactNode` | — | Custom icon in the OTP verification section, visible in the `"sent"` state. |
|
|
146
|
+
| `verifyOtpTitle` | `string` | — | Custom heading in the OTP verification section, visible in the `"sent"` state. |
|
|
147
|
+
| `verifyOtpMessage` | `string` | — | Custom body text in the OTP verification section, visible in the `"sent"` state. |
|
|
148
|
+
| `verifyOtpError` | `string` | — | Error message displayed below the OTP input after a failed verification attempt. |
|
|
149
|
+
| `label` | `FormLabelProps \| string` | — | Label rendered above the input. Accepts a plain string or a `FormLabelProps` object to provide additional configuration. |
|
|
150
|
+
| `layoutType` | `FormElementLayoutType` | — | Controls whether the container uses flex layout or a responsive column grid. |
|
|
151
|
+
| `xxlCols` | `ColSpan` | — | Column span at the `xxl` breakpoint. |
|
|
152
|
+
| `xlCols` | `ColSpan` | — | Column span at the `xl` breakpoint. |
|
|
153
|
+
| `lgCols` | `ColSpan` | — | Column span at the `lg` breakpoint. |
|
|
154
|
+
| `mdCols` | `ColSpan` | — | Column span at the `md` breakpoint. |
|
|
155
|
+
| `smCols` | `ColSpan` | — | Column span at the `sm` breakpoint. |
|
|
156
|
+
| `xsCols` | `ColSpan` | — | Column span at the `xs` breakpoint. |
|
|
157
|
+
| `xxsCols` | `ColSpan` | — | Column span at the `xxs` (smallest) breakpoint. Acts as the base value. All larger breakpoints fall back to this when their own prop is not set. |
|
|
158
|
+
|
|
159
|
+
## Rules
|
|
160
|
+
|
|
161
|
+
- Always supply a `label` so the field is accessible.
|
|
162
|
+
- Control `otpState` externally and update it via `onOtpStateChange` — the component does not manage its own state.
|
|
163
|
+
- `onSendOtp`, `onVerifyOtp`, and `onResendOtp` must be async functions; throw an error inside them to trigger the corresponding error display (`sendOtpError`, `verifyOtpError`).
|
|
164
|
+
- Always pass `fixedCountry` when `type="phone-number"` — the prop is required to lock the country selector.
|
|
165
|
+
- Set `verifyOtpCountdownTimer` to match the actual OTP expiry window from your backend.
|
|
166
|
+
- Pass `sendOtpError` and `verifyOtpError` from server responses so users receive actionable feedback.
|
|
167
|
+
- `otpState` is externally controlled — the component does not manage its own flow state.
|
|
168
|
+
- `fixedCountry` is required when `type="phone-number"`.
|
|
169
|
+
|
|
170
|
+
## Anti-patterns
|
|
171
|
+
|
|
172
|
+
- Do not try to manage the step sequence manually — use `otpState` and let `onOtpStateChange` drive transitions.
|
|
173
|
+
- Do not use for non-identity-verification scenarios — for simple code inputs, use a plain `Form.Input`.
|
|
174
|
+
- Do not omit `fixedCountry` for `type="phone-number"` — the country selector will be shown unexpectedly.
|
|
175
|
+
- Do not use synchronous functions for `onSendOtp`, `onVerifyOtp`, or `onResendOtp` — they must return Promises.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Form.PhoneNumberInput
|
|
2
|
+
|
|
3
|
+
Import: `import { Form } from "@lifesg/react-design-system/form"`
|
|
4
|
+
|
|
5
|
+
Type import: `import type { PhoneNumberInputValue } from "@lifesg/react-design-system/phone-number-input"`
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
- Collecting a phone number that requires a country code prefix.
|
|
10
|
+
- International forms where users may have numbers from different countries.
|
|
11
|
+
- Use `fixedCountry` when the service only accepts numbers from one country (e.g. Singapore-only services).
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import styles from "./MyForm.module.css";
|
|
17
|
+
|
|
18
|
+
<Form.PhoneNumberInput className={styles.phoneNumberInput} ... />
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```css
|
|
22
|
+
/* MyForm.module.css */
|
|
23
|
+
:where(body.fds-breakpoint-lg-min) .phoneNumberInput {
|
|
24
|
+
max-width: 24rem;
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Defaults to 4 columns wide at `lg` and above; full-width on mobile. Use `max-width` via a CSS Module class.
|
|
29
|
+
|
|
30
|
+
## PhoneNumberInputValue
|
|
31
|
+
|
|
32
|
+
| Prop | Type | Description |
|
|
33
|
+
| ------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
34
|
+
| `number` | `string` | The phone number |
|
|
35
|
+
| `countryCode` | `string` | The country code (e.g. `"+65"`). The `+` prefix is optional on input but always included in the `onChange` return value |
|
|
36
|
+
|
|
37
|
+
## Props
|
|
38
|
+
|
|
39
|
+
| Prop | Type | Default | Description |
|
|
40
|
+
| ------------------------- | ------------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
41
|
+
| `value` | `PhoneNumberInputValue` | — | Controlled value containing the selected country code and phone number. |
|
|
42
|
+
| `error` | `boolean` | — | Renders the input in an error state when `true`. |
|
|
43
|
+
| `allowClear` | `boolean` | — | Shows a clear button inside the input when `true` and the field has a value. |
|
|
44
|
+
| `className` | `string` | — | — |
|
|
45
|
+
| `noBorder` | `boolean` | false | Removes the visible border from the input group when `true`. |
|
|
46
|
+
| `fixedCountry` | `boolean` | false | When `true`, the country code is displayed as a static label and the user cannot change it. When `false`, a searchable dropdown is rendered. |
|
|
47
|
+
| `onChange` | `((value: PhoneNumberInputValue) => void)` | — | Called whenever the country or phone number changes. |
|
|
48
|
+
| `onClear` | `() => void` | — | Called when the clear button is activated. |
|
|
49
|
+
| `onBlur` | `(() => void)` | — | Called when the input loses focus. |
|
|
50
|
+
| `optionPlaceholder` | `string` | "Select" | Placeholder text shown in the country code dropdown when no country is selected. |
|
|
51
|
+
| `optionSearchPlaceholder` | `string` | — | Placeholder text shown inside the search field of the country code dropdown. Only relevant when `enableSearch` is `true`. |
|
|
52
|
+
| `enableSearch` | `boolean` | — | Enables a search field inside the country code dropdown. |
|
|
53
|
+
| `onHideOptions` | `(() => void)` | — | Called when the country code dropdown closes. |
|
|
54
|
+
| `onShowOptions` | `(() => void)` | — | Called when the country code dropdown opens. |
|
|
55
|
+
| `disabled` | `boolean` | — | — |
|
|
56
|
+
| `name` | `string` | — | — |
|
|
57
|
+
| `pattern` | `string` | — | — |
|
|
58
|
+
| `placeholder` | `string` | — | — |
|
|
59
|
+
| `readOnly` | `boolean` | — | — |
|
|
60
|
+
| `id` | `string` | — | — |
|
|
61
|
+
| `autoComplete` | `string` | — | — |
|
|
62
|
+
| `getAriaLabel` | `((country?: CountryValue) => string)` | — | Returns the accessible label for the phone number input. |
|
|
63
|
+
| `label` | `FormLabelProps \| string` | — | Label rendered above the input. Accepts a plain string or a `FormLabelProps` object to provide additional configuration. |
|
|
64
|
+
| `errorMessage` | `string \| React.ReactNode` | — | Inline error text rendered below the input. |
|
|
65
|
+
| `layoutType` | `FormElementLayoutType` | — | Controls whether the container uses flex layout or a responsive column grid. |
|
|
66
|
+
| `xxlCols` | `ColSpan` | — | Column span at the `xxl` breakpoint. |
|
|
67
|
+
| `xlCols` | `ColSpan` | — | Column span at the `xl` breakpoint. |
|
|
68
|
+
| `lgCols` | `ColSpan` | — | Column span at the `lg` breakpoint. |
|
|
69
|
+
| `mdCols` | `ColSpan` | — | Column span at the `md` breakpoint. |
|
|
70
|
+
| `smCols` | `ColSpan` | — | Column span at the `sm` breakpoint. |
|
|
71
|
+
| `xsCols` | `ColSpan` | — | Column span at the `xs` breakpoint. |
|
|
72
|
+
| `xxsCols` | `ColSpan` | — | Column span at the `xxs` (smallest) breakpoint. Acts as the base value. All larger breakpoints fall back to this when their own prop is not set. |
|
|
73
|
+
|
|
74
|
+
## Rules
|
|
75
|
+
|
|
76
|
+
- Always supply a `label` so the field is accessible.
|
|
77
|
+
- Pass `value` and `onChange` together for controlled usage; `onChange` returns `{ countryCode, number }`.
|
|
78
|
+
- Use `enableSearch` when the country list is long and users need to find their country quickly.
|
|
79
|
+
- Use `fixedCountry` together with a pre-set `value.countryCode` so the locked code is visible.
|
|
80
|
+
- Use `allowClear` when the field can be optionally cleared by the user.
|
|
81
|
+
- Set `errorMessage` rather than `error` alone so users understand what is wrong.
|
|
82
|
+
|
|
83
|
+
## Anti-patterns
|
|
84
|
+
|
|
85
|
+
- Do not use a plain text input for international phone numbers — formatting and country code handling must be consistent.
|
|
86
|
+
- Do not use without `fixedCountry` when only one country is supported — it creates unnecessary friction.
|
|
87
|
+
- Do not omit `value.countryCode` when `fixedCountry` is true — the locked code won't render without it.
|