@kouji-ui/core 0.0.4 → 0.1.1
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/.turbo/turbo-build.log +7 -4
- package/CHANGELOG.md +169 -0
- package/ng-package.json +5 -1
- package/package.json +18 -6
- package/src/a11y/aria-describedby.ts +7 -3
- package/src/a11y/aria-labelledby.spec.ts +55 -0
- package/src/a11y/aria-labelledby.ts +38 -0
- package/src/a11y/focus-trap.ts +4 -2
- package/src/a11y/index.ts +1 -0
- package/src/a11y/live-region.ts +4 -2
- package/src/a11y/roving-tabindex.spec.ts +139 -0
- package/src/a11y/roving-tabindex.ts +56 -7
- package/src/a11y/visually-hidden.ts +4 -2
- package/src/accordion/accordion.context.ts +22 -0
- package/src/accordion/accordion.spec.ts +233 -23
- package/src/accordion/accordion.ts +311 -39
- package/src/accordion/index.ts +8 -2
- package/src/alert/alert.context.ts +28 -0
- package/src/alert/alert.spec.ts +217 -0
- package/src/alert/alert.ts +417 -0
- package/src/alert/config.ts +46 -0
- package/src/alert/index.ts +19 -0
- package/src/avatar/avatar-group.context.ts +45 -0
- package/src/avatar/avatar-group.spec.ts +358 -0
- package/src/avatar/avatar-group.ts +259 -0
- package/src/avatar/avatar.ts +17 -4
- package/src/avatar/index.ts +11 -1
- package/src/badge/badge.spec.ts +8 -0
- package/src/badge/badge.ts +24 -3
- package/src/breadcrumb/breadcrumb-current.ts +38 -0
- package/src/breadcrumb/breadcrumb-ellipsis.ts +71 -0
- package/src/breadcrumb/breadcrumb-item.ts +41 -0
- package/src/breadcrumb/breadcrumb-link.ts +44 -0
- package/src/breadcrumb/breadcrumb-list.ts +42 -0
- package/src/breadcrumb/breadcrumb-separator.ts +45 -0
- package/src/breadcrumb/breadcrumb.context.ts +45 -0
- package/src/breadcrumb/breadcrumb.spec.ts +354 -0
- package/src/breadcrumb/breadcrumb.ts +239 -0
- package/src/breadcrumb/config.ts +84 -0
- package/src/breadcrumb/index.ts +18 -0
- package/src/button/_examples/button.example.ts +71 -0
- package/src/button/_examples/button.finance.example.ts +83 -0
- package/src/button/_examples/button.retro.example.ts +91 -0
- package/src/button/_examples/button.sizes.example.ts +60 -0
- package/src/button/_examples/index.ts +6 -0
- package/src/button/button.spec.ts +139 -8
- package/src/button/button.ts +114 -16
- package/src/button/config.spec.ts +33 -0
- package/src/button/config.ts +45 -0
- package/src/button/index.ts +2 -1
- package/src/button-group/button-group.context.ts +28 -0
- package/src/button-group/button-group.spec.ts +99 -0
- package/src/button-group/button-group.ts +99 -0
- package/src/button-group/index.ts +2 -0
- package/src/calendar/calendar-day.ts +102 -0
- package/src/calendar/calendar-grid.ts +90 -0
- package/src/calendar/calendar-header.ts +65 -0
- package/src/calendar/calendar.context.ts +53 -0
- package/src/calendar/calendar.spec.ts +225 -0
- package/src/calendar/calendar.ts +233 -0
- package/src/calendar/date-utils.ts +225 -0
- package/src/calendar/index.ts +28 -0
- package/src/carousel/carousel.context.ts +81 -0
- package/src/carousel/carousel.spec.ts +337 -0
- package/src/carousel/carousel.ts +919 -0
- package/src/carousel/index.ts +22 -0
- package/src/cascade-select/cascade-select-option.ts +205 -0
- package/src/cascade-select/cascade-select-panel.ts +106 -0
- package/src/cascade-select/cascade-select-root.ts +265 -0
- package/src/cascade-select/cascade-select-sub-panel.ts +168 -0
- package/src/cascade-select/cascade-select-trigger.ts +44 -0
- package/src/cascade-select/cascade-select.context.ts +71 -0
- package/src/cascade-select/cascade-select.spec.ts +241 -0
- package/src/cascade-select/cascade-select.ts +5 -0
- package/src/cascade-select/index.ts +11 -0
- package/src/chart/chart.ts +6 -2
- package/src/chat/chat-avatar.ts +31 -0
- package/src/chat/chat-bubble.ts +49 -0
- package/src/chat/chat-footer.ts +67 -0
- package/src/chat/chat-header.ts +41 -0
- package/src/chat/chat-log.ts +43 -0
- package/src/chat/chat.context.ts +64 -0
- package/src/chat/chat.spec.ts +341 -0
- package/src/chat/chat.ts +191 -0
- package/src/chat/config.ts +53 -0
- package/src/chat/index.ts +21 -0
- package/src/checkbox/checkbox.ts +9 -2
- package/src/color-picker/color-picker.context.ts +74 -0
- package/src/color-picker/color-picker.spec.ts +241 -0
- package/src/color-picker/color-picker.ts +698 -0
- package/src/color-picker/color-picker.utils.ts +140 -0
- package/src/color-picker/index.ts +28 -0
- package/src/combobox/combobox-input.ts +143 -0
- package/src/combobox/combobox-listbox.ts +49 -0
- package/src/combobox/combobox-option.ts +43 -0
- package/src/combobox/combobox-root.ts +281 -0
- package/src/combobox/combobox.spec.ts +75 -0
- package/src/combobox/combobox.ts +4 -0
- package/src/combobox/index.ts +4 -0
- package/src/command-palette/command-empty.ts +36 -0
- package/src/command-palette/command-group.ts +21 -0
- package/src/command-palette/command-input.ts +70 -0
- package/src/command-palette/command-item.ts +60 -0
- package/src/command-palette/command-list.ts +25 -0
- package/src/command-palette/command-palette-dialog.ts +74 -0
- package/src/command-palette/command-palette-trigger.ts +64 -0
- package/src/command-palette/command-palette.filters.ts +53 -0
- package/src/command-palette/command-palette.spec.ts +349 -0
- package/src/command-palette/command-palette.ts +204 -0
- package/src/command-palette/command-separator.ts +18 -0
- package/src/command-palette/index.ts +15 -0
- package/src/confirm-popup/confirm-popup-action.ts +39 -0
- package/src/confirm-popup/confirm-popup-cancel.ts +38 -0
- package/src/confirm-popup/confirm-popup-content.ts +113 -0
- package/src/confirm-popup/confirm-popup-message.ts +36 -0
- package/src/confirm-popup/confirm-popup-trigger.ts +31 -0
- package/src/confirm-popup/confirm-popup.context.ts +41 -0
- package/src/confirm-popup/confirm-popup.spec.ts +337 -0
- package/src/confirm-popup/confirm-popup.ts +109 -0
- package/src/confirm-popup/index.ts +12 -0
- package/src/date-picker/date-picker-calendar.ts +65 -0
- package/src/date-picker/date-picker-trigger.ts +215 -0
- package/src/date-picker/date-picker.context.ts +50 -0
- package/src/date-picker/date-picker.spec.ts +117 -0
- package/src/date-picker/date-picker.ts +127 -0
- package/src/date-picker/index.ts +7 -0
- package/src/dialog/_examples/dialog.confirm.example.ts +145 -0
- package/src/dialog/_examples/dialog.example.ts +106 -0
- package/src/dialog/_examples/dialog.finance.example.ts +121 -0
- package/src/dialog/_examples/dialog.retro.example.ts +126 -0
- package/src/dialog/_examples/index.ts +6 -0
- package/src/dialog/dialog.ref.ts +42 -0
- package/src/dialog/dialog.service.ts +57 -167
- package/src/dialog/dialog.spec.ts +35 -120
- package/src/dialog/dialog.ts +17 -192
- package/src/dialog/index.ts +3 -9
- package/src/divider/divider.spec.ts +223 -0
- package/src/divider/divider.ts +137 -0
- package/src/divider/index.ts +5 -0
- package/src/drawer/drawer.ref.ts +63 -0
- package/src/drawer/drawer.service.ts +106 -0
- package/src/drawer/drawer.spec.ts +117 -0
- package/src/drawer/drawer.ts +122 -0
- package/src/drawer/index.ts +10 -0
- package/src/dropdown-menu/dropdown-menu-content.ts +258 -0
- package/src/dropdown-menu/dropdown-menu-group.ts +31 -0
- package/src/dropdown-menu/dropdown-menu-item.ts +51 -0
- package/src/dropdown-menu/dropdown-menu-label.ts +41 -0
- package/src/dropdown-menu/dropdown-menu-separator.ts +27 -0
- package/src/dropdown-menu/dropdown-menu-trigger.ts +140 -0
- package/src/dropdown-menu/dropdown-menu.spec.ts +265 -0
- package/src/dropdown-menu/dropdown-menu.ts +60 -0
- package/src/dropdown-menu/index.ts +15 -0
- package/src/example-components.ts +39 -30
- package/src/field/field-error.ts +49 -0
- package/src/field/field-group.ts +39 -0
- package/src/field/field-help.ts +47 -0
- package/src/field/field-label.ts +30 -0
- package/src/field/field.context.ts +28 -0
- package/src/field/field.spec.ts +179 -0
- package/src/field/field.ts +121 -0
- package/src/field/index.ts +6 -0
- package/src/file-upload/file-upload.context.ts +89 -0
- package/src/file-upload/file-upload.spec.ts +290 -0
- package/src/file-upload/file-upload.ts +639 -0
- package/src/file-upload/file-upload.types.ts +59 -0
- package/src/file-upload/index.ts +23 -0
- package/src/form/form-error-summary.ts +87 -0
- package/src/form/form-field.context.ts +3 -0
- package/src/form/form-field.spec.ts +25 -0
- package/src/form/form-field.ts +52 -0
- package/src/form/form.context.ts +52 -2
- package/src/form/form.spec.ts +155 -18
- package/src/form/form.ts +315 -35
- package/src/form/index.ts +11 -2
- package/src/icon/E2E.md +14 -0
- package/src/icon/icon.css +40 -0
- package/src/icon/icon.directive.spec.ts +198 -0
- package/src/icon/icon.directive.ts +113 -0
- package/src/icon/icon.mode.spec.ts +29 -0
- package/src/icon/icon.mode.ts +23 -0
- package/src/icon/icon.providers.spec.ts +63 -0
- package/src/icon/icon.providers.ts +47 -0
- package/src/icon/icon.resolver.spec.ts +96 -0
- package/src/icon/icon.resolver.ts +49 -0
- package/src/icon/icon.tokens.ts +88 -0
- package/src/icon/icon.types.ts +48 -0
- package/src/icon/index.ts +21 -0
- package/src/input/input.ts +12 -3
- package/src/input-group/index.ts +3 -0
- package/src/input-group/input-group-addon.ts +92 -0
- package/src/input-group/input-group.context.ts +27 -0
- package/src/input-group/input-group.spec.ts +186 -0
- package/src/input-group/input-group.ts +136 -0
- package/src/input-mask/index.ts +11 -0
- package/src/input-mask/input-mask.compile.ts +69 -0
- package/src/input-mask/input-mask.engine.ts +376 -0
- package/src/input-mask/input-mask.presets.ts +24 -0
- package/src/input-mask/input-mask.spec.ts +304 -0
- package/src/input-mask/input-mask.tokens.ts +49 -0
- package/src/input-mask/input-mask.ts +280 -0
- package/src/input-otp/index.ts +2 -0
- package/src/input-otp/input-otp.context.ts +41 -0
- package/src/input-otp/input-otp.spec.ts +308 -0
- package/src/input-otp/input-otp.ts +470 -0
- package/src/kbd/index.ts +1 -0
- package/src/kbd/kbd.spec.ts +127 -0
- package/src/kbd/kbd.ts +132 -0
- package/src/link/config.ts +44 -0
- package/src/link/index.ts +2 -0
- package/src/link/link.spec.ts +246 -0
- package/src/link/link.ts +226 -0
- package/src/list/index.ts +11 -0
- package/src/list/list.spec.ts +237 -0
- package/src/list/list.ts +277 -0
- package/src/menubar/index.ts +7 -0
- package/src/menubar/menubar-item.ts +204 -0
- package/src/menubar/menubar.context.ts +43 -0
- package/src/menubar/menubar.spec.ts +433 -0
- package/src/menubar/menubar.ts +309 -0
- package/src/number-input/index.ts +4 -0
- package/src/number-input/number-input-group.ts +64 -0
- package/src/number-input/number-input.context.ts +31 -0
- package/src/number-input/number-input.format.ts +148 -0
- package/src/number-input/number-input.spec.ts +170 -0
- package/src/number-input/number-input.ts +457 -0
- package/src/number-input/number-stepper.ts +193 -0
- package/src/overlay-badge/index.ts +7 -0
- package/src/overlay-badge/overlay-badge-content.ts +73 -0
- package/src/overlay-badge/overlay-badge.context.ts +37 -0
- package/src/overlay-badge/overlay-badge.spec.ts +220 -0
- package/src/overlay-badge/overlay-badge.ts +128 -0
- package/src/pagination/config.ts +82 -0
- package/src/pagination/index.ts +19 -0
- package/src/pagination/page-tokens.ts +78 -0
- package/src/pagination/pagination-ellipsis.ts +69 -0
- package/src/pagination/pagination-first.ts +84 -0
- package/src/pagination/pagination-info.ts +70 -0
- package/src/pagination/pagination-item.ts +81 -0
- package/src/pagination/pagination-last.ts +85 -0
- package/src/pagination/pagination-next.ts +86 -0
- package/src/pagination/pagination-previous.ts +90 -0
- package/src/pagination/pagination.context.ts +52 -0
- package/src/pagination/pagination.spec.ts +388 -0
- package/src/pagination/pagination.ts +288 -0
- package/src/password-input/index.ts +15 -0
- package/src/password-input/password-input.context.ts +65 -0
- package/src/password-input/password-input.scorer.ts +34 -0
- package/src/password-input/password-input.spec.ts +274 -0
- package/src/password-input/password-input.ts +488 -0
- package/src/popover/_examples/index.ts +5 -0
- package/src/popover/_examples/popover.example.ts +92 -0
- package/src/popover/_examples/popover.finance.example.ts +106 -0
- package/src/popover/_examples/popover.retro.example.ts +103 -0
- package/src/popover/index.ts +5 -12
- package/src/popover/popover-arrow.ts +30 -0
- package/src/popover/popover-close.ts +36 -0
- package/src/popover/popover-content.ts +41 -0
- package/src/popover/popover-title.ts +39 -0
- package/src/popover/popover-trigger.ts +40 -0
- package/src/popover/popover.spec.ts +51 -44
- package/src/popover/popover.ts +5 -124
- package/src/presets/bind-presets.spec.ts +50 -0
- package/src/presets/bind-presets.ts +43 -0
- package/src/presets/index.ts +3 -0
- package/src/presets/size.spec.ts +67 -0
- package/src/presets/size.ts +59 -0
- package/src/presets/variant.spec.ts +72 -0
- package/src/presets/variant.ts +62 -0
- package/src/primitives/directionality/directionality.spec.ts +112 -0
- package/src/primitives/directionality/directionality.ts +89 -0
- package/src/primitives/directionality/index.ts +1 -0
- package/src/primitives/forms/form-control.spec.ts +11 -3
- package/src/primitives/forms/form-control.ts +6 -2
- package/src/primitives/index.ts +3 -0
- package/src/primitives/interaction/disabled.ts +12 -5
- package/src/primitives/interaction/focus-ring.ts +4 -2
- package/src/primitives/list/filterable-list.spec.ts +114 -0
- package/src/primitives/list/filterable-list.ts +126 -0
- package/src/primitives/list/group.spec.ts +99 -0
- package/src/primitives/list/group.ts +139 -0
- package/src/primitives/list/index.ts +8 -0
- package/src/primitives/list/inject-helpers.ts +8 -0
- package/src/primitives/list/item.spec.ts +173 -0
- package/src/primitives/list/item.ts +222 -0
- package/src/primitives/list/navigator.spec.ts +286 -0
- package/src/primitives/list/navigator.ts +238 -0
- package/src/primitives/list/selection.spec.ts +214 -0
- package/src/primitives/list/selection.ts +339 -0
- package/src/primitives/list/tokens.ts +106 -0
- package/src/primitives/list/type-ahead.spec.ts +58 -0
- package/src/primitives/list/type-ahead.ts +46 -0
- package/src/primitives/overlay/backdrop.spec.ts +54 -0
- package/src/primitives/overlay/backdrop.ts +31 -0
- package/src/primitives/overlay/builder.spec.ts +38 -0
- package/src/primitives/overlay/builder.ts +151 -0
- package/src/primitives/overlay/container.ts +42 -0
- package/src/primitives/overlay/context.ts +14 -0
- package/src/primitives/overlay/controller.spec.ts +86 -0
- package/src/primitives/overlay/controller.ts +190 -0
- package/src/primitives/overlay/id.spec.ts +24 -0
- package/src/primitives/overlay/id.ts +17 -0
- package/src/primitives/overlay/index.ts +18 -1
- package/src/primitives/overlay/overlay.css +42 -0
- package/src/primitives/overlay/panel.spec.ts +52 -0
- package/src/primitives/overlay/panel.ts +102 -0
- package/src/primitives/overlay/stack.spec.ts +55 -0
- package/src/primitives/overlay/stack.ts +113 -0
- package/src/primitives/overlay/strategies/backdrop/blurred.spec.ts +11 -0
- package/src/primitives/overlay/strategies/backdrop/blurred.ts +5 -0
- package/src/primitives/overlay/strategies/backdrop/none.spec.ts +10 -0
- package/src/primitives/overlay/strategies/backdrop/none.ts +9 -0
- package/src/primitives/overlay/strategies/backdrop/solid.spec.ts +18 -0
- package/src/primitives/overlay/strategies/backdrop/solid.ts +23 -0
- package/src/primitives/overlay/strategies/focus-trap/inert-based.spec.ts +28 -0
- package/src/primitives/overlay/strategies/focus-trap/inert-based.ts +38 -0
- package/src/primitives/overlay/strategies/focus-trap/no-trap.spec.ts +9 -0
- package/src/primitives/overlay/strategies/focus-trap/no-trap.ts +8 -0
- package/src/primitives/overlay/strategies/focus-trap/tab-cycle.spec.ts +49 -0
- package/src/primitives/overlay/strategies/focus-trap/tab-cycle.ts +85 -0
- package/src/primitives/overlay/strategies/index.ts +34 -0
- package/src/primitives/overlay/strategies/live-announcer/_announce.ts +35 -0
- package/src/primitives/overlay/strategies/live-announcer/assertive.spec.ts +21 -0
- package/src/primitives/overlay/strategies/live-announcer/assertive.ts +11 -0
- package/src/primitives/overlay/strategies/live-announcer/polite.spec.ts +21 -0
- package/src/primitives/overlay/strategies/live-announcer/polite.ts +11 -0
- package/src/primitives/overlay/strategies/live-announcer/silent.spec.ts +12 -0
- package/src/primitives/overlay/strategies/live-announcer/silent.ts +5 -0
- package/src/primitives/overlay/strategies/mount/body-portal.spec.ts +40 -0
- package/src/primitives/overlay/strategies/mount/body-portal.ts +91 -0
- package/src/primitives/overlay/strategies/mount/in-container.spec.ts +43 -0
- package/src/primitives/overlay/strategies/mount/in-container.ts +39 -0
- package/src/primitives/overlay/strategies/mount/in-place.spec.ts +40 -0
- package/src/primitives/overlay/strategies/mount/in-place.ts +20 -0
- package/src/primitives/overlay/strategies/position/anchored-to.spec.ts +56 -0
- package/src/primitives/overlay/strategies/position/anchored-to.ts +248 -0
- package/src/primitives/overlay/strategies/position/corner.spec.ts +36 -0
- package/src/primitives/overlay/strategies/position/corner.ts +33 -0
- package/src/primitives/overlay/strategies/position/edge-sheet.spec.ts +32 -0
- package/src/primitives/overlay/strategies/position/edge-sheet.ts +36 -0
- package/src/primitives/overlay/strategies/position/in-place-sibling.spec.ts +25 -0
- package/src/primitives/overlay/strategies/position/in-place-sibling.ts +13 -0
- package/src/primitives/overlay/strategies/position/point-at.spec.ts +40 -0
- package/src/primitives/overlay/strategies/position/point-at.ts +34 -0
- package/src/primitives/overlay/strategies/position/viewport-centered.spec.ts +31 -0
- package/src/primitives/overlay/strategies/position/viewport-centered.ts +31 -0
- package/src/primitives/overlay/strategies/scroll-lock/css-clip.spec.ts +15 -0
- package/src/primitives/overlay/strategies/scroll-lock/css-clip.ts +39 -0
- package/src/primitives/overlay/strategies/scroll-lock/html-overflow.spec.ts +21 -0
- package/src/primitives/overlay/strategies/scroll-lock/html-overflow.ts +44 -0
- package/src/primitives/overlay/strategies/scroll-lock/none.spec.ts +9 -0
- package/src/primitives/overlay/strategies/scroll-lock/none.ts +5 -0
- package/src/primitives/overlay/strategies/trigger-event/on-click.spec.ts +32 -0
- package/src/primitives/overlay/strategies/trigger-event/on-click.ts +28 -0
- package/src/primitives/overlay/strategies/trigger-event/on-context-menu.spec.ts +45 -0
- package/src/primitives/overlay/strategies/trigger-event/on-context-menu.ts +70 -0
- package/src/primitives/overlay/strategies/trigger-event/on-focus-or-input.spec.ts +35 -0
- package/src/primitives/overlay/strategies/trigger-event/on-focus-or-input.ts +34 -0
- package/src/primitives/overlay/strategies/trigger-event/on-focus.spec.ts +64 -0
- package/src/primitives/overlay/strategies/trigger-event/on-focus.ts +37 -0
- package/src/primitives/overlay/strategies/trigger-event/on-hotkey.spec.ts +37 -0
- package/src/primitives/overlay/strategies/trigger-event/on-hotkey.ts +59 -0
- package/src/primitives/overlay/strategies/trigger-event/on-hover.spec.ts +34 -0
- package/src/primitives/overlay/strategies/trigger-event/on-hover.ts +86 -0
- package/src/primitives/overlay/strategies/trigger-event/programmatic.spec.ts +16 -0
- package/src/primitives/overlay/strategies/trigger-event/programmatic.ts +12 -0
- package/src/primitives/overlay/tokens.ts +54 -0
- package/src/primitives/overlay/trigger.spec.ts +103 -0
- package/src/primitives/overlay/trigger.ts +75 -0
- package/src/primitives/overlay/types.ts +24 -0
- package/src/primitives/overlay/wrapper.ts +50 -0
- package/src/primitives/signals/deep-signal.ts +109 -0
- package/src/primitives/signals/index.ts +1 -0
- package/src/progress-bar/config.ts +48 -0
- package/src/progress-bar/index.ts +12 -0
- package/src/progress-bar/progress-bar-fill.ts +37 -0
- package/src/progress-bar/progress-bar.context.ts +34 -0
- package/src/progress-bar/progress-bar.spec.ts +423 -0
- package/src/progress-bar/progress-bar.ts +217 -0
- package/src/public-api.ts +58 -9
- package/src/radio/radio.ts +14 -3
- package/src/select/index.ts +4 -2
- package/src/select/select-content.ts +69 -0
- package/src/select/select-option.ts +35 -0
- package/src/select/select-root.ts +129 -0
- package/src/select/select-trigger.ts +93 -0
- package/src/select/select.spec.ts +133 -140
- package/src/select/select.ts +4 -250
- package/src/skeleton/index.ts +5 -0
- package/src/skeleton/skeleton.spec.ts +115 -0
- package/src/skeleton/skeleton.ts +56 -0
- package/src/slider/index.ts +10 -0
- package/src/slider/slider-range.ts +49 -0
- package/src/slider/slider-thumb.ts +335 -0
- package/src/slider/slider-track.ts +70 -0
- package/src/slider/slider.context.ts +79 -0
- package/src/slider/slider.geometry.ts +73 -0
- package/src/slider/slider.spec.ts +216 -0
- package/src/slider/slider.ts +443 -0
- package/src/speed-dial/index.ts +10 -0
- package/src/speed-dial/speed-dial-action.ts +40 -0
- package/src/speed-dial/speed-dial-actions.ts +31 -0
- package/src/speed-dial/speed-dial-trigger.ts +55 -0
- package/src/speed-dial/speed-dial.context.ts +36 -0
- package/src/speed-dial/speed-dial.spec.ts +143 -0
- package/src/speed-dial/speed-dial.ts +127 -0
- package/src/spinner/config.ts +48 -0
- package/src/spinner/index.ts +8 -0
- package/src/spinner/spinner.spec.ts +262 -0
- package/src/spinner/spinner.ts +129 -0
- package/src/stepper/index.ts +15 -0
- package/src/stepper/stepper.context.ts +63 -0
- package/src/stepper/stepper.spec.ts +403 -0
- package/src/stepper/stepper.ts +544 -0
- package/src/styles.css +17 -0
- package/src/table/column-helpers.spec.ts +74 -0
- package/src/table/column-helpers.ts +68 -0
- package/src/table/filter-fns.ts +230 -0
- package/src/table/filter-models.spec.ts +240 -0
- package/src/table/filter-models.ts +124 -0
- package/src/table/filter-params.ts +90 -0
- package/src/table/grid-api-impl.ts +321 -0
- package/src/table/grid-api.spec.ts +178 -0
- package/src/table/grid-api.ts +285 -0
- package/src/table/index.ts +73 -1
- package/src/table/table-cell.spec.ts +43 -0
- package/src/table/table-cell.ts +38 -0
- package/src/table/table-filter-outlet.ts +120 -0
- package/src/table/table-header.spec.ts +54 -0
- package/src/table/table-header.ts +63 -0
- package/src/table/table-keyboard.spec.ts +68 -0
- package/src/table/table-keyboard.ts +107 -0
- package/src/table/table-resource.spec.ts +37 -0
- package/src/table/table-resource.ts +36 -0
- package/src/table/table-row.spec.ts +54 -0
- package/src/table/table-row.ts +31 -0
- package/src/table/table-storage.spec.ts +67 -0
- package/src/table/table-storage.ts +76 -0
- package/src/table/table.spec.ts +101 -42
- package/src/table/table.ts +228 -73
- package/src/table/table.types.ts +83 -0
- package/src/tabs/index.ts +7 -2
- package/src/tabs/tabs.context.ts +27 -4
- package/src/tabs/tabs.spec.ts +336 -24
- package/src/tabs/tabs.ts +313 -79
- package/src/tag/config.ts +53 -0
- package/src/tag/index.ts +7 -0
- package/src/tag/tag-list.ts +76 -0
- package/src/tag/tag-remove.ts +61 -0
- package/src/tag/tag.context.ts +36 -0
- package/src/tag/tag.spec.ts +240 -0
- package/src/tag/tag.ts +203 -0
- package/src/textarea/config.ts +42 -0
- package/src/textarea/index.ts +8 -0
- package/src/textarea/textarea.spec.ts +176 -0
- package/src/textarea/textarea.ts +235 -0
- package/src/time-picker/index.ts +15 -0
- package/src/time-picker/time-picker-meridiem.ts +123 -0
- package/src/time-picker/time-picker-segment.ts +318 -0
- package/src/time-picker/time-picker.context.ts +38 -0
- package/src/time-picker/time-picker.format.ts +181 -0
- package/src/time-picker/time-picker.spec.ts +189 -0
- package/src/time-picker/time-picker.ts +274 -0
- package/src/toast/_examples/index.ts +5 -0
- package/src/toast/_examples/toast.example.ts +282 -0
- package/src/toast/_examples/toast.finance.example.ts +180 -0
- package/src/toast/_examples/toast.retro.example.ts +161 -0
- package/src/toast/index.ts +8 -1
- package/src/toast/toast.ref.ts +26 -0
- package/src/toast/toast.service.ts +227 -41
- package/src/toast/toast.spec.ts +166 -14
- package/src/toast/toast.strategy.ts +10 -0
- package/src/toast/toast.ts +177 -21
- package/src/toggle/toggle.ts +10 -2
- package/src/tooltip/_examples/index.ts +6 -0
- package/src/tooltip/_examples/tooltip.example.ts +82 -0
- package/src/tooltip/_examples/tooltip.finance.example.ts +97 -0
- package/src/tooltip/_examples/tooltip.placements.example.ts +83 -0
- package/src/tooltip/_examples/tooltip.retro.example.ts +107 -0
- package/src/tooltip/index.ts +4 -5
- package/src/tooltip/tooltip-arrow.ts +31 -0
- package/src/tooltip/tooltip-content.ts +38 -0
- package/src/tooltip/tooltip-group.ts +32 -0
- package/src/tooltip/tooltip-trigger.ts +47 -0
- package/src/tooltip/tooltip.spec.ts +38 -102
- package/src/tooltip/tooltip.ts +5 -129
- package/src/tree-select/index.ts +12 -0
- package/src/tree-select/tree-select-content.ts +199 -0
- package/src/tree-select/tree-select-node.ts +132 -0
- package/src/tree-select/tree-select-root.ts +272 -0
- package/src/tree-select/tree-select-trigger.ts +90 -0
- package/src/tree-select/tree-select.context.ts +63 -0
- package/src/tree-select/tree-select.spec.ts +433 -0
- package/src/tree-select/tree-select.ts +2 -0
- package/src/typography/blockquote.ts +57 -0
- package/src/typography/code.ts +53 -0
- package/src/typography/index.ts +22 -0
- package/src/typography/lead.ts +49 -0
- package/src/typography/muted.ts +29 -0
- package/src/typography/prose.css +312 -0
- package/src/typography/truncate.ts +99 -0
- package/src/typography/typography.spec.ts +203 -0
- package/tsconfig.lib.json +9 -1
- package/tsconfig.lib.prod.json +7 -1
- package/tsconfig.spec.json +9 -1
- package/vite.config.ts +8 -0
- package/src/button/button.example.ts +0 -30
- package/src/button/button.finance.example.ts +0 -37
- package/src/button/button.retro.example.ts +0 -47
- package/src/button/button.sizes.example.ts +0 -25
- package/src/dialog/dialog.confirm.example.ts +0 -57
- package/src/dialog/dialog.context.ts +0 -11
- package/src/dialog/dialog.example.ts +0 -40
- package/src/dialog/dialog.finance.example.ts +0 -47
- package/src/dialog/dialog.retro.example.ts +0 -46
- package/src/menu/index.ts +0 -2
- package/src/menu/menu.context.ts +0 -10
- package/src/menu/menu.example.ts +0 -39
- package/src/menu/menu.spec.ts +0 -168
- package/src/menu/menu.ts +0 -176
- package/src/popover/popover.context.ts +0 -18
- package/src/popover/popover.example.ts +0 -50
- package/src/popover/popover.finance.example.ts +0 -52
- package/src/popover/popover.retro.example.ts +0 -53
- package/src/primitives/overlay/overlay.spec.ts +0 -90
- package/src/primitives/overlay/overlay.ts +0 -106
- package/src/select/select.context.ts +0 -3
- package/src/toast/toast.example.ts +0 -134
- package/src/toast/toast.finance.example.ts +0 -68
- package/src/toast/toast.retro.example.ts +0 -69
- package/src/tooltip/tooltip.example.ts +0 -46
- package/src/tooltip/tooltip.finance.example.ts +0 -54
- package/src/tooltip/tooltip.placements.example.ts +0 -56
- package/src/tooltip/tooltip.retro.example.ts +0 -65
package/src/form/form.ts
CHANGED
|
@@ -1,46 +1,326 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
Directive,
|
|
3
|
+
ElementRef,
|
|
4
|
+
Injector,
|
|
5
|
+
afterNextRender,
|
|
6
|
+
booleanAttribute,
|
|
7
|
+
computed,
|
|
8
|
+
inject,
|
|
9
|
+
input,
|
|
10
|
+
model,
|
|
11
|
+
output,
|
|
12
|
+
signal,
|
|
13
|
+
} from '@angular/core';
|
|
14
|
+
import { AbstractControl, FormArray, FormGroup, FormGroupDirective, NgForm } from '@angular/forms';
|
|
15
|
+
import { Observable } from 'rxjs';
|
|
16
|
+
import {
|
|
17
|
+
InvalidControlInfo,
|
|
18
|
+
KJ_FORM,
|
|
19
|
+
KjFormContext,
|
|
20
|
+
KjFormControlRegistration,
|
|
21
|
+
} from './form.context';
|
|
3
22
|
|
|
4
23
|
/**
|
|
5
|
-
*
|
|
24
|
+
* Async submit handler signature accepted by `[kjAsyncSubmit]`. The directive
|
|
25
|
+
* awaits the returned promise/observable and toggles `submitting` for its
|
|
26
|
+
* lifetime.
|
|
27
|
+
*/
|
|
28
|
+
export type KjAsyncSubmitHandler<T = unknown> = (
|
|
29
|
+
value: T,
|
|
30
|
+
) => void | Promise<unknown> | Observable<unknown>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Coordinator directive applied to a `<form>` element. Owns the submit
|
|
34
|
+
* pipeline (intercept, validity check, focus-on-first-error, scroll-into-view,
|
|
35
|
+
* async submitting state, mark-all-as-touched, optional error summary). Defers
|
|
36
|
+
* to Angular Forms (`FormGroup` / `NgForm`) for validation; this directive is
|
|
37
|
+
* a coordinator, not a state holder.
|
|
38
|
+
*
|
|
39
|
+
* - Emits `(kjSubmit)` only when the form is valid (replaces hand-rolled
|
|
40
|
+
* `(ngSubmit)` + `if (form.invalid) return` patterns).
|
|
41
|
+
* - Emits `(kjInvalidSubmit)` when the user submitted but validity failed,
|
|
42
|
+
* useful for analytics.
|
|
43
|
+
* - When `[kjAsyncSubmit]` is set, awaits its returned `Promise`/`Observable`
|
|
44
|
+
* and reflects `aria-busy="true"` / `data-submitting=""` for the lifetime.
|
|
45
|
+
*
|
|
6
46
|
* @example
|
|
7
47
|
* ```html
|
|
8
|
-
* <
|
|
9
|
-
* <
|
|
10
|
-
* <
|
|
11
|
-
*
|
|
12
|
-
* </div>
|
|
48
|
+
* <form kjForm [formGroup]="form" (kjSubmit)="onSubmit($event)">
|
|
49
|
+
* <input formControlName="email" kjInput />
|
|
50
|
+
* <button type="submit">Sign in</button>
|
|
51
|
+
* </form>
|
|
13
52
|
* ```
|
|
14
|
-
* @category Core/Inputs
|
|
53
|
+
* @doc-category Core/Inputs
|
|
54
|
+
* @doc
|
|
55
|
+
* @doc-name form
|
|
56
|
+
* @doc-description Coordinates form submit with validation, first-error focus, and async submitting state.
|
|
57
|
+
* @doc-is-main
|
|
15
58
|
*/
|
|
16
59
|
@Directive({
|
|
17
|
-
selector: '[
|
|
18
|
-
|
|
19
|
-
|
|
60
|
+
selector: 'form[kjForm]',
|
|
61
|
+
standalone: true,
|
|
62
|
+
exportAs: 'kjForm',
|
|
63
|
+
providers: [{ provide: KJ_FORM, useExisting: KjForm }],
|
|
64
|
+
host: {
|
|
65
|
+
'[attr.aria-busy]': 'submitting() ? "true" : null',
|
|
66
|
+
'[attr.aria-describedby]': 'errorSummaryId() ?? null',
|
|
67
|
+
'[attr.data-submitting]': 'submitting() ? "" : null',
|
|
68
|
+
'[attr.data-invalid]': 'invalid() ? "" : null',
|
|
69
|
+
'(submit)': 'onNativeSubmit($event)',
|
|
70
|
+
},
|
|
20
71
|
})
|
|
21
|
-
export class
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
readonly
|
|
72
|
+
export class KjForm implements KjFormContext {
|
|
73
|
+
private readonly el = inject<ElementRef<HTMLFormElement>>(ElementRef);
|
|
74
|
+
private readonly injector = inject(Injector);
|
|
75
|
+
private readonly ngForm = inject(NgForm, { optional: true, self: true });
|
|
76
|
+
private readonly formGroupDir = inject(FormGroupDirective, { optional: true, self: true });
|
|
77
|
+
|
|
78
|
+
// ── Inputs ───────────────────────────────────────────────────────────
|
|
79
|
+
/** When `false`, skips moving focus to the first invalid control on invalid submit. */
|
|
80
|
+
readonly kjFocusOnError = input(true, { transform: booleanAttribute });
|
|
81
|
+
/** When `false`, does not call `scrollIntoView` on the focused control. */
|
|
82
|
+
readonly kjScrollOnError = input(true, { transform: booleanAttribute });
|
|
83
|
+
/** Forwarded to `scrollIntoView({ behavior })`. Auto-downgrades to `'auto'` under `prefers-reduced-motion`. */
|
|
84
|
+
readonly kjScrollBehavior = input<ScrollBehavior>('smooth');
|
|
85
|
+
/** Forwarded to `scrollIntoView({ block })`. */
|
|
86
|
+
readonly kjScrollBlock = input<ScrollLogicalPosition>('center');
|
|
87
|
+
/** When `true` (default), calls `markAllAsTouched()` before validity check so all errors show together. */
|
|
88
|
+
readonly kjMarkAllAsTouchedOnSubmit = input(true, { transform: booleanAttribute });
|
|
89
|
+
/** When `true`, calls `form.reset()` after a successful async submit resolves. */
|
|
90
|
+
readonly kjResetOnSuccess = input(false, { transform: booleanAttribute });
|
|
91
|
+
/** Optional async submit handler. When set, the directive awaits its result and toggles `submitting`. */
|
|
92
|
+
readonly kjAsyncSubmit = input<KjAsyncSubmitHandler | undefined>(undefined);
|
|
93
|
+
|
|
94
|
+
// ── Outputs ──────────────────────────────────────────────────────────
|
|
95
|
+
/** Emits the form value when the user submits and the form is valid. */
|
|
96
|
+
readonly kjSubmit = output<unknown>();
|
|
97
|
+
/** Emits the FormGroup when the user submits but validity fails. */
|
|
98
|
+
readonly kjInvalidSubmit = output<FormGroup>();
|
|
99
|
+
|
|
100
|
+
// ── Models / state ───────────────────────────────────────────────────
|
|
101
|
+
/** Two-way bindable submitting flag. When unbound, the directive owns it internally. */
|
|
102
|
+
readonly kjSubmitting = model(false);
|
|
103
|
+
|
|
104
|
+
private readonly _submitted = signal(false);
|
|
105
|
+
private readonly _invalid = signal(false);
|
|
106
|
+
private readonly _invalidControls = signal<readonly InvalidControlInfo[]>([]);
|
|
107
|
+
private readonly _errorSummaryId = signal<string | null>(null);
|
|
108
|
+
private readonly registry = new Map<string, KjFormControlRegistration>();
|
|
109
|
+
private prefersReducedMotion = false;
|
|
110
|
+
|
|
111
|
+
// ── Public surface (KjFormContext) ───────────────────────────────────
|
|
112
|
+
readonly form = computed<FormGroup | null>(() => {
|
|
113
|
+
if (this.formGroupDir) return this.formGroupDir.form;
|
|
114
|
+
if (this.ngForm) return this.ngForm.form;
|
|
115
|
+
return null;
|
|
116
|
+
});
|
|
117
|
+
readonly submitting = computed(() => this.kjSubmitting());
|
|
118
|
+
readonly submitted = this._submitted.asReadonly();
|
|
119
|
+
readonly invalid = this._invalid.asReadonly();
|
|
120
|
+
readonly invalidControls = this._invalidControls.asReadonly();
|
|
121
|
+
readonly errorSummaryId = this._errorSummaryId.asReadonly();
|
|
122
|
+
|
|
123
|
+
constructor() {
|
|
124
|
+
afterNextRender(() => {
|
|
125
|
+
try {
|
|
126
|
+
this.prefersReducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
127
|
+
} catch {
|
|
128
|
+
this.prefersReducedMotion = false;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
registerControl(reg: KjFormControlRegistration): () => void {
|
|
134
|
+
this.registry.set(reg.path, reg);
|
|
135
|
+
return () => {
|
|
136
|
+
const cur = this.registry.get(reg.path);
|
|
137
|
+
if (cur === reg) this.registry.delete(reg.path);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Internal: called by `KjFormErrorSummary` to advertise its `id` to the form. */
|
|
142
|
+
setErrorSummaryId(id: string | null): void {
|
|
143
|
+
this._errorSummaryId.set(id);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Imperative submit — equivalent to dispatching a `submit` event on the host. */
|
|
147
|
+
submit(): void {
|
|
148
|
+
this.el.nativeElement.requestSubmit();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** @internal — host listener target. */
|
|
152
|
+
onNativeSubmit(event: Event): void {
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
// Drop submit attempts while a previous submission is still in flight.
|
|
155
|
+
if (this.submitting()) return;
|
|
156
|
+
|
|
157
|
+
const form = this.form();
|
|
158
|
+
this._submitted.set(true);
|
|
159
|
+
|
|
160
|
+
if (!form) {
|
|
161
|
+
// No Angular forms binding — log a dev-mode warning and bail out.
|
|
162
|
+
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
|
|
163
|
+
console.warn(
|
|
164
|
+
'[KjForm] <form kjForm> has no `[formGroup]` or `ngForm` binding. The directive is a coordinator over Angular Forms and needs one to function.',
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (this.kjMarkAllAsTouchedOnSubmit()) {
|
|
171
|
+
form.markAllAsTouched();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (form.invalid) {
|
|
175
|
+
this._invalid.set(true);
|
|
176
|
+
const invalidControls = collectInvalidControls(form);
|
|
177
|
+
this._invalidControls.set(this.attachLabels(invalidControls));
|
|
178
|
+
this.kjInvalidSubmit.emit(form);
|
|
179
|
+
this.handleInvalidSubmit(invalidControls);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this._invalid.set(false);
|
|
184
|
+
this._invalidControls.set([]);
|
|
185
|
+
|
|
186
|
+
const handler = this.kjAsyncSubmit();
|
|
187
|
+
if (handler) {
|
|
188
|
+
this.runAsyncSubmit(handler, form);
|
|
189
|
+
} else {
|
|
190
|
+
this.kjSubmit.emit(form.value);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Internals ────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
private attachLabels(invalid: ReadonlyArray<{ path: string; control: AbstractControl }>): InvalidControlInfo[] {
|
|
197
|
+
return invalid.map((entry) => {
|
|
198
|
+
const reg = this.registry.get(entry.path);
|
|
199
|
+
return { ...entry, label: reg?.label ?? humanizePath(entry.path) };
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private handleInvalidSubmit(invalid: ReadonlyArray<{ path: string }>): void {
|
|
204
|
+
if (!this.kjFocusOnError() || invalid.length === 0) return;
|
|
205
|
+
const first = invalid[0];
|
|
206
|
+
const reg = this.registry.get(first.path);
|
|
207
|
+
const target = reg?.element ?? this.fallbackQuery(first.path);
|
|
208
|
+
if (!target) return;
|
|
209
|
+
try {
|
|
210
|
+
target.focus({ preventScroll: !this.kjScrollOnError() });
|
|
211
|
+
} catch {
|
|
212
|
+
// Some hosts (SSR/jsdom) may not implement focus options — ignore.
|
|
213
|
+
target.focus?.();
|
|
214
|
+
}
|
|
215
|
+
if (this.kjScrollOnError()) {
|
|
216
|
+
const behavior = this.prefersReducedMotion ? 'auto' : this.kjScrollBehavior();
|
|
217
|
+
target.scrollIntoView?.({ block: this.kjScrollBlock(), behavior });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private fallbackQuery(path: string): HTMLElement | null {
|
|
222
|
+
const root = this.el.nativeElement;
|
|
223
|
+
const escaped = cssEscape(path);
|
|
224
|
+
return (
|
|
225
|
+
root.querySelector<HTMLElement>(`[formcontrolname="${escaped}"]`) ??
|
|
226
|
+
root.querySelector<HTMLElement>(`[ng-reflect-name="${escaped}"]`) ??
|
|
227
|
+
null
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private runAsyncSubmit(handler: KjAsyncSubmitHandler, form: FormGroup): void {
|
|
232
|
+
let result: ReturnType<KjAsyncSubmitHandler>;
|
|
233
|
+
this.kjSubmitting.set(true);
|
|
234
|
+
try {
|
|
235
|
+
result = handler(form.value);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
this.kjSubmitting.set(false);
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
if (result instanceof Promise) {
|
|
241
|
+
result.then(
|
|
242
|
+
() => this.afterAsync(form, true),
|
|
243
|
+
() => this.afterAsync(form, false),
|
|
244
|
+
);
|
|
245
|
+
// Also emit the synchronous (kjSubmit) for consumers that use both shapes.
|
|
246
|
+
this.kjSubmit.emit(form.value);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (isObservable(result)) {
|
|
250
|
+
const sub = result.subscribe({
|
|
251
|
+
complete: () => this.afterAsync(form, true),
|
|
252
|
+
error: () => this.afterAsync(form, false),
|
|
253
|
+
});
|
|
254
|
+
// Defensive: if the observable completes synchronously, the subscribe
|
|
255
|
+
// call already cleaned us up. Otherwise this no-op preserves cleanup
|
|
256
|
+
// behaviour (we don't track for unsubscription beyond completion).
|
|
257
|
+
void sub;
|
|
258
|
+
this.kjSubmit.emit(form.value);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// Sync handler — release the flag immediately and emit.
|
|
262
|
+
this.kjSubmitting.set(false);
|
|
263
|
+
this.kjSubmit.emit(form.value);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private afterAsync(form: FormGroup, success: boolean): void {
|
|
267
|
+
this.kjSubmitting.set(false);
|
|
268
|
+
if (success && this.kjResetOnSuccess()) {
|
|
269
|
+
form.reset();
|
|
270
|
+
this._submitted.set(false);
|
|
271
|
+
this._invalid.set(false);
|
|
272
|
+
this._invalidControls.set([]);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
25
275
|
}
|
|
26
276
|
|
|
27
|
-
|
|
28
|
-
* Label element within a form field.
|
|
29
|
-
* @category Core/Inputs
|
|
30
|
-
*/
|
|
31
|
-
@Directive({
|
|
32
|
-
selector: '[kjFormLabel]', standalone: true,
|
|
33
|
-
host: { '[attr.data-invalid]': 'ctx.invalid() ? "" : null' },
|
|
34
|
-
})
|
|
35
|
-
export class KjFormLabel { readonly ctx = inject(KJ_FORM_FIELD); }
|
|
277
|
+
// ─── Helpers ──────────────────────────────────────────────────────────
|
|
36
278
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
})
|
|
46
|
-
|
|
279
|
+
/** Walks a `FormGroup` / `FormArray` tree depth-first, returning invalid leaves. */
|
|
280
|
+
function collectInvalidControls(
|
|
281
|
+
root: AbstractControl,
|
|
282
|
+
): ReadonlyArray<{ path: string; control: AbstractControl }> {
|
|
283
|
+
const out: { path: string; control: AbstractControl }[] = [];
|
|
284
|
+
const walk = (ctrl: AbstractControl, path: string): void => {
|
|
285
|
+
if (ctrl instanceof FormGroup) {
|
|
286
|
+
for (const [name, child] of Object.entries(ctrl.controls)) {
|
|
287
|
+
walk(child, path ? `${path}.${name}` : name);
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (ctrl instanceof FormArray) {
|
|
292
|
+
ctrl.controls.forEach((child, i) => walk(child, `${path}.${i}`));
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (ctrl.invalid) {
|
|
296
|
+
out.push({ path, control: ctrl });
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
walk(root, '');
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Converts `'address.street'` → `'Address street'` for fallback labels. */
|
|
304
|
+
function humanizePath(path: string): string {
|
|
305
|
+
if (!path) return '';
|
|
306
|
+
const last = path.split('.').pop() ?? path;
|
|
307
|
+
const words = last.replace(/([A-Z])/g, ' $1').replace(/[_-]+/g, ' ').trim();
|
|
308
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Type guard for RxJS observable. Avoids a hard dep on `isObservable` in tests. */
|
|
312
|
+
function isObservable(value: unknown): value is Observable<unknown> {
|
|
313
|
+
return (
|
|
314
|
+
!!value &&
|
|
315
|
+
typeof value === 'object' &&
|
|
316
|
+
typeof (value as { subscribe?: unknown }).subscribe === 'function'
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Minimal CSS.escape polyfill for `formcontrolname` attribute selectors. */
|
|
321
|
+
function cssEscape(value: string): string {
|
|
322
|
+
if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
|
|
323
|
+
return CSS.escape(value);
|
|
324
|
+
}
|
|
325
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, (ch) => `\\${ch}`);
|
|
326
|
+
}
|
package/src/form/index.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
export { KJ_FORM_FIELD, type KjFormFieldContext } from './form.context';
|
|
1
|
+
export { KjFormField, KjFormLabel, KjFormError } from './form-field';
|
|
2
|
+
export { KJ_FORM_FIELD, type KjFormFieldContext } from './form-field.context';
|
|
3
|
+
|
|
4
|
+
export { KjForm, type KjAsyncSubmitHandler } from './form';
|
|
5
|
+
export { KjFormErrorSummary } from './form-error-summary';
|
|
6
|
+
export {
|
|
7
|
+
KJ_FORM,
|
|
8
|
+
type KjFormContext,
|
|
9
|
+
type KjFormControlRegistration,
|
|
10
|
+
type InvalidControlInfo,
|
|
11
|
+
} from './form.context';
|
package/src/icon/E2E.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Icon — E2E coverage notes
|
|
2
|
+
|
|
3
|
+
The core icon layer has no consumer-visible UI in this session (no Lucide
|
|
4
|
+
adapter, no example refactor). The directive-level integration tests in
|
|
5
|
+
`icon.directive.spec.ts` cover the end-to-end path through DI:
|
|
6
|
+
|
|
7
|
+
- Eager registration via `provideIcons` → directive renders the registered value.
|
|
8
|
+
- Async loader path → directive renders nothing while pending, then re-renders on success.
|
|
9
|
+
- Decorative vs meaningful a11y modes → axe-clean.
|
|
10
|
+
- Mode switching (svg vs font) → correct CSS variable wrapping and `data-kj-icon-mode`.
|
|
11
|
+
- Selector restriction → directive only attaches to `span` and `i` hosts; ignores other tags.
|
|
12
|
+
|
|
13
|
+
Browser-level E2E lands with the Lucide adapter follow-up, when there's a
|
|
14
|
+
visible component to drive.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* packages/core/src/icon/icon.css
|
|
2
|
+
*
|
|
3
|
+
* Renders icons set up by `KjIconDirective`. The directive writes:
|
|
4
|
+
* --kj-icon → CSS-ready value (`url("...")` or `"glyph"`)
|
|
5
|
+
* data-kj-icon-mode → "svg" | "font"
|
|
6
|
+
*
|
|
7
|
+
* SVG / CSS-mask mode (default): uses `mask-image` + `currentColor`.
|
|
8
|
+
* Works for any monochrome SVG including data: URIs.
|
|
9
|
+
* Font mode: uses pseudo-element `content`. Consumers must load the icon
|
|
10
|
+
* font and (optionally) set `--kj-icon-font` on a parent.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
.kj-icon {
|
|
14
|
+
display: inline-block;
|
|
15
|
+
width: 1em;
|
|
16
|
+
height: 1em;
|
|
17
|
+
flex-shrink: 0;
|
|
18
|
+
line-height: 1;
|
|
19
|
+
vertical-align: middle;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.kj-icon[data-kj-icon-mode='svg'] {
|
|
23
|
+
background-color: currentColor;
|
|
24
|
+
mask-image: var(--kj-icon);
|
|
25
|
+
-webkit-mask-image: var(--kj-icon);
|
|
26
|
+
mask-size: contain;
|
|
27
|
+
-webkit-mask-size: contain;
|
|
28
|
+
mask-repeat: no-repeat;
|
|
29
|
+
-webkit-mask-repeat: no-repeat;
|
|
30
|
+
mask-position: center;
|
|
31
|
+
-webkit-mask-position: center;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.kj-icon[data-kj-icon-mode='font'] {
|
|
35
|
+
font-family: var(--kj-icon-font, inherit);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.kj-icon[data-kj-icon-mode='font']::before {
|
|
39
|
+
content: var(--kj-icon);
|
|
40
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { render } from '@testing-library/angular';
|
|
2
|
+
import { axe, toHaveNoViolations } from 'jest-axe';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { KjIconDirective } from './icon.directive';
|
|
5
|
+
import { provideIcons, provideIconLoader } from './icon.providers';
|
|
6
|
+
|
|
7
|
+
expect.extend(toHaveNoViolations);
|
|
8
|
+
|
|
9
|
+
describe('KjIconDirective — selector', () => {
|
|
10
|
+
it('attaches to <i> hosts', async () => {
|
|
11
|
+
const { container } = await render(`<i [kjIcon]="'settings'"></i>`, {
|
|
12
|
+
imports: [KjIconDirective],
|
|
13
|
+
providers: [provideIcons({ settings: 'url("a")' })],
|
|
14
|
+
});
|
|
15
|
+
const host = container.querySelector('i')!;
|
|
16
|
+
expect(host.classList.contains('kj-icon')).toBe(true);
|
|
17
|
+
expect(host.getAttribute('data-kj-icon-mode')).toBe('svg');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('does NOT attach to disallowed hosts (e.g. <div>)', async () => {
|
|
21
|
+
// Static `kjIcon` attribute (no binding) — Angular only matches the
|
|
22
|
+
// directive against the host element's tag, not the attribute binding.
|
|
23
|
+
const { container } = await render(`<div kjIcon="settings"></div>`, {
|
|
24
|
+
imports: [KjIconDirective],
|
|
25
|
+
providers: [provideIcons({ settings: 'url("a")' })],
|
|
26
|
+
});
|
|
27
|
+
const host = container.querySelector('div')!;
|
|
28
|
+
expect(host.classList.contains('kj-icon')).toBe(false);
|
|
29
|
+
expect(host.hasAttribute('data-kj-icon-mode')).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('KjIconDirective — svg mode (decorative default)', () => {
|
|
34
|
+
it('writes --kj-icon and data-kj-icon-mode="svg"', async () => {
|
|
35
|
+
const { container } = await render(`<span [kjIcon]="'settings'"></span>`, {
|
|
36
|
+
imports: [KjIconDirective],
|
|
37
|
+
providers: [provideIcons({ settings: 'url("a")' })],
|
|
38
|
+
});
|
|
39
|
+
const host = container.querySelector('span')!;
|
|
40
|
+
expect(host.getAttribute('data-kj-icon-mode')).toBe('svg');
|
|
41
|
+
expect(host.style.getPropertyValue('--kj-icon')).toBe('url("a")');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('applies the .kj-icon class', async () => {
|
|
45
|
+
const { container } = await render(`<span [kjIcon]="'settings'"></span>`, {
|
|
46
|
+
imports: [KjIconDirective],
|
|
47
|
+
providers: [provideIcons({ settings: 'url("a")' })],
|
|
48
|
+
});
|
|
49
|
+
expect(container.querySelector('span')!.classList.contains('kj-icon')).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('default = decorative: aria-hidden="true", no role, no aria-label', async () => {
|
|
53
|
+
const { container } = await render(`<span [kjIcon]="'settings'"></span>`, {
|
|
54
|
+
imports: [KjIconDirective],
|
|
55
|
+
providers: [provideIcons({ settings: 'url("a")' })],
|
|
56
|
+
});
|
|
57
|
+
const host = container.querySelector('span')!;
|
|
58
|
+
expect(host.getAttribute('aria-hidden')).toBe('true');
|
|
59
|
+
expect(host.hasAttribute('role')).toBe(false);
|
|
60
|
+
expect(host.hasAttribute('aria-label')).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('KjIconDirective — meaningful mode (kjIconLabel)', () => {
|
|
65
|
+
it('removes aria-hidden, sets role="img" and aria-label', async () => {
|
|
66
|
+
const { container } = await render(
|
|
67
|
+
`<span [kjIcon]="'alert'" [kjIconLabel]="'Warning'"></span>`,
|
|
68
|
+
{
|
|
69
|
+
imports: [KjIconDirective],
|
|
70
|
+
providers: [provideIcons({ alert: 'url("a")' })],
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
const host = container.querySelector('span')!;
|
|
74
|
+
expect(host.hasAttribute('aria-hidden')).toBe(false);
|
|
75
|
+
expect(host.getAttribute('role')).toBe('img');
|
|
76
|
+
expect(host.getAttribute('aria-label')).toBe('Warning');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('KjIconDirective — color tokens', () => {
|
|
81
|
+
it('inherits color by default (no inline color style)', async () => {
|
|
82
|
+
const { container } = await render(`<span [kjIcon]="'x'"></span>`, {
|
|
83
|
+
imports: [KjIconDirective],
|
|
84
|
+
providers: [provideIcons({ x: 'url("a")' })],
|
|
85
|
+
});
|
|
86
|
+
expect(container.querySelector('span')!.getAttribute('style') ?? '').not.toContain('color:');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('writes color: var(--kj-color-icon-{token}) for non-inherit', async () => {
|
|
90
|
+
const { container } = await render(
|
|
91
|
+
`<span [kjIcon]="'x'" [kjIconColor]="'danger'"></span>`,
|
|
92
|
+
{
|
|
93
|
+
imports: [KjIconDirective],
|
|
94
|
+
providers: [provideIcons({ x: 'url("a")' })],
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
const styleAttr = container.querySelector('span')!.getAttribute('style') ?? '';
|
|
98
|
+
expect(styleAttr).toContain('color: var(--kj-color-icon-danger)');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('"inherit" color writes no inline color', async () => {
|
|
102
|
+
const { container } = await render(
|
|
103
|
+
`<span [kjIcon]="'x'" [kjIconColor]="'inherit'"></span>`,
|
|
104
|
+
{
|
|
105
|
+
imports: [KjIconDirective],
|
|
106
|
+
providers: [provideIcons({ x: 'url("a")' })],
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
expect(container.querySelector('span')!.getAttribute('style') ?? '').not.toContain('color:');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('KjIconDirective — size tokens', () => {
|
|
114
|
+
it('writes font-size: var(--kj-icon-size-{token})', async () => {
|
|
115
|
+
const { container } = await render(
|
|
116
|
+
`<span [kjIcon]="'x'" [kjIconSize]="'lg'"></span>`,
|
|
117
|
+
{
|
|
118
|
+
imports: [KjIconDirective],
|
|
119
|
+
providers: [provideIcons({ x: 'url("a")' })],
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
const styleAttr = container.querySelector('span')!.getAttribute('style') ?? '';
|
|
123
|
+
expect(styleAttr).toContain('font-size: var(--kj-icon-size-lg)');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('KjIconDirective — font mode', () => {
|
|
128
|
+
it('reflects data-kj-icon-mode="font" for @font.* names', async () => {
|
|
129
|
+
const { container } = await render(
|
|
130
|
+
`<span [kjIcon]="'@font.fa-cog'"></span>`,
|
|
131
|
+
{
|
|
132
|
+
imports: [KjIconDirective],
|
|
133
|
+
providers: [provideIcons({ '@font.fa-cog': '"\\f013"' })],
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
const host = container.querySelector('span')!;
|
|
137
|
+
expect(host.getAttribute('data-kj-icon-mode')).toBe('font');
|
|
138
|
+
expect(host.style.getPropertyValue('--kj-icon')).toBe('"\\f013"');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('KjIconDirective — loader pending', () => {
|
|
143
|
+
it('renders no --kj-icon value while async load is pending', async () => {
|
|
144
|
+
const neverResolves = () =>
|
|
145
|
+
new Promise<string>(() => {/* hangs forever */});
|
|
146
|
+
const { container } = await render(`<span [kjIcon]="'missing'"></span>`, {
|
|
147
|
+
imports: [KjIconDirective],
|
|
148
|
+
providers: [provideIconLoader(neverResolves)],
|
|
149
|
+
});
|
|
150
|
+
const host = container.querySelector('span')!;
|
|
151
|
+
expect(host.style.getPropertyValue('--kj-icon')).toBe('');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('updates --kj-icon once loader resolves', async () => {
|
|
155
|
+
let resolveLoad!: (v: string) => void;
|
|
156
|
+
const loader = (_n: string) =>
|
|
157
|
+
new Promise<string>((res) => {
|
|
158
|
+
resolveLoad = res;
|
|
159
|
+
});
|
|
160
|
+
const { container, fixture } = await render(
|
|
161
|
+
`<span [kjIcon]="'settings'"></span>`,
|
|
162
|
+
{
|
|
163
|
+
imports: [KjIconDirective],
|
|
164
|
+
providers: [provideIconLoader(loader)],
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
const host = container.querySelector('span')!;
|
|
168
|
+
expect(host.style.getPropertyValue('--kj-icon')).toBe('');
|
|
169
|
+
|
|
170
|
+
resolveLoad('url("loaded")');
|
|
171
|
+
await Promise.resolve();
|
|
172
|
+
await Promise.resolve();
|
|
173
|
+
fixture.detectChanges();
|
|
174
|
+
|
|
175
|
+
expect(host.style.getPropertyValue('--kj-icon')).toBe('url("loaded")');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('KjIconDirective — a11y audits', () => {
|
|
180
|
+
it('decorative icon passes axe', async () => {
|
|
181
|
+
const { container } = await render(`<span [kjIcon]="'settings'"></span>`, {
|
|
182
|
+
imports: [KjIconDirective],
|
|
183
|
+
providers: [provideIcons({ settings: 'url("a")' })],
|
|
184
|
+
});
|
|
185
|
+
expect(await axe(container)).toHaveNoViolations();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('meaningful icon passes axe', async () => {
|
|
189
|
+
const { container } = await render(
|
|
190
|
+
`<span [kjIcon]="'alert'" [kjIconLabel]="'Warning'"></span>`,
|
|
191
|
+
{
|
|
192
|
+
imports: [KjIconDirective],
|
|
193
|
+
providers: [provideIcons({ alert: 'url("a")' })],
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
expect(await axe(container)).toHaveNoViolations();
|
|
197
|
+
});
|
|
198
|
+
});
|