@archytechtor/ui 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +105 -2
- package/dist/components/accordion/accordion.d.ts +25 -0
- package/dist/components/accordion/accordion.js +88 -0
- package/dist/components/accordion/index.d.ts +3 -0
- package/dist/components/accordion/index.js +1 -0
- package/dist/components/accordion/types.d.ts +39 -0
- package/dist/components/alert/alert.d.ts +25 -0
- package/dist/components/alert/alert.js +59 -0
- package/dist/components/alert/index.d.ts +3 -0
- package/dist/components/alert/index.js +3 -0
- package/dist/components/alert/types.d.ts +32 -0
- package/dist/components/alert/types.js +19 -0
- package/dist/components/alert-dialog/alert-dialog.d.ts +73 -0
- package/dist/components/alert-dialog/alert-dialog.js +170 -0
- package/dist/components/alert-dialog/index.d.ts +3 -0
- package/dist/components/alert-dialog/index.js +1 -0
- package/dist/components/alert-dialog/types.d.ts +80 -0
- package/dist/components/aspect-ratio/aspect-ratio.d.ts +8 -0
- package/dist/components/aspect-ratio/aspect-ratio.js +20 -0
- package/dist/components/aspect-ratio/index.d.ts +3 -0
- package/dist/components/aspect-ratio/index.js +1 -0
- package/dist/components/aspect-ratio/types.d.ts +8 -0
- package/dist/components/attachment/attachment.d.ts +57 -0
- package/dist/components/attachment/attachment.js +139 -0
- package/dist/components/attachment/index.d.ts +3 -0
- package/dist/components/attachment/index.js +3 -0
- package/dist/components/attachment/types.d.ts +79 -0
- package/dist/components/attachment/types.js +43 -0
- package/dist/components/avatar/avatar.d.ts +39 -0
- package/dist/components/avatar/avatar.js +88 -0
- package/dist/components/avatar/index.d.ts +3 -0
- package/dist/components/avatar/index.js +3 -0
- package/dist/components/avatar/types.d.ts +54 -0
- package/dist/components/avatar/types.js +21 -0
- package/dist/components/badge/badge.d.ts +7 -0
- package/dist/components/badge/badge.js +23 -0
- package/dist/components/badge/index.d.ts +3 -0
- package/dist/components/badge/index.js +3 -0
- package/dist/components/badge/types.d.ts +21 -0
- package/dist/components/badge/types.js +26 -0
- package/dist/components/breadcrumb/breadcrumb.d.ts +43 -0
- package/dist/components/breadcrumb/breadcrumb.js +115 -0
- package/dist/components/breadcrumb/index.d.ts +3 -0
- package/dist/components/breadcrumb/index.js +1 -0
- package/dist/components/breadcrumb/types.d.ts +36 -0
- package/dist/components/bubble/bubble.d.ts +25 -0
- package/dist/components/bubble/bubble.js +69 -0
- package/dist/components/bubble/index.d.ts +3 -0
- package/dist/components/bubble/index.js +3 -0
- package/dist/components/bubble/types.d.ts +51 -0
- package/dist/components/bubble/types.js +60 -0
- package/dist/components/button/button.d.ts +7 -0
- package/dist/components/button/button.js +155 -0
- package/dist/components/button/index.d.ts +4 -0
- package/dist/components/button/index.js +4 -0
- package/dist/components/button/types.d.ts +80 -0
- package/dist/components/button/types.js +99 -0
- package/dist/components/button/use-hold-to-confirm.d.ts +25 -0
- package/dist/components/button/use-hold-to-confirm.js +161 -0
- package/dist/components/button-group/button-group.d.ts +20 -0
- package/dist/components/button-group/button-group.js +62 -0
- package/dist/components/button-group/index.d.ts +3 -0
- package/dist/components/button-group/index.js +3 -0
- package/dist/components/button-group/types.d.ts +30 -0
- package/dist/components/button-group/types.js +19 -0
- package/dist/components/calendar/calendar.d.ts +15 -0
- package/dist/components/calendar/calendar.js +157 -0
- package/dist/components/calendar/index.d.ts +3 -0
- package/dist/components/calendar/index.js +1 -0
- package/dist/components/calendar/types.d.ts +21 -0
- package/dist/components/card/card.d.ts +43 -0
- package/dist/components/card/card.js +97 -0
- package/dist/components/card/index.d.ts +3 -0
- package/dist/components/card/index.js +3 -0
- package/dist/components/card/types.d.ts +44 -0
- package/dist/components/card/types.js +19 -0
- package/dist/components/carousel/carousel.d.ts +36 -0
- package/dist/components/carousel/carousel.js +190 -0
- package/dist/components/carousel/index.d.ts +3 -0
- package/dist/components/carousel/index.js +1 -0
- package/dist/components/carousel/types.d.ts +79 -0
- package/dist/components/chart/chart.d.ts +35 -0
- package/dist/components/chart/chart.js +212 -0
- package/dist/components/chart/index.d.ts +3 -0
- package/dist/components/chart/index.js +1 -0
- package/dist/components/chart/types.d.ts +89 -0
- package/dist/components/chart/types.js +7 -0
- package/dist/components/checkbox/checkbox.d.ts +7 -0
- package/dist/components/checkbox/checkbox.js +31 -0
- package/dist/components/checkbox/index.d.ts +3 -0
- package/dist/components/checkbox/index.js +1 -0
- package/dist/components/checkbox/types.d.ts +25 -0
- package/dist/components/code-block/code-block.d.ts +49 -0
- package/dist/components/code-block/code-block.js +188 -0
- package/dist/components/code-block/highlighter.d.ts +15 -0
- package/dist/components/code-block/highlighter.js +123 -0
- package/dist/components/code-block/index.d.ts +5 -0
- package/dist/components/code-block/index.js +5 -0
- package/dist/components/code-block/types.d.ts +94 -0
- package/dist/components/code-block/types.js +38 -0
- package/dist/components/code-block/use-highlighted-code.d.ts +9 -0
- package/dist/components/code-block/use-highlighted-code.js +67 -0
- package/dist/components/collapsible/collapsible.d.ts +19 -0
- package/dist/components/collapsible/collapsible.js +44 -0
- package/dist/components/collapsible/index.d.ts +3 -0
- package/dist/components/collapsible/index.js +1 -0
- package/dist/components/collapsible/types.d.ts +26 -0
- package/dist/components/combobox/combobox.d.ts +104 -0
- package/dist/components/combobox/combobox.js +365 -0
- package/dist/components/combobox/index.d.ts +3 -0
- package/dist/components/combobox/index.js +1 -0
- package/dist/components/combobox/types.d.ts +229 -0
- package/dist/components/command/command.d.ts +55 -0
- package/dist/components/command/command.js +179 -0
- package/dist/components/command/index.d.ts +3 -0
- package/dist/components/command/index.js +1 -0
- package/dist/components/command/types.d.ts +69 -0
- package/dist/components/context-menu/context-menu.d.ts +92 -0
- package/dist/components/context-menu/context-menu.js +270 -0
- package/dist/components/context-menu/index.d.ts +3 -0
- package/dist/components/context-menu/index.js +1 -0
- package/dist/components/context-menu/types.d.ts +138 -0
- package/dist/components/data-table/data-table-features.d.ts +21 -0
- package/dist/components/data-table/data-table-features.js +22 -0
- package/dist/components/data-table/data-table.d.ts +69 -0
- package/dist/components/data-table/data-table.js +283 -0
- package/dist/components/data-table/index.d.ts +4 -0
- package/dist/components/data-table/index.js +3 -0
- package/dist/components/data-table/types.d.ts +63 -0
- package/dist/components/date-input/date-input.d.ts +7 -0
- package/dist/components/date-input/date-input.js +147 -0
- package/dist/components/date-input/index.d.ts +3 -0
- package/dist/components/date-input/index.js +1 -0
- package/dist/components/date-input/types.d.ts +27 -0
- package/dist/components/date-picker/date-picker.d.ts +7 -0
- package/dist/components/date-picker/date-picker.js +74 -0
- package/dist/components/date-picker/index.d.ts +3 -0
- package/dist/components/date-picker/index.js +1 -0
- package/dist/components/date-picker/types.d.ts +38 -0
- package/dist/components/date-range-picker/date-range-picker.d.ts +7 -0
- package/dist/components/date-range-picker/date-range-picker.js +89 -0
- package/dist/components/date-range-picker/index.d.ts +3 -0
- package/dist/components/date-range-picker/index.js +1 -0
- package/dist/components/date-range-picker/types.d.ts +43 -0
- package/dist/components/dialog/dialog.d.ts +61 -0
- package/dist/components/dialog/dialog.js +160 -0
- package/dist/components/dialog/index.d.ts +3 -0
- package/dist/components/dialog/index.js +1 -0
- package/dist/components/dialog/types.d.ts +68 -0
- package/dist/components/drawer/drawer.d.ts +69 -0
- package/dist/components/drawer/drawer.js +245 -0
- package/dist/components/drawer/index.d.ts +3 -0
- package/dist/components/drawer/index.js +1 -0
- package/dist/components/drawer/types.d.ts +93 -0
- package/dist/components/dropdown-menu/dropdown-menu.d.ts +92 -0
- package/dist/components/dropdown-menu/dropdown-menu.js +268 -0
- package/dist/components/dropdown-menu/index.d.ts +3 -0
- package/dist/components/dropdown-menu/index.js +1 -0
- package/dist/components/dropdown-menu/types.d.ts +143 -0
- package/dist/components/empty/empty.d.ts +37 -0
- package/dist/components/empty/empty.js +84 -0
- package/dist/components/empty/index.d.ts +3 -0
- package/dist/components/empty/index.js +3 -0
- package/dist/components/empty/types.d.ts +39 -0
- package/dist/components/empty/types.js +13 -0
- package/dist/components/expanded-card/expanded-card.d.ts +43 -0
- package/dist/components/expanded-card/expanded-card.js +352 -0
- package/dist/components/expanded-card/index.d.ts +3 -0
- package/dist/components/expanded-card/index.js +1 -0
- package/dist/components/expanded-card/types.d.ts +52 -0
- package/dist/components/field/field.d.ts +62 -0
- package/dist/components/field/field.js +173 -0
- package/dist/components/field/index.d.ts +3 -0
- package/dist/components/field/index.js +3 -0
- package/dist/components/field/types.d.ts +81 -0
- package/dist/components/field/types.js +21 -0
- package/dist/components/form/form.d.ts +49 -0
- package/dist/components/form/form.js +136 -0
- package/dist/components/form/index.d.ts +3 -0
- package/dist/components/form/index.js +1 -0
- package/dist/components/form/types.d.ts +59 -0
- package/dist/components/hover-card/hover-card.d.ts +19 -0
- package/dist/components/hover-card/hover-card.js +63 -0
- package/dist/components/hover-card/index.d.ts +3 -0
- package/dist/components/hover-card/index.js +1 -0
- package/dist/components/hover-card/types.d.ts +27 -0
- package/dist/components/image-gallery/image-gallery.d.ts +8 -0
- package/dist/components/image-gallery/image-gallery.js +294 -0
- package/dist/components/image-gallery/index.d.ts +4 -0
- package/dist/components/image-gallery/index.js +3 -0
- package/dist/components/image-gallery/masonry.d.ts +11 -0
- package/dist/components/image-gallery/masonry.js +62 -0
- package/dist/components/image-gallery/types.d.ts +80 -0
- package/dist/components/input/index.d.ts +3 -0
- package/dist/components/input/index.js +1 -0
- package/dist/components/input/input.d.ts +7 -0
- package/dist/components/input/input.js +18 -0
- package/dist/components/input/types.d.ts +28 -0
- package/dist/components/input-group/index.d.ts +3 -0
- package/dist/components/input-group/index.js +3 -0
- package/dist/components/input-group/input-group.d.ts +37 -0
- package/dist/components/input-group/input-group.js +103 -0
- package/dist/components/input-group/types.d.ts +64 -0
- package/dist/components/input-group/types.js +31 -0
- package/dist/components/input-otp/index.d.ts +3 -0
- package/dist/components/input-otp/index.js +1 -0
- package/dist/components/input-otp/input-otp.d.ts +25 -0
- package/dist/components/input-otp/input-otp.js +76 -0
- package/dist/components/input-otp/types.d.ts +43 -0
- package/dist/components/item/index.d.ts +3 -0
- package/dist/components/item/index.js +3 -0
- package/dist/components/item/item.d.ts +61 -0
- package/dist/components/item/item.js +149 -0
- package/dist/components/item/types.d.ts +75 -0
- package/dist/components/item/types.js +48 -0
- package/dist/components/kbd/index.d.ts +3 -0
- package/dist/components/kbd/index.js +1 -0
- package/dist/components/kbd/kbd.d.ts +13 -0
- package/dist/components/kbd/kbd.js +30 -0
- package/dist/components/kbd/types.d.ts +9 -0
- package/dist/components/label/index.d.ts +3 -0
- package/dist/components/label/index.js +1 -0
- package/dist/components/label/label.d.ts +7 -0
- package/dist/components/label/label.js +17 -0
- package/dist/components/label/types.d.ts +8 -0
- package/dist/components/loader/index.d.ts +3 -0
- package/dist/components/loader/index.js +3 -0
- package/dist/components/loader/loader.d.ts +8 -0
- package/dist/components/loader/loader.js +386 -0
- package/dist/components/loader/types.d.ts +27 -0
- package/dist/components/loader/types.js +58 -0
- package/dist/components/marker/index.d.ts +3 -0
- package/dist/components/marker/index.js +3 -0
- package/dist/components/marker/marker.d.ts +19 -0
- package/dist/components/marker/marker.js +51 -0
- package/dist/components/marker/types.d.ts +30 -0
- package/dist/components/marker/types.js +16 -0
- package/dist/components/menubar/index.d.ts +3 -0
- package/dist/components/menubar/index.js +1 -0
- package/dist/components/menubar/menubar.d.ts +98 -0
- package/dist/components/menubar/menubar.js +284 -0
- package/dist/components/menubar/types.d.ts +146 -0
- package/dist/components/message/index.d.ts +3 -0
- package/dist/components/message/index.js +1 -0
- package/dist/components/message/message.d.ts +37 -0
- package/dist/components/message/message.js +83 -0
- package/dist/components/message/types.d.ts +32 -0
- package/dist/components/message-scroller/index.d.ts +4 -0
- package/dist/components/message-scroller/index.js +3 -0
- package/dist/components/message-scroller/message-scroller-primitive.d.ts +21 -0
- package/dist/components/message-scroller/message-scroller-primitive.js +1349 -0
- package/dist/components/message-scroller/message-scroller.d.ts +39 -0
- package/dist/components/message-scroller/message-scroller.js +103 -0
- package/dist/components/message-scroller/types.d.ts +107 -0
- package/dist/components/money-input/index.d.ts +3 -0
- package/dist/components/money-input/index.js +1 -0
- package/dist/components/money-input/money-input.d.ts +7 -0
- package/dist/components/money-input/money-input.js +143 -0
- package/dist/components/money-input/types.d.ts +20 -0
- package/dist/components/morph-image/index.d.ts +3 -0
- package/dist/components/morph-image/index.js +3 -0
- package/dist/components/morph-image/morph-image.d.ts +8 -0
- package/dist/components/morph-image/morph-image.js +155 -0
- package/dist/components/morph-image/types.d.ts +38 -0
- package/dist/components/morph-image/types.js +18 -0
- package/dist/components/native-select/index.d.ts +3 -0
- package/dist/components/native-select/index.js +3 -0
- package/dist/components/native-select/native-select.d.ts +20 -0
- package/dist/components/native-select/native-select.js +57 -0
- package/dist/components/native-select/types.d.ts +52 -0
- package/dist/components/native-select/types.js +23 -0
- package/dist/components/navigation-menu/index.d.ts +3 -0
- package/dist/components/navigation-menu/index.js +3 -0
- package/dist/components/navigation-menu/navigation-menu.d.ts +50 -0
- package/dist/components/navigation-menu/navigation-menu.js +145 -0
- package/dist/components/navigation-menu/types.d.ts +79 -0
- package/dist/components/navigation-menu/types.js +15 -0
- package/dist/components/number-stepper/index.d.ts +3 -0
- package/dist/components/number-stepper/index.js +3 -0
- package/dist/components/number-stepper/number-stepper.d.ts +9 -0
- package/dist/components/number-stepper/number-stepper.js +223 -0
- package/dist/components/number-stepper/types.d.ts +47 -0
- package/dist/components/number-stepper/types.js +174 -0
- package/dist/components/pagination/index.d.ts +3 -0
- package/dist/components/pagination/index.js +1 -0
- package/dist/components/pagination/pagination.d.ts +43 -0
- package/dist/components/pagination/pagination.js +121 -0
- package/dist/components/pagination/types.d.ts +37 -0
- package/dist/components/phone-input/index.d.ts +3 -0
- package/dist/components/phone-input/index.js +1 -0
- package/dist/components/phone-input/phone-input.d.ts +7 -0
- package/dist/components/phone-input/phone-input.js +54 -0
- package/dist/components/phone-input/types.d.ts +12 -0
- package/dist/components/popover/index.d.ts +3 -0
- package/dist/components/popover/index.js +1 -0
- package/dist/components/popover/popover.d.ts +37 -0
- package/dist/components/popover/popover.js +100 -0
- package/dist/components/popover/types.d.ts +51 -0
- package/dist/components/progress/index.d.ts +3 -0
- package/dist/components/progress/index.js +1 -0
- package/dist/components/progress/progress.d.ts +32 -0
- package/dist/components/progress/progress.js +100 -0
- package/dist/components/progress/types.d.ts +31 -0
- package/dist/components/questionnaire/index.d.ts +3 -0
- package/dist/components/questionnaire/index.js +1 -0
- package/dist/components/questionnaire/questionnaire-primitive.js +1563 -0
- package/dist/components/questionnaire/questionnaire.d.ts +91 -0
- package/dist/components/questionnaire/questionnaire.js +315 -0
- package/dist/components/questionnaire/types.d.ts +203 -0
- package/dist/components/radio-group/index.d.ts +3 -0
- package/dist/components/radio-group/index.js +1 -0
- package/dist/components/radio-group/radio-group.d.ts +13 -0
- package/dist/components/radio-group/radio-group.js +44 -0
- package/dist/components/radio-group/types.d.ts +33 -0
- package/dist/components/reorder-list/index.d.ts +3 -0
- package/dist/components/reorder-list/index.js +1 -0
- package/dist/components/reorder-list/reorder-list.d.ts +19 -0
- package/dist/components/reorder-list/reorder-list.js +279 -0
- package/dist/components/reorder-list/types.d.ts +65 -0
- package/dist/components/resizable/index.d.ts +3 -0
- package/dist/components/resizable/index.js +1 -0
- package/dist/components/resizable/resizable.d.ts +19 -0
- package/dist/components/resizable/resizable.js +44 -0
- package/dist/components/resizable/types.d.ts +89 -0
- package/dist/components/scroll-area/index.d.ts +3 -0
- package/dist/components/scroll-area/index.js +1 -0
- package/dist/components/scroll-area/scroll-area.d.ts +13 -0
- package/dist/components/scroll-area/scroll-area.js +47 -0
- package/dist/components/scroll-area/types.d.ts +19 -0
- package/dist/components/select/index.d.ts +3 -0
- package/dist/components/select/index.js +1 -0
- package/dist/components/select/select.d.ts +61 -0
- package/dist/components/select/select.js +183 -0
- package/dist/components/select/types.d.ts +100 -0
- package/dist/components/separator/index.d.ts +3 -0
- package/dist/components/separator/index.js +1 -0
- package/dist/components/separator/separator.d.ts +7 -0
- package/dist/components/separator/separator.js +20 -0
- package/dist/components/separator/types.d.ts +9 -0
- package/dist/components/sheet/index.d.ts +3 -0
- package/dist/components/sheet/index.js +1 -0
- package/dist/components/sheet/sheet.d.ts +61 -0
- package/dist/components/sheet/sheet.js +155 -0
- package/dist/components/sheet/types.d.ts +69 -0
- package/dist/components/sidebar/index.d.ts +3 -0
- package/dist/components/sidebar/index.js +3 -0
- package/dist/components/sidebar/sidebar.d.ts +145 -0
- package/dist/components/sidebar/sidebar.js +550 -0
- package/dist/components/sidebar/types.d.ts +186 -0
- package/dist/components/sidebar/types.js +42 -0
- package/dist/components/sidebar/use-mobile.js +21 -0
- package/dist/components/skeleton/index.d.ts +3 -0
- package/dist/components/skeleton/index.js +1 -0
- package/dist/components/skeleton/skeleton.d.ts +8 -0
- package/dist/components/skeleton/skeleton.js +18 -0
- package/dist/components/skeleton/types.d.ts +5 -0
- package/dist/components/slider/index.d.ts +3 -0
- package/dist/components/slider/index.js +1 -0
- package/dist/components/slider/slider.d.ts +7 -0
- package/dist/components/slider/slider.js +106 -0
- package/dist/components/slider/types.d.ts +39 -0
- package/dist/components/spinner/index.d.ts +3 -0
- package/dist/components/spinner/index.js +1 -0
- package/dist/components/spinner/spinner.d.ts +7 -0
- package/dist/components/spinner/spinner.js +21 -0
- package/dist/components/spinner/types.d.ts +8 -0
- package/dist/components/switch/index.d.ts +3 -0
- package/dist/components/switch/index.js +3 -0
- package/dist/components/switch/switch.d.ts +7 -0
- package/dist/components/switch/switch.js +32 -0
- package/dist/components/switch/types.d.ts +37 -0
- package/dist/components/switch/types.js +23 -0
- package/dist/components/table/index.d.ts +3 -0
- package/dist/components/table/index.js +1 -0
- package/dist/components/table/table.d.ts +49 -0
- package/dist/components/table/table.js +112 -0
- package/dist/components/table/types.d.ts +33 -0
- package/dist/components/tabs/index.d.ts +3 -0
- package/dist/components/tabs/index.js +3 -0
- package/dist/components/tabs/tabs.d.ts +25 -0
- package/dist/components/tabs/tabs.js +81 -0
- package/dist/components/tabs/types.d.ts +61 -0
- package/dist/components/tabs/types.js +18 -0
- package/dist/components/textarea/index.d.ts +3 -0
- package/dist/components/textarea/index.js +1 -0
- package/dist/components/textarea/textarea.d.ts +7 -0
- package/dist/components/textarea/textarea.js +17 -0
- package/dist/components/textarea/types.d.ts +26 -0
- package/dist/components/toast/index.d.ts +3 -0
- package/dist/components/toast/index.js +1 -0
- package/dist/components/toast/toast.d.ts +63 -0
- package/dist/components/toast/toast.js +321 -0
- package/dist/components/toast/types.d.ts +120 -0
- package/dist/components/toggle/index.d.ts +3 -0
- package/dist/components/toggle/index.js +3 -0
- package/dist/components/toggle/toggle.d.ts +7 -0
- package/dist/components/toggle/toggle.js +32 -0
- package/dist/components/toggle/types.d.ts +35 -0
- package/dist/components/toggle/types.js +37 -0
- package/dist/components/toggle-group/index.d.ts +3 -0
- package/dist/components/toggle-group/index.js +1 -0
- package/dist/components/toggle-group/toggle-group.d.ts +13 -0
- package/dist/components/toggle-group/toggle-group.js +88 -0
- package/dist/components/toggle-group/types.d.ts +52 -0
- package/dist/components/tooltip/index.d.ts +3 -0
- package/dist/components/tooltip/index.js +1 -0
- package/dist/components/tooltip/tooltip.d.ts +25 -0
- package/dist/components/tooltip/tooltip.js +79 -0
- package/dist/components/tooltip/types.d.ts +48 -0
- package/dist/components/trace/index.d.ts +3 -0
- package/dist/components/trace/index.js +1 -0
- package/dist/components/trace/trace.d.ts +52 -0
- package/dist/components/trace/trace.js +132 -0
- package/dist/components/trace/types.d.ts +54 -0
- package/dist/components/typography/index.d.ts +3 -0
- package/dist/components/typography/index.js +3 -0
- package/dist/components/typography/types.d.ts +29 -0
- package/dist/components/typography/types.js +25 -0
- package/dist/components/typography/typography.d.ts +7 -0
- package/dist/components/typography/typography.js +40 -0
- package/dist/components/video-player/index.d.ts +6 -0
- package/dist/components/video-player/index.js +5 -0
- package/dist/components/video-player/types.d.ts +143 -0
- package/dist/components/video-player/use-video-player.js +319 -0
- package/dist/components/video-player/video-player-context.d.ts +45 -0
- package/dist/components/video-player/video-player-context.js +15 -0
- package/dist/components/video-player/video-player-controls.d.ts +38 -0
- package/dist/components/video-player/video-player-controls.js +226 -0
- package/dist/components/video-player/video-player-overlay.d.ts +13 -0
- package/dist/components/video-player/video-player-overlay.js +113 -0
- package/dist/components/video-player/video-player.d.ts +8 -0
- package/dist/components/video-player/video-player.js +139 -0
- package/dist/index.d.ts +244 -0
- package/dist/index.js +114 -0
- package/dist/lib/floating.d.ts +44 -0
- package/dist/lib/focus-source.js +51 -0
- package/dist/lib/format.d.ts +67 -0
- package/dist/lib/format.js +130 -0
- package/dist/lib/locale.d.ts +104 -0
- package/dist/lib/locale.js +103 -0
- package/dist/lib/part-props.d.ts +15 -0
- package/dist/lib/use-controllable-state.js +22 -0
- package/dist/lib/utils.d.ts +9 -0
- package/dist/lib/utils.js +16 -0
- package/dist/styles/index.css +126 -0
- package/dist/styles/theme.css +80 -0
- package/dist/styles/utilities.css +553 -0
- package/package.json +89 -3
|
@@ -0,0 +1,1563 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { mergeProps } from "@base-ui/react/merge-props";
|
|
3
|
+
import { createContext, createElement, use, useCallback, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { useRender } from "@base-ui/react/use-render";
|
|
5
|
+
// src/components/questionnaire/questionnaire-primitive.ts
|
|
6
|
+
/** Типы полей, у которых стрелки вверх/вниз переводят фокус, пока поле пустое */
|
|
7
|
+
const TEXT_INPUT_TYPES = [
|
|
8
|
+
"email",
|
|
9
|
+
"password",
|
|
10
|
+
"search",
|
|
11
|
+
"tel",
|
|
12
|
+
"text",
|
|
13
|
+
"url"
|
|
14
|
+
];
|
|
15
|
+
/** Типы input, которые не считаются полями ввода текста */
|
|
16
|
+
const NON_TEXT_INPUT_TYPES = [
|
|
17
|
+
"button",
|
|
18
|
+
"checkbox",
|
|
19
|
+
"radio",
|
|
20
|
+
"reset",
|
|
21
|
+
"submit"
|
|
22
|
+
];
|
|
23
|
+
const RootContext = createContext(null);
|
|
24
|
+
const ItemContext = createContext(null);
|
|
25
|
+
const ChoiceContext = createContext(null);
|
|
26
|
+
/**
|
|
27
|
+
* Контекст опроса или ошибка, если часть вне Questionnaire
|
|
28
|
+
* @param part - имя части для текста ошибки
|
|
29
|
+
* @returns контекст опроса
|
|
30
|
+
*/
|
|
31
|
+
function useRootContext(part) {
|
|
32
|
+
const context = use(RootContext);
|
|
33
|
+
if (!context) throw new Error(`${part} должен находиться внутри Questionnaire`);
|
|
34
|
+
return context;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Контекст вопроса или ошибка, если часть вне QuestionnaireItem
|
|
38
|
+
* @param part - имя части для текста ошибки
|
|
39
|
+
* @returns контекст вопроса
|
|
40
|
+
*/
|
|
41
|
+
function useItemContext(part) {
|
|
42
|
+
const context = use(ItemContext);
|
|
43
|
+
if (!context) throw new Error(`${part} должен находиться внутри QuestionnaireItem`);
|
|
44
|
+
return context;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Контекст варианта или ошибка, если часть вне QuestionnaireChoice
|
|
48
|
+
* @param part - имя части для текста ошибки
|
|
49
|
+
* @returns контекст варианта
|
|
50
|
+
*/
|
|
51
|
+
function useChoiceContext(part) {
|
|
52
|
+
const context = use(ChoiceContext);
|
|
53
|
+
if (!context) throw new Error(`${part} должен находиться внутри QuestionnaireChoice`);
|
|
54
|
+
return context;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Собрать список через пробел без пустых частей
|
|
58
|
+
* @param tokens - части списка
|
|
59
|
+
* @returns строка или undefined, если частей нет
|
|
60
|
+
*/
|
|
61
|
+
function joinTokens(tokens) {
|
|
62
|
+
const result = [];
|
|
63
|
+
for (const token of tokens) if (token) result.push(token);
|
|
64
|
+
return result.join(" ") || void 0;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Есть ли в значении непустой текст
|
|
68
|
+
* @param value - значение поля или список значений
|
|
69
|
+
* @returns true, если есть хотя бы один непустой текст
|
|
70
|
+
*/
|
|
71
|
+
function hasText(value) {
|
|
72
|
+
if (Array.isArray(value)) return value.some((part) => String(part).trim().length > 0);
|
|
73
|
+
return value !== null && value !== void 0 && String(value).trim().length > 0;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Клавиши подсказок по режиму
|
|
77
|
+
* @param mode - буквы или цифры
|
|
78
|
+
* @returns A–Z, 1–9 или пустой список
|
|
79
|
+
*/
|
|
80
|
+
function getShortcutKeys(mode) {
|
|
81
|
+
if (mode === "letters") return Array.from({ length: 26 }, (_, index) => String.fromCharCode(65 + index));
|
|
82
|
+
if (mode === "numbers") return Array.from({ length: 9 }, (_, index) => String(index + 1));
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Клавиша подсказки по нажатой клавише
|
|
87
|
+
* @param key - нажатая клавиша
|
|
88
|
+
* @param mode - буквы или цифры
|
|
89
|
+
* @returns клавиша подсказки или null
|
|
90
|
+
*/
|
|
91
|
+
function getShortcutByKey(key, mode) {
|
|
92
|
+
const shortcut = mode === "letters" ? key.toUpperCase() : key;
|
|
93
|
+
if (getShortcutKeys(mode).includes(shortcut)) return shortcut;
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Значение aria-keyshortcuts ответа
|
|
98
|
+
* @param shortcut - клавиша варианта
|
|
99
|
+
* @param withEnter - Enter переводит к следующему вопросу
|
|
100
|
+
* @returns список клавиш или undefined
|
|
101
|
+
*/
|
|
102
|
+
function getAnswerKeyShortcuts(shortcut, withEnter) {
|
|
103
|
+
return joinTokens([shortcut, withEnter ? "Enter" : null]);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Дан ли ответ этим элементом
|
|
107
|
+
* @param control - вариант или поле
|
|
108
|
+
* @returns true, если вариант выбран или в поле есть текст
|
|
109
|
+
*/
|
|
110
|
+
function isAnswered(control) {
|
|
111
|
+
if (control.type === "choice") return control.element.checked;
|
|
112
|
+
return control.element.hasAttribute("name") && hasText(control.element.value);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Пустое текстовое поле: из него стрелки переводят фокус к вариантам
|
|
116
|
+
* @param control - вариант или поле
|
|
117
|
+
* @returns true для пустого текстового поля
|
|
118
|
+
*/
|
|
119
|
+
function isEmptyTextInput(control) {
|
|
120
|
+
return control?.type === "input" && TEXT_INPUT_TYPES.includes(control.element.type) && !hasText(control.element.value);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Элемент ввода текста: в нём стрелки и буквы работают как обычно
|
|
124
|
+
* @param element - элемент
|
|
125
|
+
* @returns true для полей ввода текста
|
|
126
|
+
*/
|
|
127
|
+
function isTextEntry(element) {
|
|
128
|
+
if (element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) return true;
|
|
129
|
+
if (element instanceof HTMLInputElement) return !NON_TEXT_INPUT_TYPES.includes(element.type);
|
|
130
|
+
return element instanceof HTMLElement && element.isContentEditable;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Радиокнопка ли элемент
|
|
134
|
+
* @param element - элемент
|
|
135
|
+
* @returns true для input type=radio
|
|
136
|
+
*/
|
|
137
|
+
function isRadio(element) {
|
|
138
|
+
return element instanceof HTMLInputElement && element.type === "radio";
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Сравнить элементы по порядку в документе
|
|
142
|
+
* @param first - первый элемент
|
|
143
|
+
* @param second - второй элемент
|
|
144
|
+
* @returns -1, если первый раньше, 1 — если позже, 0 — если тот же
|
|
145
|
+
*/
|
|
146
|
+
function compareByDocumentPosition(first, second) {
|
|
147
|
+
if (first.element === second.element) return 0;
|
|
148
|
+
const position = first.element.compareDocumentPosition(second.element);
|
|
149
|
+
if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
|
|
150
|
+
if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Статус вопроса
|
|
155
|
+
* @param skipped - вопрос пропущен
|
|
156
|
+
* @param answered - есть ответ
|
|
157
|
+
* @returns статус
|
|
158
|
+
*/
|
|
159
|
+
function getItemStatus(skipped, answered) {
|
|
160
|
+
if (skipped) return "skipped";
|
|
161
|
+
if (answered) return "answered";
|
|
162
|
+
return "unanswered";
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Разобрать Root items
|
|
166
|
+
* @param items - описания вопросов
|
|
167
|
+
* @returns коллекция или null, если items не переданы
|
|
168
|
+
*/
|
|
169
|
+
function createItemCollection(items) {
|
|
170
|
+
if (items === void 0) return null;
|
|
171
|
+
const enabledItems = [];
|
|
172
|
+
const itemByName = /* @__PURE__ */ new Map();
|
|
173
|
+
for (const item of items) {
|
|
174
|
+
if (!item.disabled) enabledItems.push(item);
|
|
175
|
+
itemByName.set(item.name, item);
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
enabledItems,
|
|
179
|
+
itemByName,
|
|
180
|
+
items
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Начальный вопрос: defaultItem, если он включён, иначе первый включённый
|
|
185
|
+
* @param collection - описания вопросов
|
|
186
|
+
* @param defaultItem - желаемый вопрос
|
|
187
|
+
* @returns имя вопроса или null
|
|
188
|
+
*/
|
|
189
|
+
function resolveItemName(collection, defaultItem) {
|
|
190
|
+
if (!collection) return defaultItem ?? null;
|
|
191
|
+
const item = defaultItem ? collection.itemByName.get(defaultItem) : void 0;
|
|
192
|
+
if (item && !item.disabled) return item.name;
|
|
193
|
+
return collection.enabledItems[0]?.name ?? null;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Клавиши вариантов по значениям из Root items: отключённые варианты пропускаются
|
|
197
|
+
* @param definition - описание вопроса
|
|
198
|
+
* @param mode - буквы или цифры
|
|
199
|
+
* @returns клавиша по значению варианта
|
|
200
|
+
*/
|
|
201
|
+
function getChoiceShortcuts(definition, mode) {
|
|
202
|
+
const result = /* @__PURE__ */ new Map();
|
|
203
|
+
if (!definition || !mode) return result;
|
|
204
|
+
const keys = getShortcutKeys(mode);
|
|
205
|
+
let index = 0;
|
|
206
|
+
for (const choice of definition.choices ?? []) {
|
|
207
|
+
if (choice.disabled) continue;
|
|
208
|
+
const key = keys[index];
|
|
209
|
+
if (!key) break;
|
|
210
|
+
result.set(choice.value, key);
|
|
211
|
+
index += 1;
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Ошибки в самих Root items: повторы имён и значений, неверный defaultItem
|
|
217
|
+
* @param collection - описания вопросов
|
|
218
|
+
* @param defaultItem - начальный вопрос
|
|
219
|
+
* @returns тексты предупреждений
|
|
220
|
+
*/
|
|
221
|
+
function getDefinitionWarnings(collection, defaultItem) {
|
|
222
|
+
const result = [];
|
|
223
|
+
const names = /* @__PURE__ */ new Set();
|
|
224
|
+
for (const item of collection.items) {
|
|
225
|
+
if (names.has(item.name)) result.push(`вопрос «${item.name}» описан несколько раз`);
|
|
226
|
+
names.add(item.name);
|
|
227
|
+
const values = /* @__PURE__ */ new Set();
|
|
228
|
+
for (const choice of item.choices ?? []) {
|
|
229
|
+
if (values.has(choice.value)) result.push(`вариант «${choice.value}» описан несколько раз в вопросе «${item.name}»`);
|
|
230
|
+
values.add(choice.value);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (defaultItem) {
|
|
234
|
+
const item = collection.itemByName.get(defaultItem);
|
|
235
|
+
if (!item || item.disabled) result.push(`defaultItem «${defaultItem}» — не включённый вопрос, открыт первый включённый`);
|
|
236
|
+
}
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Расхождения Root items с отрисованными вопросами и вариантами
|
|
241
|
+
* @param collection - описания вопросов
|
|
242
|
+
* @param registeredItems - отрисованные вопросы
|
|
243
|
+
* @param shortcuts - подсказки клавиш
|
|
244
|
+
* @returns тексты предупреждений
|
|
245
|
+
*/
|
|
246
|
+
function getRenderWarnings(collection, registeredItems, shortcuts) {
|
|
247
|
+
const result = [];
|
|
248
|
+
const registeredByName = /* @__PURE__ */ new Map();
|
|
249
|
+
for (const item of registeredItems) registeredByName.set(item.name, item);
|
|
250
|
+
for (const definition of collection.items) {
|
|
251
|
+
const item = registeredByName.get(definition.name);
|
|
252
|
+
if (!item) {
|
|
253
|
+
if (!definition.disabled) result.push(`вопрос «${definition.name}» описан, но не отрисован`);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (item.disabled !== Boolean(definition.disabled)) result.push(`у вопроса «${definition.name}» разный disabled в items и QuestionnaireItem`);
|
|
257
|
+
if (item.required !== Boolean(definition.required)) result.push(`у вопроса «${definition.name}» разный required в items и QuestionnaireItem`);
|
|
258
|
+
const choices = definition.choices ?? [];
|
|
259
|
+
const definedValues = /* @__PURE__ */ new Set();
|
|
260
|
+
const renderedByValue = /* @__PURE__ */ new Map();
|
|
261
|
+
for (const choice of choices) definedValues.add(choice.value);
|
|
262
|
+
for (const choice of item.choices) renderedByValue.set(choice.value, choice);
|
|
263
|
+
for (const choice of choices) {
|
|
264
|
+
const rendered = renderedByValue.get(choice.value);
|
|
265
|
+
if (!rendered) {
|
|
266
|
+
result.push(`вариант «${choice.value}» вопроса «${definition.name}» описан, но не отрисован`);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (rendered.disabled !== Boolean(choice.disabled)) result.push(`у варианта «${choice.value}» вопроса «${definition.name}» разный disabled в items и QuestionnaireChoice`);
|
|
270
|
+
}
|
|
271
|
+
if (!shortcuts) continue;
|
|
272
|
+
const enabledDefined = [];
|
|
273
|
+
const enabledRendered = [];
|
|
274
|
+
for (const choice of item.choices) {
|
|
275
|
+
if (!definedValues.has(choice.value)) result.push(`вариант «${choice.value}» вопроса «${definition.name}» не описан в items и останется без клавиши`);
|
|
276
|
+
if (!choice.disabled) enabledRendered.push(choice.value);
|
|
277
|
+
}
|
|
278
|
+
for (const choice of choices) if (!choice.disabled) enabledDefined.push(choice.value);
|
|
279
|
+
const sameValues = enabledDefined.length > 0 && enabledDefined.length === enabledRendered.length && enabledDefined.every((value) => enabledRendered.includes(value));
|
|
280
|
+
const otherOrder = enabledDefined.some((value, index) => value !== enabledRendered[index]);
|
|
281
|
+
if (sameValues && otherOrder) result.push(`порядок вариантов вопроса «${definition.name}» в items и в разметке различается`);
|
|
282
|
+
}
|
|
283
|
+
for (const item of registeredItems) if (!item.disabled && !collection.itemByName.has(item.name)) result.push(`вопрос «${item.name}» отрисован, но не описан в items и не участвует в опросе`);
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Логика варианта ответа: регистрация в вопросе, выбор, клавиша
|
|
288
|
+
* @param parameters - параметры варианта
|
|
289
|
+
* @returns пропсы input и состояние варианта
|
|
290
|
+
*/
|
|
291
|
+
function useChoice({ checked, defaultChecked, disabled, onChange, value }) {
|
|
292
|
+
const item = useItemContext("QuestionnaireChoice");
|
|
293
|
+
const id = useId();
|
|
294
|
+
const [element, setElement] = useState(null);
|
|
295
|
+
const initialCheckedRef = useRef(defaultChecked);
|
|
296
|
+
const isControlled = checked !== void 0;
|
|
297
|
+
const isDisabled = item.disabled || disabled;
|
|
298
|
+
const isSelected = item.selectedAnswerIds.includes(id);
|
|
299
|
+
const isSkipped = item.status === "skipped";
|
|
300
|
+
let isChecked = isSelected;
|
|
301
|
+
if (isControlled) isChecked = isSkipped ? false : checked;
|
|
302
|
+
const type = item.multiple ? "checkbox" : "radio";
|
|
303
|
+
const shortcut = item.shortcutByChoiceValue?.get(value) ?? item.shortcutByAnswerId.get(id) ?? null;
|
|
304
|
+
const { registerAnswerControl, registerAnswerSelection, resetVersion, setAnswerDefault, syncControlledAnswerSelection } = item;
|
|
305
|
+
useLayoutEffect(() => registerAnswerSelection(id, initialCheckedRef.current), [id, registerAnswerSelection]);
|
|
306
|
+
useLayoutEffect(() => setAnswerDefault(id, defaultChecked), [
|
|
307
|
+
id,
|
|
308
|
+
defaultChecked,
|
|
309
|
+
setAnswerDefault
|
|
310
|
+
]);
|
|
311
|
+
useLayoutEffect(() => {
|
|
312
|
+
if (!element) return;
|
|
313
|
+
return registerAnswerControl({
|
|
314
|
+
type: "choice",
|
|
315
|
+
id,
|
|
316
|
+
element,
|
|
317
|
+
disabled: isDisabled,
|
|
318
|
+
ownDisabled: disabled,
|
|
319
|
+
value
|
|
320
|
+
});
|
|
321
|
+
}, [
|
|
322
|
+
id,
|
|
323
|
+
disabled,
|
|
324
|
+
isDisabled,
|
|
325
|
+
element,
|
|
326
|
+
registerAnswerControl,
|
|
327
|
+
value
|
|
328
|
+
]);
|
|
329
|
+
useLayoutEffect(() => {
|
|
330
|
+
if (isControlled) syncControlledAnswerSelection(id, checked);
|
|
331
|
+
}, [
|
|
332
|
+
id,
|
|
333
|
+
isControlled,
|
|
334
|
+
checked,
|
|
335
|
+
resetVersion,
|
|
336
|
+
syncControlledAnswerSelection
|
|
337
|
+
]);
|
|
338
|
+
useLayoutEffect(() => {
|
|
339
|
+
if (!element) return;
|
|
340
|
+
element.defaultChecked = isControlled ? checked : defaultChecked;
|
|
341
|
+
if (resetVersion > 0) element.checked = isChecked;
|
|
342
|
+
}, [
|
|
343
|
+
isChecked,
|
|
344
|
+
isControlled,
|
|
345
|
+
checked,
|
|
346
|
+
defaultChecked,
|
|
347
|
+
element,
|
|
348
|
+
resetVersion
|
|
349
|
+
]);
|
|
350
|
+
/**
|
|
351
|
+
* Вариант выбран или снят пользователем
|
|
352
|
+
* @param event - событие input
|
|
353
|
+
*/
|
|
354
|
+
const handleChange = (event) => {
|
|
355
|
+
onChange?.(event);
|
|
356
|
+
if (event.defaultPrevented) return;
|
|
357
|
+
if (!isControlled) {
|
|
358
|
+
item.setAnswerSelectionFromInteraction(id, event.target.checked);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (isSkipped && checked === event.target.checked) item.setAnswerSelectionFromInteraction(id, checked);
|
|
362
|
+
};
|
|
363
|
+
const name = isSkipped ? void 0 : item.name;
|
|
364
|
+
const invalid = item.invalid || void 0;
|
|
365
|
+
const keyShortcuts = getAnswerKeyShortcuts(shortcut, !isDisabled && isChecked);
|
|
366
|
+
const required = item.required && !item.multiple && !item.hasInputAnswer;
|
|
367
|
+
return {
|
|
368
|
+
inputProps: {
|
|
369
|
+
"ref": setElement,
|
|
370
|
+
"aria-invalid": invalid,
|
|
371
|
+
"aria-keyshortcuts": keyShortcuts,
|
|
372
|
+
"checked": isChecked,
|
|
373
|
+
"disabled": isDisabled,
|
|
374
|
+
"id": id,
|
|
375
|
+
"name": name,
|
|
376
|
+
"onChange": handleChange,
|
|
377
|
+
"required": required,
|
|
378
|
+
"type": type,
|
|
379
|
+
"value": value
|
|
380
|
+
},
|
|
381
|
+
state: {
|
|
382
|
+
checked: isChecked,
|
|
383
|
+
disabled: isDisabled,
|
|
384
|
+
invalid: item.invalid,
|
|
385
|
+
shortcut,
|
|
386
|
+
type
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Логика поля свободного ответа: регистрация в вопросе и учёт ответа
|
|
392
|
+
* @param parameters - параметры поля
|
|
393
|
+
* @returns ref, пропсы input и состояние поля
|
|
394
|
+
*/
|
|
395
|
+
function useInput({ defaultValue, disabled, onChange, type, value }) {
|
|
396
|
+
const item = useItemContext("QuestionnaireInput");
|
|
397
|
+
const id = useId();
|
|
398
|
+
const inputRef = useRef(null);
|
|
399
|
+
const initialFilledRef = useRef(hasText(defaultValue));
|
|
400
|
+
const isControlled = value !== void 0;
|
|
401
|
+
const isDefaultFilled = hasText(defaultValue);
|
|
402
|
+
const isValueFilled = hasText(value);
|
|
403
|
+
const [filled, setFilled] = useState(isDefaultFilled);
|
|
404
|
+
const isDisabled = item.disabled || disabled;
|
|
405
|
+
const isFilled = isControlled ? isValueFilled : filled;
|
|
406
|
+
const isSelected = item.selectedAnswerIds.includes(id);
|
|
407
|
+
const { registerAnswerControl, registerAnswerSelection, resetVersion, setAnswerDefault, syncControlledAnswerSelection } = item;
|
|
408
|
+
useLayoutEffect(() => registerAnswerSelection(id, initialFilledRef.current), [id, registerAnswerSelection]);
|
|
409
|
+
useLayoutEffect(() => setAnswerDefault(id, isDefaultFilled), [
|
|
410
|
+
isDefaultFilled,
|
|
411
|
+
id,
|
|
412
|
+
setAnswerDefault
|
|
413
|
+
]);
|
|
414
|
+
useLayoutEffect(() => {
|
|
415
|
+
const element = inputRef.current;
|
|
416
|
+
if (!element) return;
|
|
417
|
+
return registerAnswerControl({
|
|
418
|
+
type: "input",
|
|
419
|
+
id,
|
|
420
|
+
element,
|
|
421
|
+
disabled: isDisabled
|
|
422
|
+
});
|
|
423
|
+
}, [
|
|
424
|
+
isDisabled,
|
|
425
|
+
id,
|
|
426
|
+
registerAnswerControl
|
|
427
|
+
]);
|
|
428
|
+
useLayoutEffect(() => {
|
|
429
|
+
if (isControlled) {
|
|
430
|
+
syncControlledAnswerSelection(id, isValueFilled);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (resetVersion > 0) setFilled(isDefaultFilled);
|
|
434
|
+
}, [
|
|
435
|
+
isControlled,
|
|
436
|
+
isValueFilled,
|
|
437
|
+
value,
|
|
438
|
+
isDefaultFilled,
|
|
439
|
+
id,
|
|
440
|
+
resetVersion,
|
|
441
|
+
syncControlledAnswerSelection
|
|
442
|
+
]);
|
|
443
|
+
useLayoutEffect(() => {
|
|
444
|
+
const element = inputRef.current;
|
|
445
|
+
if (element && isControlled) element.defaultValue = String(value);
|
|
446
|
+
}, [isControlled, value]);
|
|
447
|
+
/**
|
|
448
|
+
* Изменился текст поля
|
|
449
|
+
* @param event - событие input
|
|
450
|
+
*/
|
|
451
|
+
const handleChange = (event) => {
|
|
452
|
+
onChange?.(event);
|
|
453
|
+
if (event.defaultPrevented) return;
|
|
454
|
+
const nextFilled = event.target.value.trim().length > 0;
|
|
455
|
+
if (!isControlled) {
|
|
456
|
+
setFilled(nextFilled);
|
|
457
|
+
item.setAnswerSelectionFromInteraction(id, nextFilled);
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
return {
|
|
461
|
+
inputRef,
|
|
462
|
+
inputProps: {
|
|
463
|
+
"aria-invalid": item.invalid || void 0,
|
|
464
|
+
"aria-keyshortcuts": getAnswerKeyShortcuts(null, !isDisabled && isFilled && isSelected),
|
|
465
|
+
"defaultValue": isControlled ? void 0 : defaultValue,
|
|
466
|
+
"disabled": isDisabled,
|
|
467
|
+
"form": isSelected ? void 0 : "",
|
|
468
|
+
"id": id,
|
|
469
|
+
"name": isSelected ? item.name : void 0,
|
|
470
|
+
"onChange": handleChange,
|
|
471
|
+
"type": type,
|
|
472
|
+
"value": isControlled ? value : void 0
|
|
473
|
+
},
|
|
474
|
+
state: {
|
|
475
|
+
disabled: isDisabled,
|
|
476
|
+
filled: isFilled,
|
|
477
|
+
invalid: item.invalid
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Ключ отрисованных вариантов: значения и отключённость по порядку
|
|
483
|
+
* @param controls - элементы ответа по порядку в разметке
|
|
484
|
+
* @returns JSON-список вариантов
|
|
485
|
+
*/
|
|
486
|
+
function getChoicesKey(controls) {
|
|
487
|
+
const choices = [];
|
|
488
|
+
for (const control of controls) if (control.type === "choice") choices.push({
|
|
489
|
+
disabled: control.ownDisabled,
|
|
490
|
+
value: control.value
|
|
491
|
+
});
|
|
492
|
+
return JSON.stringify(choices);
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Логика вопроса: ответы, статус, проверка, навигация стрелками и клавишами
|
|
496
|
+
* @param parameters - параметры вопроса
|
|
497
|
+
* @returns контекст, пропсы fieldset и состояние вопроса
|
|
498
|
+
*/
|
|
499
|
+
function useItem({ ariaDescribedBy, ariaKeyShortcuts, disabled, invalid, multiple, name, onStatusChange, required }) {
|
|
500
|
+
const { activeItemName, domVersion, first, itemDefinitionByName, last, nativeValidation, registerItem, shortcuts } = useRootContext("QuestionnaireItem");
|
|
501
|
+
const [itemElement, setItemElement] = useState(null);
|
|
502
|
+
const [answerControls, setAnswerControls] = useState([]);
|
|
503
|
+
const [attempted, setAttempted] = useState(false);
|
|
504
|
+
const [selectedAnswerIds, setSelectedAnswerIds] = useState([]);
|
|
505
|
+
const [skipped, setSkipped] = useState(false);
|
|
506
|
+
const [resetVersion, setResetVersion] = useState(0);
|
|
507
|
+
const [descriptionIds, setDescriptionIds] = useState([]);
|
|
508
|
+
const [errorIds, setErrorIds] = useState([]);
|
|
509
|
+
const defaultAnswerIdsRef = useRef([]);
|
|
510
|
+
const multipleRef = useRef(multiple);
|
|
511
|
+
const previousMultipleRef = useRef(multiple);
|
|
512
|
+
const itemApiRef = useRef(null);
|
|
513
|
+
multipleRef.current = multiple;
|
|
514
|
+
const active = !disabled && activeItemName === name;
|
|
515
|
+
const sortedControls = useMemo(() => [...answerControls].sort(compareByDocumentPosition), [answerControls, domVersion]);
|
|
516
|
+
const enabledControls = useMemo(() => sortedControls.filter((control) => !control.disabled), [sortedControls]);
|
|
517
|
+
const status = getItemStatus(skipped, enabledControls.some((control) => selectedAnswerIds.includes(control.id)));
|
|
518
|
+
const skippedOptional = status === "skipped" && !required;
|
|
519
|
+
const valid = disabled || skippedOptional || !invalid && status === "answered";
|
|
520
|
+
const showInvalid = !disabled && !skippedOptional && (invalid || attempted && !valid);
|
|
521
|
+
const hasInputAnswer = enabledControls.some((control) => control.type === "input");
|
|
522
|
+
const statusRef = useRef(status);
|
|
523
|
+
const definition = itemDefinitionByName?.get(name);
|
|
524
|
+
const shortcutByChoiceValue = useMemo(() => {
|
|
525
|
+
if (!itemDefinitionByName) return null;
|
|
526
|
+
return getChoiceShortcuts(definition, shortcuts);
|
|
527
|
+
}, [
|
|
528
|
+
definition,
|
|
529
|
+
itemDefinitionByName,
|
|
530
|
+
shortcuts
|
|
531
|
+
]);
|
|
532
|
+
const shortcutByAnswerId = useMemo(() => {
|
|
533
|
+
const result = /* @__PURE__ */ new Map();
|
|
534
|
+
if (shortcutByChoiceValue) return result;
|
|
535
|
+
const keys = getShortcutKeys(shortcuts);
|
|
536
|
+
for (const control of enabledControls) {
|
|
537
|
+
const key = keys[result.size];
|
|
538
|
+
if (!key) break;
|
|
539
|
+
if (control.type === "choice") result.set(control.id, key);
|
|
540
|
+
}
|
|
541
|
+
return result;
|
|
542
|
+
}, [
|
|
543
|
+
enabledControls,
|
|
544
|
+
shortcutByChoiceValue,
|
|
545
|
+
shortcuts
|
|
546
|
+
]);
|
|
547
|
+
const registerAnswerControl = useCallback((control) => {
|
|
548
|
+
setAnswerControls((previous) => {
|
|
549
|
+
const result = [];
|
|
550
|
+
for (const current of previous) if (current.element !== control.element && current.id !== control.id) result.push(current);
|
|
551
|
+
result.push(control);
|
|
552
|
+
return result;
|
|
553
|
+
});
|
|
554
|
+
return () => {
|
|
555
|
+
setAnswerControls((previous) => previous.filter((current) => current !== control));
|
|
556
|
+
};
|
|
557
|
+
}, []);
|
|
558
|
+
useLayoutEffect(() => {
|
|
559
|
+
if (statusRef.current !== status) {
|
|
560
|
+
statusRef.current = status;
|
|
561
|
+
onStatusChange?.(status);
|
|
562
|
+
}
|
|
563
|
+
}, [onStatusChange, status]);
|
|
564
|
+
const updateSelection = useCallback((id, selected) => {
|
|
565
|
+
setSelectedAnswerIds((previous) => {
|
|
566
|
+
if (!selected) return previous.filter((current) => current !== id);
|
|
567
|
+
if (!multiple) return [id];
|
|
568
|
+
if (previous.includes(id)) return previous;
|
|
569
|
+
return [...previous, id];
|
|
570
|
+
});
|
|
571
|
+
}, [multiple]);
|
|
572
|
+
const setAnswerSelectionFromInteraction = useCallback((id, selected) => {
|
|
573
|
+
setSkipped(false);
|
|
574
|
+
updateSelection(id, selected);
|
|
575
|
+
}, [updateSelection]);
|
|
576
|
+
const syncControlledAnswerSelection = useCallback((id, selected) => {
|
|
577
|
+
if (selected) setSkipped(false);
|
|
578
|
+
updateSelection(id, selected);
|
|
579
|
+
}, [updateSelection]);
|
|
580
|
+
const registerAnswerSelection = useCallback((id, selected) => {
|
|
581
|
+
if (selected) {
|
|
582
|
+
defaultAnswerIdsRef.current = [...defaultAnswerIdsRef.current.filter((current) => current !== id), id];
|
|
583
|
+
setSelectedAnswerIds((previous) => {
|
|
584
|
+
if (multipleRef.current) {
|
|
585
|
+
if (previous.includes(id)) return previous;
|
|
586
|
+
return [...previous, id];
|
|
587
|
+
}
|
|
588
|
+
if (previous.length) return previous;
|
|
589
|
+
return [id];
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
return () => {
|
|
593
|
+
defaultAnswerIdsRef.current = defaultAnswerIdsRef.current.filter((current) => current !== id);
|
|
594
|
+
setSelectedAnswerIds((previous) => previous.filter((current) => current !== id));
|
|
595
|
+
};
|
|
596
|
+
}, []);
|
|
597
|
+
const setAnswerDefault = useCallback((id, selected) => {
|
|
598
|
+
const defaults = defaultAnswerIdsRef.current;
|
|
599
|
+
if (selected) {
|
|
600
|
+
if (!defaults.includes(id)) defaultAnswerIdsRef.current = [...defaults, id];
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
defaultAnswerIdsRef.current = defaults.filter((current) => current !== id);
|
|
604
|
+
}, []);
|
|
605
|
+
const registerDescription = useCallback((id) => {
|
|
606
|
+
setDescriptionIds((previous) => {
|
|
607
|
+
if (previous.includes(id)) return previous;
|
|
608
|
+
return [...previous, id];
|
|
609
|
+
});
|
|
610
|
+
return () => {
|
|
611
|
+
setDescriptionIds((previous) => previous.filter((current) => current !== id));
|
|
612
|
+
};
|
|
613
|
+
}, []);
|
|
614
|
+
const registerError = useCallback((id) => {
|
|
615
|
+
setErrorIds((previous) => {
|
|
616
|
+
if (previous.includes(id)) return previous;
|
|
617
|
+
return [...previous, id];
|
|
618
|
+
});
|
|
619
|
+
return () => {
|
|
620
|
+
setErrorIds((previous) => previous.filter((current) => current !== id));
|
|
621
|
+
};
|
|
622
|
+
}, []);
|
|
623
|
+
const validate = useCallback(() => {
|
|
624
|
+
setAttempted(true);
|
|
625
|
+
if (!valid) return false;
|
|
626
|
+
if (!nativeValidation) return true;
|
|
627
|
+
const invalidControl = enabledControls.find((control) => isAnswered(control) && control.element.willValidate && !control.element.validity.valid);
|
|
628
|
+
if (invalidControl) {
|
|
629
|
+
invalidControl.element.focus();
|
|
630
|
+
invalidControl.element.reportValidity();
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
return true;
|
|
634
|
+
}, [
|
|
635
|
+
enabledControls,
|
|
636
|
+
nativeValidation,
|
|
637
|
+
valid
|
|
638
|
+
]);
|
|
639
|
+
const focus = useCallback(() => {
|
|
640
|
+
itemElement?.focus();
|
|
641
|
+
}, [itemElement]);
|
|
642
|
+
const focusInvalid = useCallback(() => {
|
|
643
|
+
const filledInput = itemElement?.querySelector("input[data-filled][name]:not(:disabled)");
|
|
644
|
+
const firstField = itemElement?.querySelector("input:not([type=hidden]):not(:disabled), textarea:not(:disabled)");
|
|
645
|
+
(filledInput ?? firstField ?? itemElement)?.focus();
|
|
646
|
+
}, [itemElement]);
|
|
647
|
+
const reset = useCallback(() => {
|
|
648
|
+
const defaults = defaultAnswerIdsRef.current;
|
|
649
|
+
const nextSelection = multiple ? [...defaults] : defaults.slice(0, 1);
|
|
650
|
+
setAttempted(false);
|
|
651
|
+
setSkipped(false);
|
|
652
|
+
setSelectedAnswerIds(nextSelection);
|
|
653
|
+
setResetVersion((previous) => previous + 1);
|
|
654
|
+
}, [multiple]);
|
|
655
|
+
const skip = useCallback(() => {
|
|
656
|
+
if (required) return;
|
|
657
|
+
setSelectedAnswerIds([]);
|
|
658
|
+
setSkipped(true);
|
|
659
|
+
}, [required]);
|
|
660
|
+
useLayoutEffect(() => {
|
|
661
|
+
const wasMultiple = previousMultipleRef.current;
|
|
662
|
+
previousMultipleRef.current = multiple;
|
|
663
|
+
if (!wasMultiple || multiple) return;
|
|
664
|
+
setSelectedAnswerIds((previous) => {
|
|
665
|
+
const firstSelected = enabledControls.find((control) => previous.includes(control.id));
|
|
666
|
+
if (firstSelected) return [firstSelected.id];
|
|
667
|
+
return [];
|
|
668
|
+
});
|
|
669
|
+
}, [enabledControls, multiple]);
|
|
670
|
+
const getAnswerByElement = useCallback((element) => enabledControls.find((control) => control.element === element) ?? null, [enabledControls]);
|
|
671
|
+
const getAnswerByShortcut = useCallback((shortcut) => {
|
|
672
|
+
if (shortcutByChoiceValue) {
|
|
673
|
+
let choiceValue;
|
|
674
|
+
for (const [value, key] of shortcutByChoiceValue) if (key === shortcut) {
|
|
675
|
+
choiceValue = value;
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
return enabledControls.find((control) => control.type === "choice" && control.value === choiceValue) ?? null;
|
|
679
|
+
}
|
|
680
|
+
let answerId;
|
|
681
|
+
for (const [id, key] of shortcutByAnswerId) if (key === shortcut) {
|
|
682
|
+
answerId = id;
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
return enabledControls.find((control) => control.id === answerId) ?? null;
|
|
686
|
+
}, [
|
|
687
|
+
enabledControls,
|
|
688
|
+
shortcutByAnswerId,
|
|
689
|
+
shortcutByChoiceValue
|
|
690
|
+
]);
|
|
691
|
+
const moveAnswerFocus = useCallback((target, direction) => {
|
|
692
|
+
const index = enabledControls.findIndex((control) => control.element === target);
|
|
693
|
+
const current = enabledControls[index] ?? null;
|
|
694
|
+
const count = enabledControls.length;
|
|
695
|
+
if (!count || isTextEntry(target) && !isEmptyTextInput(current) || index < 0 && target !== itemElement) return false;
|
|
696
|
+
let nextControl;
|
|
697
|
+
if (index < 0) {
|
|
698
|
+
const edgeControl = direction === "next" ? enabledControls[0] : enabledControls[count - 1];
|
|
699
|
+
nextControl = enabledControls.find(isAnswered) ?? edgeControl;
|
|
700
|
+
} else nextControl = enabledControls[(index + (direction === "next" ? 1 : -1) + count) % count];
|
|
701
|
+
if (!nextControl || nextControl.element === target || index >= 0 && isRadio(target) && isRadio(nextControl.element)) return false;
|
|
702
|
+
nextControl.element.focus();
|
|
703
|
+
if (nextControl.type === "choice" && isRadio(nextControl.element)) nextControl.element.click();
|
|
704
|
+
return true;
|
|
705
|
+
}, [enabledControls, itemElement]);
|
|
706
|
+
const choicesKey = getChoicesKey(sortedControls);
|
|
707
|
+
const choices = useMemo(() => JSON.parse(choicesKey), [choicesKey]);
|
|
708
|
+
useLayoutEffect(() => {
|
|
709
|
+
itemApiRef.current = {
|
|
710
|
+
focus,
|
|
711
|
+
focusInvalid,
|
|
712
|
+
getAnswerByElement,
|
|
713
|
+
getAnswerByShortcut,
|
|
714
|
+
moveAnswerFocus,
|
|
715
|
+
reset,
|
|
716
|
+
skip,
|
|
717
|
+
validate
|
|
718
|
+
};
|
|
719
|
+
});
|
|
720
|
+
useLayoutEffect(() => {
|
|
721
|
+
if (!itemElement) return;
|
|
722
|
+
return registerItem({
|
|
723
|
+
choices,
|
|
724
|
+
disabled,
|
|
725
|
+
element: itemElement,
|
|
726
|
+
focus: () => itemApiRef.current.focus(),
|
|
727
|
+
focusInvalid: () => itemApiRef.current.focusInvalid(),
|
|
728
|
+
getAnswerByElement: (element) => itemApiRef.current.getAnswerByElement(element),
|
|
729
|
+
getAnswerByShortcut: (shortcut) => itemApiRef.current.getAnswerByShortcut(shortcut),
|
|
730
|
+
moveAnswerFocus: (target, direction) => itemApiRef.current.moveAnswerFocus(target, direction),
|
|
731
|
+
name,
|
|
732
|
+
required,
|
|
733
|
+
reset: () => itemApiRef.current.reset(),
|
|
734
|
+
skip: () => itemApiRef.current.skip(),
|
|
735
|
+
status,
|
|
736
|
+
validate: () => itemApiRef.current.validate()
|
|
737
|
+
});
|
|
738
|
+
}, [
|
|
739
|
+
choices,
|
|
740
|
+
disabled,
|
|
741
|
+
itemElement,
|
|
742
|
+
name,
|
|
743
|
+
registerItem,
|
|
744
|
+
required,
|
|
745
|
+
status
|
|
746
|
+
]);
|
|
747
|
+
const context = useMemo(() => ({
|
|
748
|
+
active,
|
|
749
|
+
disabled,
|
|
750
|
+
hasInputAnswer,
|
|
751
|
+
invalid: showInvalid,
|
|
752
|
+
multiple,
|
|
753
|
+
name,
|
|
754
|
+
registerAnswerControl,
|
|
755
|
+
registerAnswerSelection,
|
|
756
|
+
registerDescription,
|
|
757
|
+
registerError,
|
|
758
|
+
required,
|
|
759
|
+
resetVersion,
|
|
760
|
+
selectedAnswerIds,
|
|
761
|
+
setAnswerDefault,
|
|
762
|
+
setAnswerSelectionFromInteraction,
|
|
763
|
+
shortcutByAnswerId,
|
|
764
|
+
shortcutByChoiceValue,
|
|
765
|
+
shortcuts,
|
|
766
|
+
status,
|
|
767
|
+
syncControlledAnswerSelection
|
|
768
|
+
}), [
|
|
769
|
+
active,
|
|
770
|
+
disabled,
|
|
771
|
+
hasInputAnswer,
|
|
772
|
+
showInvalid,
|
|
773
|
+
multiple,
|
|
774
|
+
name,
|
|
775
|
+
registerAnswerControl,
|
|
776
|
+
registerAnswerSelection,
|
|
777
|
+
registerDescription,
|
|
778
|
+
registerError,
|
|
779
|
+
required,
|
|
780
|
+
resetVersion,
|
|
781
|
+
selectedAnswerIds,
|
|
782
|
+
setAnswerDefault,
|
|
783
|
+
setAnswerSelectionFromInteraction,
|
|
784
|
+
shortcutByAnswerId,
|
|
785
|
+
shortcutByChoiceValue,
|
|
786
|
+
shortcuts,
|
|
787
|
+
status,
|
|
788
|
+
syncControlledAnswerSelection
|
|
789
|
+
]);
|
|
790
|
+
const describedByIds = [...descriptionIds];
|
|
791
|
+
if (showInvalid) describedByIds.push(...errorIds);
|
|
792
|
+
describedByIds.push(ariaDescribedBy ?? "");
|
|
793
|
+
const keyShortcuts = [ariaKeyShortcuts];
|
|
794
|
+
if (active) {
|
|
795
|
+
keyShortcuts.push("Meta+Enter Control+Enter");
|
|
796
|
+
if (enabledControls.length) keyShortcuts.push("ArrowUp ArrowDown");
|
|
797
|
+
if (!first) keyShortcuts.push("ArrowLeft");
|
|
798
|
+
if (!last && status !== "unanswered") keyShortcuts.push("ArrowRight");
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
context,
|
|
802
|
+
setItemElement,
|
|
803
|
+
itemProps: {
|
|
804
|
+
"aria-describedby": joinTokens(describedByIds),
|
|
805
|
+
"aria-invalid": showInvalid || void 0,
|
|
806
|
+
"aria-keyshortcuts": joinTokens(keyShortcuts),
|
|
807
|
+
"disabled": disabled,
|
|
808
|
+
"hidden": !active,
|
|
809
|
+
"inert": !active,
|
|
810
|
+
"tabIndex": -1
|
|
811
|
+
},
|
|
812
|
+
state: {
|
|
813
|
+
active,
|
|
814
|
+
disabled,
|
|
815
|
+
invalid: showInvalid,
|
|
816
|
+
multiple,
|
|
817
|
+
required,
|
|
818
|
+
status
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Логика опроса: открытый вопрос, переходы, проверка, отправка и сброс, клавиатура
|
|
824
|
+
* @param parameters - параметры опроса
|
|
825
|
+
* @returns контекст, пропсы формы и состояние
|
|
826
|
+
*/
|
|
827
|
+
function useRoot({ defaultItem, item, items, noValidate, onItemChange, onReset, onSubmit, shortcuts }) {
|
|
828
|
+
const collection = useMemo(() => createItemCollection(items), [items]);
|
|
829
|
+
const [registeredItems, setRegisteredItems] = useState([]);
|
|
830
|
+
const [uncontrolledItem, setUncontrolledItem] = useState(() => resolveItemName(collection, defaultItem));
|
|
831
|
+
const [formElement, setFormElement] = useState(null);
|
|
832
|
+
const [domVersion, setDomVersion] = useState(0);
|
|
833
|
+
const pendingFocusRef = useRef(null);
|
|
834
|
+
const isControlled = item !== void 0;
|
|
835
|
+
const activeItemName = isControlled ? item : uncontrolledItem;
|
|
836
|
+
const previousItemRef = useRef(activeItemName);
|
|
837
|
+
const nativeValidation = !noValidate;
|
|
838
|
+
const shortcutMode = shortcuts ?? null;
|
|
839
|
+
const warningsRef = useRef(/* @__PURE__ */ new Set());
|
|
840
|
+
useLayoutEffect(() => {
|
|
841
|
+
if (!collection || !formElement) {
|
|
842
|
+
warningsRef.current.clear();
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
let cancelled = false;
|
|
846
|
+
queueMicrotask(() => {
|
|
847
|
+
if (cancelled) return;
|
|
848
|
+
const warnings = /* @__PURE__ */ new Set([...getDefinitionWarnings(collection, defaultItem), ...getRenderWarnings(collection, registeredItems, shortcutMode)]);
|
|
849
|
+
for (const warning of warnings) if (!warningsRef.current.has(warning)) console.warn(`[Questionnaire] ${warning}`);
|
|
850
|
+
warningsRef.current = warnings;
|
|
851
|
+
});
|
|
852
|
+
return () => {
|
|
853
|
+
cancelled = true;
|
|
854
|
+
};
|
|
855
|
+
}, [
|
|
856
|
+
collection,
|
|
857
|
+
defaultItem,
|
|
858
|
+
registeredItems,
|
|
859
|
+
formElement,
|
|
860
|
+
shortcutMode
|
|
861
|
+
]);
|
|
862
|
+
useLayoutEffect(() => {
|
|
863
|
+
if (!formElement || typeof MutationObserver === "undefined") return;
|
|
864
|
+
const observer = new MutationObserver(() => {
|
|
865
|
+
setDomVersion((previous) => previous + 1);
|
|
866
|
+
});
|
|
867
|
+
observer.observe(formElement, {
|
|
868
|
+
childList: true,
|
|
869
|
+
subtree: true
|
|
870
|
+
});
|
|
871
|
+
return () => observer.disconnect();
|
|
872
|
+
}, [formElement]);
|
|
873
|
+
const enabledRegistered = useMemo(() => {
|
|
874
|
+
const result = [];
|
|
875
|
+
for (const registered of registeredItems) if (!registered.disabled) result.push(registered);
|
|
876
|
+
return result.sort(compareByDocumentPosition);
|
|
877
|
+
}, [domVersion, registeredItems]);
|
|
878
|
+
const registeredByName = useMemo(() => {
|
|
879
|
+
const result = /* @__PURE__ */ new Map();
|
|
880
|
+
for (const registered of enabledRegistered) result.set(registered.name, registered);
|
|
881
|
+
return result;
|
|
882
|
+
}, [enabledRegistered]);
|
|
883
|
+
const orderedItems = collection?.enabledItems ?? enabledRegistered;
|
|
884
|
+
const activeIndex = orderedItems.findIndex((current) => current.name === activeItemName);
|
|
885
|
+
const activeItem = activeIndex < 0 || !activeItemName ? null : registeredByName.get(activeItemName) ?? null;
|
|
886
|
+
const activeDefinition = activeItemName ? collection?.itemByName.get(activeItemName) : void 0;
|
|
887
|
+
let activeItemRequired = null;
|
|
888
|
+
let activeItemStatus = null;
|
|
889
|
+
if (activeIndex >= 0) {
|
|
890
|
+
activeItemRequired = activeDefinition ? Boolean(activeDefinition.required) : activeItem?.required ?? false;
|
|
891
|
+
const fallbackStatus = activeItemName ? "unanswered" : null;
|
|
892
|
+
activeItemStatus = activeItem?.status ?? fallbackStatus;
|
|
893
|
+
}
|
|
894
|
+
const validationItems = useMemo(() => {
|
|
895
|
+
if (!collection) return enabledRegistered;
|
|
896
|
+
const result = [];
|
|
897
|
+
for (const definition of collection.enabledItems) {
|
|
898
|
+
const registered = registeredByName.get(definition.name);
|
|
899
|
+
if (registered) result.push(registered);
|
|
900
|
+
}
|
|
901
|
+
return result;
|
|
902
|
+
}, [
|
|
903
|
+
collection,
|
|
904
|
+
registeredByName,
|
|
905
|
+
enabledRegistered
|
|
906
|
+
]);
|
|
907
|
+
const total = orderedItems.length;
|
|
908
|
+
const current = activeIndex < 0 ? 0 : activeIndex + 1;
|
|
909
|
+
const first = total > 0 && activeIndex === 0;
|
|
910
|
+
const last = total > 0 && activeIndex === total - 1;
|
|
911
|
+
const openItem = useCallback((name, target = "item") => {
|
|
912
|
+
if (name === activeItemName) return;
|
|
913
|
+
pendingFocusRef.current = {
|
|
914
|
+
name,
|
|
915
|
+
target
|
|
916
|
+
};
|
|
917
|
+
if (!isControlled) setUncontrolledItem(name);
|
|
918
|
+
onItemChange?.(name);
|
|
919
|
+
}, [
|
|
920
|
+
activeItemName,
|
|
921
|
+
isControlled,
|
|
922
|
+
onItemChange
|
|
923
|
+
]);
|
|
924
|
+
const openItemAt = useCallback((index) => {
|
|
925
|
+
const target = orderedItems[index];
|
|
926
|
+
if (target) openItem(target.name);
|
|
927
|
+
}, [orderedItems, openItem]);
|
|
928
|
+
useLayoutEffect(() => {
|
|
929
|
+
if (total === 0) return;
|
|
930
|
+
if (activeIndex < 0) {
|
|
931
|
+
const firstName = orderedItems[0]?.name ?? "";
|
|
932
|
+
if (!isControlled && activeItemName === null) {
|
|
933
|
+
setUncontrolledItem(firstName);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
openItem(firstName);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const pending = pendingFocusRef.current;
|
|
940
|
+
const changed = previousItemRef.current !== activeItemName;
|
|
941
|
+
previousItemRef.current = activeItemName;
|
|
942
|
+
if (!pending || pending.name !== activeItemName) {
|
|
943
|
+
if (isControlled && changed) {
|
|
944
|
+
pendingFocusRef.current = null;
|
|
945
|
+
activeItem?.focus();
|
|
946
|
+
}
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
if (pending.target === "invalid") activeItem?.focusInvalid();
|
|
950
|
+
else activeItem?.focus();
|
|
951
|
+
pendingFocusRef.current = null;
|
|
952
|
+
}, [
|
|
953
|
+
activeItem,
|
|
954
|
+
activeItemName,
|
|
955
|
+
isControlled,
|
|
956
|
+
activeIndex,
|
|
957
|
+
orderedItems,
|
|
958
|
+
openItem,
|
|
959
|
+
total
|
|
960
|
+
]);
|
|
961
|
+
const registerItem = useCallback((registered) => {
|
|
962
|
+
setRegisteredItems((previous) => {
|
|
963
|
+
const result = [];
|
|
964
|
+
for (const current of previous) if (current.element !== registered.element && current.name !== registered.name) result.push(current);
|
|
965
|
+
result.push(registered);
|
|
966
|
+
return result;
|
|
967
|
+
});
|
|
968
|
+
return () => {
|
|
969
|
+
setRegisteredItems((previous) => previous.filter((current) => current !== registered));
|
|
970
|
+
};
|
|
971
|
+
}, []);
|
|
972
|
+
const goPrevious = useCallback(() => {
|
|
973
|
+
if (activeIndex > 0) openItemAt(activeIndex - 1);
|
|
974
|
+
}, [activeIndex, openItemAt]);
|
|
975
|
+
const goNext = useCallback(() => {
|
|
976
|
+
if (!activeItem || activeIndex >= total - 1) return;
|
|
977
|
+
if (!activeItem.validate()) {
|
|
978
|
+
activeItem.focusInvalid();
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
openItemAt(activeIndex + 1);
|
|
982
|
+
}, [
|
|
983
|
+
activeItem,
|
|
984
|
+
activeIndex,
|
|
985
|
+
openItemAt,
|
|
986
|
+
total
|
|
987
|
+
]);
|
|
988
|
+
const advance = useCallback(() => {
|
|
989
|
+
if (!activeItem) return;
|
|
990
|
+
if (!activeItem.validate()) {
|
|
991
|
+
activeItem.focusInvalid();
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
if (last) {
|
|
995
|
+
formElement?.requestSubmit();
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
openItemAt(activeIndex + 1);
|
|
999
|
+
}, [
|
|
1000
|
+
activeItem,
|
|
1001
|
+
activeIndex,
|
|
1002
|
+
last,
|
|
1003
|
+
formElement,
|
|
1004
|
+
openItemAt
|
|
1005
|
+
]);
|
|
1006
|
+
const skipCurrent = useCallback(() => {
|
|
1007
|
+
if (!activeItem || activeItem.required) return;
|
|
1008
|
+
activeItem.skip();
|
|
1009
|
+
if (!last) {
|
|
1010
|
+
openItemAt(activeIndex + 1);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
queueMicrotask(() => {
|
|
1014
|
+
formElement?.requestSubmit();
|
|
1015
|
+
});
|
|
1016
|
+
}, [
|
|
1017
|
+
activeItem,
|
|
1018
|
+
activeIndex,
|
|
1019
|
+
last,
|
|
1020
|
+
formElement,
|
|
1021
|
+
openItemAt
|
|
1022
|
+
]);
|
|
1023
|
+
/**
|
|
1024
|
+
* Сброс формы: ответы к начальным, открыть начальный вопрос
|
|
1025
|
+
* @param event - событие reset
|
|
1026
|
+
*/
|
|
1027
|
+
const handleReset = (event) => {
|
|
1028
|
+
onReset?.(event);
|
|
1029
|
+
if (event.defaultPrevented) return;
|
|
1030
|
+
for (const registered of registeredItems) registered.reset();
|
|
1031
|
+
let resetName;
|
|
1032
|
+
if (collection) resetName = resolveItemName(collection, defaultItem);
|
|
1033
|
+
else resetName = enabledRegistered.find((registered) => registered.name === defaultItem)?.name ?? enabledRegistered[0]?.name;
|
|
1034
|
+
if (resetName) openItem(resetName);
|
|
1035
|
+
};
|
|
1036
|
+
/**
|
|
1037
|
+
* Отправка формы: открыть первый вопрос с ошибкой или передать событие дальше
|
|
1038
|
+
* @param event - событие submit
|
|
1039
|
+
*/
|
|
1040
|
+
const handleSubmit = (event) => {
|
|
1041
|
+
const invalidItem = validationItems.find((registered) => !registered.validate());
|
|
1042
|
+
if (invalidItem) {
|
|
1043
|
+
event.preventDefault();
|
|
1044
|
+
openItem(invalidItem.name, "invalid");
|
|
1045
|
+
if (invalidItem.name === activeItemName) {
|
|
1046
|
+
invalidItem.focusInvalid();
|
|
1047
|
+
pendingFocusRef.current = null;
|
|
1048
|
+
}
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
onSubmit?.(event);
|
|
1052
|
+
};
|
|
1053
|
+
/**
|
|
1054
|
+
* Клавиатура: Ctrl/Cmd+Enter и Enter — дальше, стрелки — ответы и вопросы, буквы/цифры — варианты
|
|
1055
|
+
* @param event - событие клавиатуры
|
|
1056
|
+
*/
|
|
1057
|
+
const handleKeyDown = (event) => {
|
|
1058
|
+
const { key, target } = event;
|
|
1059
|
+
if (event.defaultPrevented || event.nativeEvent.isComposing || event.keyCode === 229) return;
|
|
1060
|
+
if (!activeItem || !(target instanceof Element)) return;
|
|
1061
|
+
if (key === "Enter" && (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey) {
|
|
1062
|
+
event.preventDefault();
|
|
1063
|
+
if (!event.repeat) advance();
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
1067
|
+
if (key === "ArrowUp" || key === "ArrowDown") {
|
|
1068
|
+
const direction = key === "ArrowDown" ? "next" : "previous";
|
|
1069
|
+
if (activeItem.moveAnswerFocus(target, direction)) {
|
|
1070
|
+
event.preventDefault();
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if ((key === "ArrowLeft" || key === "ArrowRight") && !isTextEntry(target) && !isRadio(target)) {
|
|
1075
|
+
event.preventDefault();
|
|
1076
|
+
if (event.repeat) return;
|
|
1077
|
+
if (key === "ArrowLeft") goPrevious();
|
|
1078
|
+
else if (activeItem.status !== "unanswered") goNext();
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
if (key === "Enter") {
|
|
1082
|
+
const answer = activeItem.getAnswerByElement(target);
|
|
1083
|
+
if (!answer) return;
|
|
1084
|
+
event.preventDefault();
|
|
1085
|
+
if (!event.repeat && isAnswered(answer)) advance();
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
if (!shortcutMode || isTextEntry(target)) return;
|
|
1089
|
+
const shortcut = getShortcutByKey(key, shortcutMode);
|
|
1090
|
+
const answer = shortcut ? activeItem.getAnswerByShortcut(shortcut) : null;
|
|
1091
|
+
if (!answer) return;
|
|
1092
|
+
event.preventDefault();
|
|
1093
|
+
if (event.repeat) return;
|
|
1094
|
+
answer.element.focus();
|
|
1095
|
+
if (answer.type === "choice") answer.element.click();
|
|
1096
|
+
};
|
|
1097
|
+
const state = {
|
|
1098
|
+
current,
|
|
1099
|
+
first,
|
|
1100
|
+
last,
|
|
1101
|
+
total
|
|
1102
|
+
};
|
|
1103
|
+
const itemDefinitionByName = collection?.itemByName ?? null;
|
|
1104
|
+
return {
|
|
1105
|
+
context: useMemo(() => ({
|
|
1106
|
+
current,
|
|
1107
|
+
first,
|
|
1108
|
+
last,
|
|
1109
|
+
total,
|
|
1110
|
+
activeItem,
|
|
1111
|
+
activeItemName,
|
|
1112
|
+
activeItemRequired,
|
|
1113
|
+
activeItemStatus,
|
|
1114
|
+
domVersion,
|
|
1115
|
+
goNext,
|
|
1116
|
+
goPrevious,
|
|
1117
|
+
itemDefinitionByName,
|
|
1118
|
+
nativeValidation,
|
|
1119
|
+
registerItem,
|
|
1120
|
+
shortcuts: shortcutMode,
|
|
1121
|
+
skipCurrent
|
|
1122
|
+
}), [
|
|
1123
|
+
activeItem,
|
|
1124
|
+
activeItemName,
|
|
1125
|
+
activeItemRequired,
|
|
1126
|
+
activeItemStatus,
|
|
1127
|
+
itemDefinitionByName,
|
|
1128
|
+
current,
|
|
1129
|
+
domVersion,
|
|
1130
|
+
first,
|
|
1131
|
+
goNext,
|
|
1132
|
+
goPrevious,
|
|
1133
|
+
last,
|
|
1134
|
+
nativeValidation,
|
|
1135
|
+
registerItem,
|
|
1136
|
+
shortcutMode,
|
|
1137
|
+
skipCurrent,
|
|
1138
|
+
total
|
|
1139
|
+
]),
|
|
1140
|
+
setFormElement,
|
|
1141
|
+
rootProps: {
|
|
1142
|
+
"data-shortcuts": shortcutMode ?? void 0,
|
|
1143
|
+
"onKeyDown": handleKeyDown,
|
|
1144
|
+
"onReset": handleReset,
|
|
1145
|
+
"onSubmit": handleSubmit
|
|
1146
|
+
},
|
|
1147
|
+
state
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Корень опроса: форма
|
|
1152
|
+
* @param props - пропсы корня
|
|
1153
|
+
* @returns форма с контекстом опроса
|
|
1154
|
+
*/
|
|
1155
|
+
function Root({ defaultItem, item, items, noValidate = true, onItemChange, onReset, onSubmit, ref, shortcuts, ...props }) {
|
|
1156
|
+
const { context, setFormElement, rootProps, state } = useRoot({
|
|
1157
|
+
defaultItem,
|
|
1158
|
+
item,
|
|
1159
|
+
items,
|
|
1160
|
+
noValidate,
|
|
1161
|
+
onItemChange,
|
|
1162
|
+
onReset,
|
|
1163
|
+
onSubmit,
|
|
1164
|
+
shortcuts
|
|
1165
|
+
});
|
|
1166
|
+
const element = useRender({
|
|
1167
|
+
defaultTagName: "form",
|
|
1168
|
+
ref: [setFormElement, ref ?? null],
|
|
1169
|
+
state,
|
|
1170
|
+
props: mergeProps({
|
|
1171
|
+
...rootProps,
|
|
1172
|
+
noValidate
|
|
1173
|
+
}, props)
|
|
1174
|
+
});
|
|
1175
|
+
return createElement(RootContext, { value: context }, element);
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Прогресс опроса: role=progressbar с номером вопроса
|
|
1179
|
+
* @param props - пропсы прогресса
|
|
1180
|
+
* @returns индикатор прогресса
|
|
1181
|
+
*/
|
|
1182
|
+
function Progress({ render, ref, ...props }) {
|
|
1183
|
+
const { current, first, last, total } = useRootContext("QuestionnaireProgress");
|
|
1184
|
+
const hasItems = total > 0;
|
|
1185
|
+
return useRender({
|
|
1186
|
+
defaultTagName: "div",
|
|
1187
|
+
render,
|
|
1188
|
+
ref,
|
|
1189
|
+
state: {
|
|
1190
|
+
current,
|
|
1191
|
+
first,
|
|
1192
|
+
last,
|
|
1193
|
+
total
|
|
1194
|
+
},
|
|
1195
|
+
props: mergeProps({
|
|
1196
|
+
"aria-live": "polite",
|
|
1197
|
+
"aria-valuemax": hasItems ? total : void 0,
|
|
1198
|
+
"aria-valuemin": hasItems ? 1 : void 0,
|
|
1199
|
+
"aria-valuenow": hasItems ? current : void 0,
|
|
1200
|
+
"role": "progressbar"
|
|
1201
|
+
}, props)
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Вопрос: fieldset, виден, только пока открыт
|
|
1206
|
+
* @param props - пропсы вопроса
|
|
1207
|
+
* @returns вопрос с контекстом
|
|
1208
|
+
*/
|
|
1209
|
+
function Item({ "aria-describedby": ariaDescribedBy, "aria-keyshortcuts": ariaKeyShortcuts, disabled = false, invalid = false, multiple = false, name, onStatusChange, ref, required = false, ...props }) {
|
|
1210
|
+
const { context, setItemElement, itemProps, state } = useItem({
|
|
1211
|
+
ariaDescribedBy,
|
|
1212
|
+
ariaKeyShortcuts,
|
|
1213
|
+
disabled,
|
|
1214
|
+
invalid,
|
|
1215
|
+
multiple,
|
|
1216
|
+
name,
|
|
1217
|
+
onStatusChange,
|
|
1218
|
+
required
|
|
1219
|
+
});
|
|
1220
|
+
const element = useRender({
|
|
1221
|
+
defaultTagName: "fieldset",
|
|
1222
|
+
ref: [setItemElement, ref ?? null],
|
|
1223
|
+
state,
|
|
1224
|
+
stateAttributesMapping: { active: (value) => value ? { "data-active": "" } : null },
|
|
1225
|
+
props: mergeProps(itemProps, props)
|
|
1226
|
+
});
|
|
1227
|
+
return createElement(ItemContext, { value: context }, element);
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Заголовок вопроса: legend
|
|
1231
|
+
* @param props - пропсы заголовка
|
|
1232
|
+
* @returns legend
|
|
1233
|
+
*/
|
|
1234
|
+
function Title({ render, ref, ...props }) {
|
|
1235
|
+
useItemContext("QuestionnaireTitle");
|
|
1236
|
+
return useRender({
|
|
1237
|
+
defaultTagName: "legend",
|
|
1238
|
+
render,
|
|
1239
|
+
ref,
|
|
1240
|
+
props
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Описание вопроса: попадает в aria-describedby вопроса
|
|
1245
|
+
* @param props - пропсы описания
|
|
1246
|
+
* @returns абзац
|
|
1247
|
+
*/
|
|
1248
|
+
function Description({ id, render, ref, ...props }) {
|
|
1249
|
+
const { registerDescription } = useItemContext("QuestionnaireDescription");
|
|
1250
|
+
const generatedId = useId();
|
|
1251
|
+
const descriptionId = id ?? generatedId;
|
|
1252
|
+
useLayoutEffect(() => registerDescription(descriptionId), [descriptionId, registerDescription]);
|
|
1253
|
+
return useRender({
|
|
1254
|
+
defaultTagName: "p",
|
|
1255
|
+
render,
|
|
1256
|
+
ref,
|
|
1257
|
+
props: mergeProps({ id: descriptionId }, props)
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Список вариантов
|
|
1262
|
+
* @param props - пропсы списка
|
|
1263
|
+
* @returns div
|
|
1264
|
+
*/
|
|
1265
|
+
function Choices({ render, ref, ...props }) {
|
|
1266
|
+
const { shortcuts } = useItemContext("QuestionnaireChoices");
|
|
1267
|
+
return useRender({
|
|
1268
|
+
defaultTagName: "div",
|
|
1269
|
+
render,
|
|
1270
|
+
ref,
|
|
1271
|
+
state: { shortcuts },
|
|
1272
|
+
props
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Атрибуты выбранного и невыбранного варианта
|
|
1277
|
+
* @param checked - вариант выбран
|
|
1278
|
+
* @returns data-checked или data-unchecked
|
|
1279
|
+
*/
|
|
1280
|
+
function getCheckedAttributes(checked) {
|
|
1281
|
+
if (checked) return { "data-checked": "" };
|
|
1282
|
+
return { "data-unchecked": "" };
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Вариант ответа: label вокруг input
|
|
1286
|
+
* @param props - пропсы варианта
|
|
1287
|
+
* @returns label с контекстом варианта
|
|
1288
|
+
*/
|
|
1289
|
+
function Choice({ checked, defaultChecked = false, disabled = false, onChange, render, ref, value, ...props }) {
|
|
1290
|
+
const choice = useChoice({
|
|
1291
|
+
checked,
|
|
1292
|
+
defaultChecked,
|
|
1293
|
+
disabled,
|
|
1294
|
+
onChange,
|
|
1295
|
+
value
|
|
1296
|
+
});
|
|
1297
|
+
const element = useRender({
|
|
1298
|
+
defaultTagName: "label",
|
|
1299
|
+
render,
|
|
1300
|
+
ref,
|
|
1301
|
+
state: choice.state,
|
|
1302
|
+
stateAttributesMapping: { checked: getCheckedAttributes },
|
|
1303
|
+
props
|
|
1304
|
+
});
|
|
1305
|
+
return createElement(ChoiceContext, { value: choice }, element);
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* input варианта: радиокнопка или флажок
|
|
1309
|
+
* @param props - пропсы input
|
|
1310
|
+
* @returns input
|
|
1311
|
+
*/
|
|
1312
|
+
function ChoiceInput({ render, ref, ...props }) {
|
|
1313
|
+
const { inputProps, state } = useChoiceContext("QuestionnaireChoiceInput");
|
|
1314
|
+
const { ref: inputRef, ...ownProps } = inputProps;
|
|
1315
|
+
return useRender({
|
|
1316
|
+
defaultTagName: "input",
|
|
1317
|
+
render,
|
|
1318
|
+
ref: [inputRef, ref ?? null],
|
|
1319
|
+
state,
|
|
1320
|
+
stateAttributesMapping: { checked: getCheckedAttributes },
|
|
1321
|
+
props: mergeProps(ownProps, props)
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Подпись варианта
|
|
1326
|
+
* @param props - пропсы подписи
|
|
1327
|
+
* @returns span
|
|
1328
|
+
*/
|
|
1329
|
+
function ChoiceLabel({ render, ref, ...props }) {
|
|
1330
|
+
useChoiceContext("QuestionnaireChoiceLabel");
|
|
1331
|
+
return useRender({
|
|
1332
|
+
defaultTagName: "span",
|
|
1333
|
+
render,
|
|
1334
|
+
ref,
|
|
1335
|
+
props
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Подсказка клавиши варианта: скрыта, если клавиши нет
|
|
1340
|
+
* @param props - пропсы подсказки
|
|
1341
|
+
* @returns span
|
|
1342
|
+
*/
|
|
1343
|
+
function ChoiceShortcut({ children, render, ref, ...props }) {
|
|
1344
|
+
const { state } = useChoiceContext("QuestionnaireChoiceShortcut");
|
|
1345
|
+
const { shortcut } = state;
|
|
1346
|
+
return useRender({
|
|
1347
|
+
defaultTagName: "span",
|
|
1348
|
+
render,
|
|
1349
|
+
ref,
|
|
1350
|
+
state: { shortcut },
|
|
1351
|
+
props: mergeProps({
|
|
1352
|
+
"aria-hidden": true,
|
|
1353
|
+
"children": children ?? shortcut,
|
|
1354
|
+
"hidden": shortcut === null
|
|
1355
|
+
}, props)
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Атрибуты заполненного и пустого поля
|
|
1360
|
+
* @param filled - в поле есть текст
|
|
1361
|
+
* @returns data-filled или data-empty
|
|
1362
|
+
*/
|
|
1363
|
+
function getFilledAttributes(filled) {
|
|
1364
|
+
if (filled) return { "data-filled": "" };
|
|
1365
|
+
return { "data-empty": "" };
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Поле свободного ответа: ответ, только пока в нём есть текст
|
|
1369
|
+
* @param props - пропсы поля
|
|
1370
|
+
* @returns input
|
|
1371
|
+
*/
|
|
1372
|
+
function Input({ defaultValue, disabled = false, onChange, ref, render, type = "text", value, ...props }) {
|
|
1373
|
+
const { inputRef, inputProps, state } = useInput({
|
|
1374
|
+
defaultValue,
|
|
1375
|
+
disabled,
|
|
1376
|
+
onChange,
|
|
1377
|
+
type,
|
|
1378
|
+
value
|
|
1379
|
+
});
|
|
1380
|
+
return useRender({
|
|
1381
|
+
defaultTagName: "input",
|
|
1382
|
+
render,
|
|
1383
|
+
ref: [inputRef, ref ?? null],
|
|
1384
|
+
state,
|
|
1385
|
+
stateAttributesMapping: { filled: getFilledAttributes },
|
|
1386
|
+
props: mergeProps(inputProps, props)
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Ошибка вопроса: видна, когда ответ не прошёл проверку
|
|
1391
|
+
* @param props - пропсы ошибки
|
|
1392
|
+
* @returns абзац с role=alert
|
|
1393
|
+
*/
|
|
1394
|
+
function ErrorMessage({ id, render, ref, ...props }) {
|
|
1395
|
+
const { invalid, registerError } = useItemContext("QuestionnaireError");
|
|
1396
|
+
const generatedId = useId();
|
|
1397
|
+
const errorId = id ?? generatedId;
|
|
1398
|
+
const role = invalid ? "alert" : void 0;
|
|
1399
|
+
useLayoutEffect(() => registerError(errorId), [errorId, registerError]);
|
|
1400
|
+
return useRender({
|
|
1401
|
+
defaultTagName: "p",
|
|
1402
|
+
render,
|
|
1403
|
+
ref,
|
|
1404
|
+
state: { invalid },
|
|
1405
|
+
props: mergeProps({
|
|
1406
|
+
hidden: !invalid,
|
|
1407
|
+
id: errorId,
|
|
1408
|
+
role
|
|
1409
|
+
}, props)
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Атрибуты видимой и скрытой кнопки
|
|
1414
|
+
* @param visible - кнопка видна
|
|
1415
|
+
* @returns data-visible или data-hidden
|
|
1416
|
+
*/
|
|
1417
|
+
function getVisibleAttributes(visible) {
|
|
1418
|
+
if (visible) return { "data-visible": "" };
|
|
1419
|
+
return { "data-hidden": "" };
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Кнопка навигации: скрыта и недоступна, когда неприменима
|
|
1423
|
+
* @param parameters - параметры кнопки
|
|
1424
|
+
* @returns кнопка
|
|
1425
|
+
*/
|
|
1426
|
+
function useNavigationButton({ props, onClick, shortcut, status, visible }) {
|
|
1427
|
+
const { disabled = false, render, ref, tabIndex, ...ownProps } = props;
|
|
1428
|
+
const activeShortcut = visible && !disabled ? shortcut ?? null : null;
|
|
1429
|
+
return useRender({
|
|
1430
|
+
defaultTagName: "button",
|
|
1431
|
+
render,
|
|
1432
|
+
ref,
|
|
1433
|
+
state: {
|
|
1434
|
+
disabled,
|
|
1435
|
+
shortcut: activeShortcut,
|
|
1436
|
+
status,
|
|
1437
|
+
visible
|
|
1438
|
+
},
|
|
1439
|
+
stateAttributesMapping: { visible: getVisibleAttributes },
|
|
1440
|
+
props: mergeProps({
|
|
1441
|
+
"aria-hidden": !visible || void 0,
|
|
1442
|
+
"aria-keyshortcuts": activeShortcut ?? void 0,
|
|
1443
|
+
"disabled": disabled,
|
|
1444
|
+
"hidden": !visible,
|
|
1445
|
+
"inert": !visible,
|
|
1446
|
+
"onClick": onClick,
|
|
1447
|
+
"tabIndex": visible ? tabIndex : -1
|
|
1448
|
+
}, ownProps)
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Кнопка «Назад»: видна, если открыт не первый вопрос
|
|
1453
|
+
* @param props - пропсы кнопки
|
|
1454
|
+
* @returns кнопка
|
|
1455
|
+
*/
|
|
1456
|
+
function Previous({ onClick, type = "button", ...props }) {
|
|
1457
|
+
const root = useRootContext("QuestionnairePrevious");
|
|
1458
|
+
/**
|
|
1459
|
+
* Нажатие: к предыдущему вопросу
|
|
1460
|
+
* @param event - событие клика
|
|
1461
|
+
*/
|
|
1462
|
+
const handleClick = (event) => {
|
|
1463
|
+
onClick?.(event);
|
|
1464
|
+
if (!event.defaultPrevented) root.goPrevious();
|
|
1465
|
+
};
|
|
1466
|
+
return useNavigationButton({
|
|
1467
|
+
props: {
|
|
1468
|
+
...props,
|
|
1469
|
+
type
|
|
1470
|
+
},
|
|
1471
|
+
onClick: handleClick,
|
|
1472
|
+
status: root.activeItemStatus,
|
|
1473
|
+
visible: root.total > 1 && !root.first
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Кнопка «Пропустить»: видна у необязательного вопроса
|
|
1478
|
+
* @param props - пропсы кнопки
|
|
1479
|
+
* @returns кнопка
|
|
1480
|
+
*/
|
|
1481
|
+
function Skip({ onClick, type = "button", ...props }) {
|
|
1482
|
+
const root = useRootContext("QuestionnaireSkip");
|
|
1483
|
+
/**
|
|
1484
|
+
* Нажатие: пропустить вопрос
|
|
1485
|
+
* @param event - событие клика
|
|
1486
|
+
*/
|
|
1487
|
+
const handleClick = (event) => {
|
|
1488
|
+
onClick?.(event);
|
|
1489
|
+
if (!event.defaultPrevented) root.skipCurrent();
|
|
1490
|
+
};
|
|
1491
|
+
return useNavigationButton({
|
|
1492
|
+
props: {
|
|
1493
|
+
...props,
|
|
1494
|
+
type
|
|
1495
|
+
},
|
|
1496
|
+
onClick: handleClick,
|
|
1497
|
+
status: root.activeItemStatus,
|
|
1498
|
+
visible: root.activeItemRequired === false
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Кнопка «Далее»: видна, если открыт не последний вопрос
|
|
1503
|
+
* @param props - пропсы кнопки
|
|
1504
|
+
* @returns кнопка
|
|
1505
|
+
*/
|
|
1506
|
+
function Next({ onClick, type = "button", ...props }) {
|
|
1507
|
+
const root = useRootContext("QuestionnaireNext");
|
|
1508
|
+
/**
|
|
1509
|
+
* Нажатие: к следующему вопросу
|
|
1510
|
+
* @param event - событие клика
|
|
1511
|
+
*/
|
|
1512
|
+
const handleClick = (event) => {
|
|
1513
|
+
onClick?.(event);
|
|
1514
|
+
if (!event.defaultPrevented) root.goNext();
|
|
1515
|
+
};
|
|
1516
|
+
return useNavigationButton({
|
|
1517
|
+
props: {
|
|
1518
|
+
...props,
|
|
1519
|
+
type
|
|
1520
|
+
},
|
|
1521
|
+
onClick: handleClick,
|
|
1522
|
+
shortcut: "Enter",
|
|
1523
|
+
status: root.activeItemStatus,
|
|
1524
|
+
visible: root.total > 1 && !root.last
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Кнопка отправки: видна на последнем вопросе
|
|
1529
|
+
* @param props - пропсы кнопки
|
|
1530
|
+
* @returns кнопка
|
|
1531
|
+
*/
|
|
1532
|
+
function Submit({ type = "submit", ...props }) {
|
|
1533
|
+
const root = useRootContext("QuestionnaireSubmit");
|
|
1534
|
+
return useNavigationButton({
|
|
1535
|
+
props: {
|
|
1536
|
+
...props,
|
|
1537
|
+
type
|
|
1538
|
+
},
|
|
1539
|
+
shortcut: "Enter",
|
|
1540
|
+
status: root.activeItemStatus,
|
|
1541
|
+
visible: root.total > 0 && root.last
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
/** Части опроса без оформления */
|
|
1545
|
+
const QuestionnairePrimitive = {
|
|
1546
|
+
Root,
|
|
1547
|
+
Progress,
|
|
1548
|
+
Item,
|
|
1549
|
+
Title,
|
|
1550
|
+
Description,
|
|
1551
|
+
Choices,
|
|
1552
|
+
Choice,
|
|
1553
|
+
ChoiceInput,
|
|
1554
|
+
ChoiceLabel,
|
|
1555
|
+
ChoiceShortcut,
|
|
1556
|
+
Input,
|
|
1557
|
+
Error: ErrorMessage,
|
|
1558
|
+
Previous,
|
|
1559
|
+
Skip,
|
|
1560
|
+
Next,
|
|
1561
|
+
Submit
|
|
1562
|
+
};
|
|
1563
|
+
export { QuestionnairePrimitive };
|