@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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { signal } from '@angular/core';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { onFocusOrInput } from './on-focus-or-input';
|
|
4
|
+
import type { KjOverlayContext } from '../../context';
|
|
5
|
+
|
|
6
|
+
describe('onFocusOrInput', () => {
|
|
7
|
+
it('opens on focusin and on input event', () => {
|
|
8
|
+
const trigger = document.createElement('input');
|
|
9
|
+
document.body.appendChild(trigger);
|
|
10
|
+
const isOpen = signal(false);
|
|
11
|
+
const ctx: KjOverlayContext = {
|
|
12
|
+
state: signal('closed'), isOpen,
|
|
13
|
+
triggerEl: signal(trigger), panelEl: signal(null),
|
|
14
|
+
stack: {} as never, platform: { isBrowser: true },
|
|
15
|
+
requestClose: () => {},
|
|
16
|
+
};
|
|
17
|
+
const toggle = vi.fn(() => isOpen.set(!isOpen()));
|
|
18
|
+
const s = onFocusOrInput();
|
|
19
|
+
s.attach(ctx);
|
|
20
|
+
s.bindToggle(toggle);
|
|
21
|
+
|
|
22
|
+
trigger.dispatchEvent(new Event('focusin'));
|
|
23
|
+
expect(toggle).toHaveBeenCalledTimes(1);
|
|
24
|
+
|
|
25
|
+
isOpen.set(false);
|
|
26
|
+
trigger.dispatchEvent(new Event('input'));
|
|
27
|
+
expect(toggle).toHaveBeenCalledTimes(2);
|
|
28
|
+
|
|
29
|
+
s.detach(); trigger.remove();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('ariaHasPopup is "listbox"', () => {
|
|
33
|
+
expect(onFocusOrInput().ariaHasPopup).toBe('listbox');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { KjOverlayContext } from '../../context';
|
|
2
|
+
import type { KjTriggerEventStrategy } from '../../tokens';
|
|
3
|
+
|
|
4
|
+
export function onFocusOrInput(): KjTriggerEventStrategy {
|
|
5
|
+
let ctx: KjOverlayContext | null = null;
|
|
6
|
+
let toggle: (() => void) | null = null;
|
|
7
|
+
let onFocusIn: ((e: Event) => void) | null = null;
|
|
8
|
+
let onInput: ((e: Event) => void) | null = null;
|
|
9
|
+
|
|
10
|
+
const wire = () => {
|
|
11
|
+
if (!ctx?.platform.isBrowser) return;
|
|
12
|
+
const trigger = ctx.triggerEl();
|
|
13
|
+
if (!trigger || onFocusIn) return;
|
|
14
|
+
onFocusIn = () => { if (!ctx?.isOpen()) toggle?.(); };
|
|
15
|
+
onInput = () => { if (!ctx?.isOpen()) toggle?.(); };
|
|
16
|
+
trigger.addEventListener('focusin', onFocusIn);
|
|
17
|
+
trigger.addEventListener('input', onInput);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
ariaHasPopup: 'listbox',
|
|
22
|
+
attach(c) { ctx = c; wire(); },
|
|
23
|
+
bindToggle(t) { toggle = t; wire(); },
|
|
24
|
+
onOpen() {}, onClose() {},
|
|
25
|
+
detach() {
|
|
26
|
+
const trigger = ctx?.triggerEl();
|
|
27
|
+
if (trigger) {
|
|
28
|
+
if (onFocusIn) trigger.removeEventListener('focusin', onFocusIn);
|
|
29
|
+
if (onInput) trigger.removeEventListener('input', onInput);
|
|
30
|
+
}
|
|
31
|
+
onFocusIn = onInput = null; toggle = null; ctx = null;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { signal } from '@angular/core';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { onFocus } from './on-focus';
|
|
4
|
+
import type { KjOverlayContext } from '../../context';
|
|
5
|
+
|
|
6
|
+
function makeCtx(trigger: HTMLElement, isOpenSig = signal(false), panel: HTMLElement | null = null): KjOverlayContext {
|
|
7
|
+
return {
|
|
8
|
+
state: signal('closed'), isOpen: isOpenSig,
|
|
9
|
+
triggerEl: signal(trigger), panelEl: signal(panel),
|
|
10
|
+
stack: {} as never, platform: { isBrowser: true },
|
|
11
|
+
requestClose: () => {},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('onFocus', () => {
|
|
16
|
+
it('focusin opens (when closed)', () => {
|
|
17
|
+
const trigger = document.createElement('button');
|
|
18
|
+
document.body.appendChild(trigger);
|
|
19
|
+
const isOpen = signal(false);
|
|
20
|
+
const ctx = makeCtx(trigger, isOpen);
|
|
21
|
+
const toggle = vi.fn(() => isOpen.set(!isOpen()));
|
|
22
|
+
const s = onFocus();
|
|
23
|
+
s.attach(ctx);
|
|
24
|
+
s.bindToggle(toggle);
|
|
25
|
+
trigger.dispatchEvent(new FocusEvent('focusin'));
|
|
26
|
+
expect(toggle).toHaveBeenCalledTimes(1);
|
|
27
|
+
s.detach();
|
|
28
|
+
trigger.remove();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('focusout closes (open) when relatedTarget outside panel', () => {
|
|
32
|
+
const trigger = document.createElement('button');
|
|
33
|
+
const panel = document.createElement('div');
|
|
34
|
+
document.body.append(trigger, panel);
|
|
35
|
+
const isOpen = signal(true);
|
|
36
|
+
const ctx = makeCtx(trigger, isOpen, panel);
|
|
37
|
+
const toggle = vi.fn(() => isOpen.set(!isOpen()));
|
|
38
|
+
const s = onFocus();
|
|
39
|
+
s.attach(ctx);
|
|
40
|
+
s.bindToggle(toggle);
|
|
41
|
+
const out = new FocusEvent('focusout', { relatedTarget: document.body });
|
|
42
|
+
trigger.dispatchEvent(out);
|
|
43
|
+
expect(toggle).toHaveBeenCalledTimes(1);
|
|
44
|
+
s.detach(); trigger.remove(); panel.remove();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('focusout to inside panel does NOT close', () => {
|
|
48
|
+
const trigger = document.createElement('button');
|
|
49
|
+
const panel = document.createElement('div');
|
|
50
|
+
const inner = document.createElement('button');
|
|
51
|
+
panel.appendChild(inner);
|
|
52
|
+
document.body.append(trigger, panel);
|
|
53
|
+
const isOpen = signal(true);
|
|
54
|
+
const ctx = makeCtx(trigger, isOpen, panel);
|
|
55
|
+
const toggle = vi.fn(() => isOpen.set(!isOpen()));
|
|
56
|
+
const s = onFocus();
|
|
57
|
+
s.attach(ctx);
|
|
58
|
+
s.bindToggle(toggle);
|
|
59
|
+
const out = new FocusEvent('focusout', { relatedTarget: inner });
|
|
60
|
+
trigger.dispatchEvent(out);
|
|
61
|
+
expect(toggle).not.toHaveBeenCalled();
|
|
62
|
+
s.detach(); trigger.remove(); panel.remove();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { KjOverlayContext } from '../../context';
|
|
2
|
+
import type { KjTriggerEventStrategy } from '../../tokens';
|
|
3
|
+
|
|
4
|
+
export function onFocus(): KjTriggerEventStrategy {
|
|
5
|
+
let ctx: KjOverlayContext | null = null;
|
|
6
|
+
let toggle: (() => void) | null = null;
|
|
7
|
+
let onIn: ((e: FocusEvent) => void) | null = null;
|
|
8
|
+
let onOut: ((e: FocusEvent) => void) | null = null;
|
|
9
|
+
|
|
10
|
+
const wire = () => {
|
|
11
|
+
if (!ctx?.platform.isBrowser) return;
|
|
12
|
+
const trigger = ctx.triggerEl();
|
|
13
|
+
if (!trigger || onIn) return;
|
|
14
|
+
onIn = () => { if (!ctx?.isOpen()) toggle?.(); };
|
|
15
|
+
onOut = (e: FocusEvent) => {
|
|
16
|
+
const related = e.relatedTarget as Node | null;
|
|
17
|
+
const panel = ctx?.panelEl();
|
|
18
|
+
if (panel && related && panel.contains(related)) return;
|
|
19
|
+
if (ctx?.isOpen()) toggle?.();
|
|
20
|
+
};
|
|
21
|
+
trigger.addEventListener('focusin', onIn);
|
|
22
|
+
trigger.addEventListener('focusout', onOut);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
ariaHasPopup: null,
|
|
27
|
+
attach(c) { ctx = c; wire(); },
|
|
28
|
+
bindToggle(t) { toggle = t; wire(); },
|
|
29
|
+
onOpen() {}, onClose() {},
|
|
30
|
+
detach() {
|
|
31
|
+
const trigger = ctx?.triggerEl();
|
|
32
|
+
if (trigger && onIn) trigger.removeEventListener('focusin', onIn);
|
|
33
|
+
if (trigger && onOut) trigger.removeEventListener('focusout', onOut);
|
|
34
|
+
onIn = onOut = null; toggle = null; ctx = null;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { onHotkey } from './on-hotkey';
|
|
3
|
+
|
|
4
|
+
describe('onHotkey', () => {
|
|
5
|
+
it('matches mod+k as ctrl+k on non-Mac and triggers toggle', () => {
|
|
6
|
+
const isMac = /Mac|iPhone|iPad|iPod/.test(navigator.platform);
|
|
7
|
+
const toggle = vi.fn();
|
|
8
|
+
const s = onHotkey('mod+k');
|
|
9
|
+
s.attach({} as never);
|
|
10
|
+
s.bindToggle(toggle);
|
|
11
|
+
|
|
12
|
+
const e = new KeyboardEvent('keydown', {
|
|
13
|
+
key: 'k',
|
|
14
|
+
ctrlKey: !isMac,
|
|
15
|
+
metaKey: isMac,
|
|
16
|
+
cancelable: true,
|
|
17
|
+
});
|
|
18
|
+
document.dispatchEvent(e);
|
|
19
|
+
expect(toggle).toHaveBeenCalledTimes(1);
|
|
20
|
+
expect(e.defaultPrevented).toBe(true);
|
|
21
|
+
s.detach();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('does not match without modifiers', () => {
|
|
25
|
+
const toggle = vi.fn();
|
|
26
|
+
const s = onHotkey('mod+k');
|
|
27
|
+
s.attach({} as never);
|
|
28
|
+
s.bindToggle(toggle);
|
|
29
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k' }));
|
|
30
|
+
expect(toggle).not.toHaveBeenCalled();
|
|
31
|
+
s.detach();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('ariaHasPopup is null', () => {
|
|
35
|
+
expect(onHotkey('mod+k').ariaHasPopup).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { KjTriggerEventStrategy } from '../../tokens';
|
|
2
|
+
|
|
3
|
+
interface ParsedChord { mod: boolean; ctrl: boolean; alt: boolean; shift: boolean; key: string; }
|
|
4
|
+
|
|
5
|
+
function parseChord(chord: string): ParsedChord {
|
|
6
|
+
const parts = chord.toLowerCase().split('+').map(p => p.trim());
|
|
7
|
+
const out: ParsedChord = { mod: false, ctrl: false, alt: false, shift: false, key: '' };
|
|
8
|
+
for (const p of parts) {
|
|
9
|
+
if (p === 'mod') out.mod = true;
|
|
10
|
+
else if (p === 'ctrl' || p === 'control') out.ctrl = true;
|
|
11
|
+
else if (p === 'alt' || p === 'option') out.alt = true;
|
|
12
|
+
else if (p === 'shift') out.shift = true;
|
|
13
|
+
else out.key = p;
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isMac(): boolean {
|
|
19
|
+
if (typeof navigator === 'undefined') return false;
|
|
20
|
+
return /Mac|iPhone|iPad|iPod/.test(navigator.platform);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function matches(e: KeyboardEvent, parsed: ParsedChord): boolean {
|
|
24
|
+
if (e.key.toLowerCase() !== parsed.key) return false;
|
|
25
|
+
const wantMeta = parsed.mod && isMac();
|
|
26
|
+
const wantCtrl = parsed.ctrl || (parsed.mod && !isMac());
|
|
27
|
+
if (!!e.metaKey !== wantMeta) return false;
|
|
28
|
+
if (!!e.ctrlKey !== wantCtrl) return false;
|
|
29
|
+
if (!!e.altKey !== parsed.alt) return false;
|
|
30
|
+
if (!!e.shiftKey !== parsed.shift) return false;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function onHotkey(chord: string): KjTriggerEventStrategy {
|
|
35
|
+
const parsed = parseChord(chord);
|
|
36
|
+
let toggle: (() => void) | null = null;
|
|
37
|
+
let listener: ((e: KeyboardEvent) => void) | null = null;
|
|
38
|
+
let installed = false;
|
|
39
|
+
|
|
40
|
+
const install = () => {
|
|
41
|
+
if (installed || typeof document === 'undefined') return;
|
|
42
|
+
listener = (e: KeyboardEvent) => {
|
|
43
|
+
if (matches(e, parsed)) { e.preventDefault(); toggle?.(); }
|
|
44
|
+
};
|
|
45
|
+
document.addEventListener('keydown', listener);
|
|
46
|
+
installed = true;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
ariaHasPopup: null,
|
|
51
|
+
attach() { install(); },
|
|
52
|
+
bindToggle(t) { toggle = t; install(); },
|
|
53
|
+
onOpen() {}, onClose() {},
|
|
54
|
+
detach() {
|
|
55
|
+
if (listener) document.removeEventListener('keydown', listener);
|
|
56
|
+
listener = null; toggle = null; installed = false;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { signal } from '@angular/core';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import { onHover } from './on-hover';
|
|
4
|
+
import type { KjOverlayContext } from '../../context';
|
|
5
|
+
|
|
6
|
+
describe('onHover', () => {
|
|
7
|
+
it('ariaHasPopup is null and strategy has lifecycle methods', () => {
|
|
8
|
+
const s = onHover();
|
|
9
|
+
expect(s.ariaHasPopup).toBeNull();
|
|
10
|
+
expect(typeof s.attach).toBe('function');
|
|
11
|
+
expect(typeof s.bindToggle).toBe('function');
|
|
12
|
+
expect(typeof s.detach).toBe('function');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('pointerenter with openDelay=0 triggers toggle synchronously after microtask', async () => {
|
|
16
|
+
const trigger = document.createElement('button');
|
|
17
|
+
const isOpen = signal(false);
|
|
18
|
+
const ctx: KjOverlayContext = {
|
|
19
|
+
state: signal('closed'), isOpen,
|
|
20
|
+
triggerEl: signal(trigger), panelEl: signal(null),
|
|
21
|
+
stack: {} as never, platform: { isBrowser: true },
|
|
22
|
+
requestClose: () => {},
|
|
23
|
+
};
|
|
24
|
+
let called = 0;
|
|
25
|
+
const toggle = () => { called++; };
|
|
26
|
+
const s = onHover({ openDelay: 0 });
|
|
27
|
+
s.attach(ctx);
|
|
28
|
+
s.bindToggle(toggle);
|
|
29
|
+
trigger.dispatchEvent(new Event('pointerenter'));
|
|
30
|
+
await new Promise(r => setTimeout(r, 5));
|
|
31
|
+
expect(called).toBe(1);
|
|
32
|
+
s.detach();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { isSignal, type Signal } from '@angular/core';
|
|
2
|
+
import type { KjOverlayContext } from '../../context';
|
|
3
|
+
import type { KjTriggerEventStrategy } from '../../tokens';
|
|
4
|
+
|
|
5
|
+
type Reactive<T> = T | Signal<T> | (() => T);
|
|
6
|
+
const read = <T>(v: Reactive<T> | undefined, fallback: T): T => {
|
|
7
|
+
if (v === undefined) return fallback;
|
|
8
|
+
if (isSignal(v)) return v();
|
|
9
|
+
if (typeof v === 'function') return (v as () => T)();
|
|
10
|
+
return v;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export interface KjOnHoverOpts {
|
|
14
|
+
openDelay?: Reactive<number>;
|
|
15
|
+
closeDelay?: Reactive<number>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type KjOnHoverStrategy = KjTriggerEventStrategy & {
|
|
19
|
+
configure(opts: Partial<KjOnHoverOpts>): void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Returns the effective hover-listening target. `pointerenter`/`pointerleave`
|
|
24
|
+
* do not bubble, and elements with `display: contents` (e.g. `<kj-button>`)
|
|
25
|
+
* never receive these events because they have no rendered box. Walk to the
|
|
26
|
+
* first descendant with a layout box.
|
|
27
|
+
*/
|
|
28
|
+
const effectiveHoverTarget = (el: HTMLElement): HTMLElement => {
|
|
29
|
+
if (typeof window === 'undefined') return el;
|
|
30
|
+
const r = el.getBoundingClientRect();
|
|
31
|
+
if (r.width > 0 || r.height > 0) return el;
|
|
32
|
+
let cur: HTMLElement | null = el;
|
|
33
|
+
while (cur) {
|
|
34
|
+
const child = cur.firstElementChild as HTMLElement | null;
|
|
35
|
+
if (!child) break;
|
|
36
|
+
const cr = child.getBoundingClientRect();
|
|
37
|
+
if (cr.width > 0 || cr.height > 0) return child;
|
|
38
|
+
cur = child;
|
|
39
|
+
}
|
|
40
|
+
return el;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function onHover(initialOpts: Partial<KjOnHoverOpts> = {}): KjOnHoverStrategy {
|
|
44
|
+
let opts: Partial<KjOnHoverOpts> = { ...initialOpts };
|
|
45
|
+
let ctx: KjOverlayContext | null = null;
|
|
46
|
+
let toggle: (() => void) | null = null;
|
|
47
|
+
let openTimer = 0, closeTimer = 0;
|
|
48
|
+
let onEnter: ((e: Event) => void) | null = null;
|
|
49
|
+
let onLeave: ((e: Event) => void) | null = null;
|
|
50
|
+
let listenTarget: HTMLElement | null = null;
|
|
51
|
+
|
|
52
|
+
const wire = () => {
|
|
53
|
+
if (!ctx?.platform.isBrowser) return;
|
|
54
|
+
const trigger = ctx.triggerEl();
|
|
55
|
+
if (!trigger || onEnter) return;
|
|
56
|
+
listenTarget = effectiveHoverTarget(trigger);
|
|
57
|
+
onEnter = () => {
|
|
58
|
+
if (closeTimer) { clearTimeout(closeTimer); closeTimer = 0; }
|
|
59
|
+
if (ctx?.isOpen()) return;
|
|
60
|
+
openTimer = setTimeout(() => { toggle?.(); openTimer = 0; }, read(opts.openDelay, 0)) as unknown as number;
|
|
61
|
+
};
|
|
62
|
+
onLeave = () => {
|
|
63
|
+
if (openTimer) { clearTimeout(openTimer); openTimer = 0; }
|
|
64
|
+
if (!ctx?.isOpen()) return;
|
|
65
|
+
closeTimer = setTimeout(() => { toggle?.(); closeTimer = 0; }, read(opts.closeDelay, 0)) as unknown as number;
|
|
66
|
+
};
|
|
67
|
+
listenTarget.addEventListener('pointerenter', onEnter);
|
|
68
|
+
listenTarget.addEventListener('pointerleave', onLeave);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
ariaHasPopup: null,
|
|
73
|
+
attach(c) { ctx = c; wire(); },
|
|
74
|
+
bindToggle(t) { toggle = t; wire(); },
|
|
75
|
+
onOpen() {}, onClose() {},
|
|
76
|
+
detach() {
|
|
77
|
+
if (listenTarget && onEnter) listenTarget.removeEventListener('pointerenter', onEnter);
|
|
78
|
+
if (listenTarget && onLeave) listenTarget.removeEventListener('pointerleave', onLeave);
|
|
79
|
+
if (openTimer) clearTimeout(openTimer);
|
|
80
|
+
if (closeTimer) clearTimeout(closeTimer);
|
|
81
|
+
onEnter = onLeave = null; listenTarget = null; openTimer = closeTimer = 0;
|
|
82
|
+
toggle = null; ctx = null;
|
|
83
|
+
},
|
|
84
|
+
configure(newOpts) { opts = { ...opts, ...newOpts }; },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { programmatic } from './programmatic';
|
|
3
|
+
|
|
4
|
+
describe('programmatic', () => {
|
|
5
|
+
it('never invokes toggle from DOM events', () => {
|
|
6
|
+
const toggle = vi.fn();
|
|
7
|
+
const s = programmatic();
|
|
8
|
+
s.attach({} as never);
|
|
9
|
+
s.bindToggle(toggle);
|
|
10
|
+
expect(toggle).not.toHaveBeenCalled();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('ariaHasPopup is null', () => {
|
|
14
|
+
expect(programmatic().ariaHasPopup).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { KjTriggerEventStrategy } from '../../tokens';
|
|
2
|
+
|
|
3
|
+
export function programmatic(): KjTriggerEventStrategy {
|
|
4
|
+
let _toggle: (() => void) | null = null;
|
|
5
|
+
return {
|
|
6
|
+
ariaHasPopup: null,
|
|
7
|
+
attach() {},
|
|
8
|
+
bindToggle(t) { _toggle = t; },
|
|
9
|
+
onOpen() {}, onClose() {},
|
|
10
|
+
detach() { _toggle = null; },
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { InjectionToken, type Signal } from '@angular/core';
|
|
2
|
+
import type { KjOverlayContext } from './context';
|
|
3
|
+
import type { KjPanelRole, KjPlacement } from './types';
|
|
4
|
+
|
|
5
|
+
/** Common shape for every overlay strategy. */
|
|
6
|
+
export interface KjStrategy {
|
|
7
|
+
attach(ctx: KjOverlayContext): void;
|
|
8
|
+
onOpen?(): void;
|
|
9
|
+
onClose?(): void;
|
|
10
|
+
detach(): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface KjMountStrategy extends KjStrategy {
|
|
14
|
+
resolveContainer(): HTMLElement;
|
|
15
|
+
readonly portalled: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface KjPositionStrategy extends KjStrategy {
|
|
19
|
+
update(): void;
|
|
20
|
+
readonly placement?: Signal<KjPlacement | null>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface KjBackdropStrategy extends KjStrategy {
|
|
24
|
+
readonly inertSiblings: boolean;
|
|
25
|
+
readonly closeOnClick: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface KjFocusTrapStrategy extends KjStrategy {
|
|
29
|
+
focusFirst(): void;
|
|
30
|
+
restoreFocus(): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
34
|
+
export interface KjScrollLockStrategy extends KjStrategy {}
|
|
35
|
+
|
|
36
|
+
export interface KjLiveAnnouncerStrategy extends KjStrategy {
|
|
37
|
+
announce(message: string): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type KjAriaHasPopup = 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | null;
|
|
41
|
+
|
|
42
|
+
export interface KjTriggerEventStrategy extends KjStrategy {
|
|
43
|
+
readonly ariaHasPopup: KjAriaHasPopup;
|
|
44
|
+
bindToggle(toggle: () => void): void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const KJ_OVERLAY_MOUNT_STRATEGY = new InjectionToken<KjMountStrategy>('KJ_OVERLAY_MOUNT_STRATEGY');
|
|
48
|
+
export const KJ_OVERLAY_POSITION_STRATEGY = new InjectionToken<KjPositionStrategy>('KJ_OVERLAY_POSITION_STRATEGY');
|
|
49
|
+
export const KJ_OVERLAY_BACKDROP_STRATEGY = new InjectionToken<KjBackdropStrategy | null>('KJ_OVERLAY_BACKDROP_STRATEGY');
|
|
50
|
+
export const KJ_OVERLAY_FOCUS_TRAP_STRATEGY = new InjectionToken<KjFocusTrapStrategy>('KJ_OVERLAY_FOCUS_TRAP_STRATEGY');
|
|
51
|
+
export const KJ_OVERLAY_SCROLL_LOCK_STRATEGY = new InjectionToken<KjScrollLockStrategy>('KJ_OVERLAY_SCROLL_LOCK_STRATEGY');
|
|
52
|
+
export const KJ_OVERLAY_LIVE_ANNOUNCER_STRATEGY = new InjectionToken<KjLiveAnnouncerStrategy>('KJ_OVERLAY_LIVE_ANNOUNCER_STRATEGY');
|
|
53
|
+
export const KJ_OVERLAY_TRIGGER_EVENT_STRATEGY = new InjectionToken<KjTriggerEventStrategy>('KJ_OVERLAY_TRIGGER_EVENT_STRATEGY');
|
|
54
|
+
export const KJ_OVERLAY_PANEL_ROLE = new InjectionToken<KjPanelRole>('KJ_OVERLAY_PANEL_ROLE');
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
|
|
2
|
+
import { render } from '@testing-library/angular';
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
import { KjOverlayTrigger } from './trigger';
|
|
5
|
+
import { KjOverlayController } from './controller';
|
|
6
|
+
import { KJ_OVERLAY_TRIGGER_EVENT_STRATEGY } from './tokens';
|
|
7
|
+
import { onClick } from './strategies/trigger-event/on-click';
|
|
8
|
+
import { bodyPortal } from './strategies/mount/body-portal';
|
|
9
|
+
import { viewportCentered } from './strategies/position/viewport-centered';
|
|
10
|
+
|
|
11
|
+
@Component({
|
|
12
|
+
selector: 'kj-trig-host',
|
|
13
|
+
standalone: true,
|
|
14
|
+
hostDirectives: [KjOverlayTrigger],
|
|
15
|
+
providers: [
|
|
16
|
+
KjOverlayController,
|
|
17
|
+
{ provide: KJ_OVERLAY_TRIGGER_EVENT_STRATEGY, useFactory: () => onClick() },
|
|
18
|
+
],
|
|
19
|
+
changeDetection: ChangeDetectionStrategy.Eager,
|
|
20
|
+
template: '',
|
|
21
|
+
})
|
|
22
|
+
class TrigHost {
|
|
23
|
+
private readonly ctrl = inject(KjOverlayController);
|
|
24
|
+
constructor() {
|
|
25
|
+
this.ctrl.attachStrategies({
|
|
26
|
+
mount: bodyPortal(),
|
|
27
|
+
position: viewportCentered(),
|
|
28
|
+
trigger: inject(KJ_OVERLAY_TRIGGER_EVENT_STRATEGY),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Component({
|
|
34
|
+
selector: 'kj-trig-menu-host',
|
|
35
|
+
standalone: true,
|
|
36
|
+
hostDirectives: [KjOverlayTrigger],
|
|
37
|
+
providers: [
|
|
38
|
+
KjOverlayController,
|
|
39
|
+
{
|
|
40
|
+
provide: KJ_OVERLAY_TRIGGER_EVENT_STRATEGY,
|
|
41
|
+
useFactory: () => ({
|
|
42
|
+
ariaHasPopup: 'menu' as const,
|
|
43
|
+
attach() {},
|
|
44
|
+
bindToggle() {},
|
|
45
|
+
onOpen() {},
|
|
46
|
+
onClose() {},
|
|
47
|
+
detach() {},
|
|
48
|
+
}),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
changeDetection: ChangeDetectionStrategy.Eager,
|
|
52
|
+
template: '',
|
|
53
|
+
})
|
|
54
|
+
class TrigMenuHost {
|
|
55
|
+
private readonly ctrl = inject(KjOverlayController);
|
|
56
|
+
constructor() {
|
|
57
|
+
this.ctrl.attachStrategies({
|
|
58
|
+
mount: bodyPortal(),
|
|
59
|
+
position: viewportCentered(),
|
|
60
|
+
trigger: inject(KJ_OVERLAY_TRIGGER_EVENT_STRATEGY),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Component({
|
|
66
|
+
selector: 'kj-trig-ctrl-host',
|
|
67
|
+
standalone: true,
|
|
68
|
+
hostDirectives: [KjOverlayTrigger],
|
|
69
|
+
providers: [
|
|
70
|
+
KjOverlayController,
|
|
71
|
+
{ provide: KJ_OVERLAY_TRIGGER_EVENT_STRATEGY, useFactory: () => onClick() },
|
|
72
|
+
],
|
|
73
|
+
changeDetection: ChangeDetectionStrategy.Eager,
|
|
74
|
+
template: '',
|
|
75
|
+
})
|
|
76
|
+
class TrigCtrlHost {
|
|
77
|
+
readonly ctrl = inject(KjOverlayController);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('KjOverlayTrigger', () => {
|
|
81
|
+
it('host has aria-haspopup and data-state attrs initially', async () => {
|
|
82
|
+
const { fixture } = await render(TrigHost);
|
|
83
|
+
const el = fixture.nativeElement as HTMLElement;
|
|
84
|
+
expect(el.getAttribute('data-state')).toBe('closed');
|
|
85
|
+
expect(el.getAttribute('aria-expanded')).toBe('false');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('exports as kjOverlayTrigger', () => {
|
|
89
|
+
expect(KjOverlayTrigger).toBeTruthy();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('controller is provided per-host', async () => {
|
|
93
|
+
const { fixture } = await render(TrigCtrlHost);
|
|
94
|
+
expect(fixture.componentInstance.ctrl).toBeTruthy();
|
|
95
|
+
expect(fixture.componentInstance.ctrl.state()).toBe('closed');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('aria-haspopup uses strategy value', async () => {
|
|
99
|
+
const { fixture } = await render(TrigMenuHost);
|
|
100
|
+
const el = fixture.nativeElement as HTMLElement;
|
|
101
|
+
expect(el.getAttribute('aria-haspopup')).toBe('menu');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Directive, ElementRef, inject, model, computed, effect, untracked } from '@angular/core';
|
|
2
|
+
import { KjOverlayController } from './controller';
|
|
3
|
+
import { KJ_OVERLAY_TRIGGER_EVENT_STRATEGY } from './tokens';
|
|
4
|
+
import type { KjOverlayPanel } from './panel';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Marks an element as the overlay trigger — owns `aria-haspopup`,
|
|
8
|
+
* `aria-expanded`, `aria-controls`, the trigger-event strategy, and the
|
|
9
|
+
* two-way `kjOpen` model that mirrors the controller's open state.
|
|
10
|
+
*
|
|
11
|
+
* @doc-category Core/Overlay
|
|
12
|
+
* @doc
|
|
13
|
+
* @doc-name overlay-trigger
|
|
14
|
+
* @doc-is-main
|
|
15
|
+
* @doc-description Marks an element as the trigger that opens and closes an overlay panel.
|
|
16
|
+
*/
|
|
17
|
+
@Directive({
|
|
18
|
+
selector: '[kjOverlayTrigger]',
|
|
19
|
+
exportAs: 'kjOverlayTrigger',
|
|
20
|
+
// NOTE: do NOT provide KjOverlayController here. Consumer trigger directives
|
|
21
|
+
// (e.g. KjSelectTrigger, KjTreeSelectTrigger) provide it on their host
|
|
22
|
+
// element so the SAME instance is visible to both KjOverlayTrigger and any
|
|
23
|
+
// sibling KjOverlayPanel via [kjFor]. For "controller-on-root" patterns
|
|
24
|
+
// (e.g. KjColorPicker), the root directive provides it and ALL descendants
|
|
25
|
+
// share one instance.
|
|
26
|
+
host: {
|
|
27
|
+
'[attr.aria-haspopup]': 'ariaHasPopup() ?? null',
|
|
28
|
+
'[attr.aria-expanded]': 'isOpen()',
|
|
29
|
+
'[attr.aria-controls]': 'panelId() ?? null',
|
|
30
|
+
'[attr.data-state]': 'state()',
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
export class KjOverlayTrigger {
|
|
34
|
+
private readonly host = inject(ElementRef<HTMLElement>);
|
|
35
|
+
/**
|
|
36
|
+
* The overlay controller for this trigger's scope. Exposed publicly so
|
|
37
|
+
* sibling `KjOverlayPanel` directives wired via `[kjFor]` can read it
|
|
38
|
+
* from their element injector (which would otherwise not see this
|
|
39
|
+
* directive's providers).
|
|
40
|
+
*/
|
|
41
|
+
readonly controller = inject(KjOverlayController);
|
|
42
|
+
private readonly triggerStrategy = inject(KJ_OVERLAY_TRIGGER_EVENT_STRATEGY);
|
|
43
|
+
|
|
44
|
+
readonly kjOpen = model<boolean>(false);
|
|
45
|
+
readonly state = this.controller.state;
|
|
46
|
+
readonly isOpen = this.controller.isOpen;
|
|
47
|
+
readonly panelId = computed(() => this.controller.panelEl()?.id ?? null);
|
|
48
|
+
readonly ariaHasPopup = computed(() => this.triggerStrategy.ariaHasPopup);
|
|
49
|
+
|
|
50
|
+
attachPanel(panel: KjOverlayPanel): void {
|
|
51
|
+
this.controller.bindPanel(panel.host.nativeElement);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
constructor() {
|
|
55
|
+
this.controller.bindTrigger(this.host.nativeElement);
|
|
56
|
+
// The trigger strategy lives on the trigger element. Attach + bind here so
|
|
57
|
+
// its DOM listeners wire up regardless of whether a panel ever attaches
|
|
58
|
+
// (e.g. tooltip with no [kjFor]).
|
|
59
|
+
this.triggerStrategy.attach(this.controller.context);
|
|
60
|
+
this.triggerStrategy.bindToggle(() => this.controller.toggle());
|
|
61
|
+
// Sync model → controller (user toggled kjOpen via two-way binding)
|
|
62
|
+
effect(() => {
|
|
63
|
+
const wantOpen = this.kjOpen();
|
|
64
|
+
const isOpen = untracked(() => this.controller.isOpen());
|
|
65
|
+
if (wantOpen && !isOpen) this.controller.open();
|
|
66
|
+
if (!wantOpen && isOpen) this.controller.close('programmatic');
|
|
67
|
+
});
|
|
68
|
+
// Sync controller → model (controller state changed via trigger event / outside-click / esc)
|
|
69
|
+
effect(() => {
|
|
70
|
+
const isOpen = this.controller.isOpen();
|
|
71
|
+
const wantOpen = untracked(() => this.kjOpen());
|
|
72
|
+
if (isOpen !== wantOpen) this.kjOpen.set(isOpen);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Side on which a floating element is placed relative to its anchor. */
|
|
2
|
+
export type KjSide = 'top' | 'bottom' | 'left' | 'right';
|
|
3
|
+
|
|
4
|
+
/** Alignment of the floating element along the cross-axis of its side. */
|
|
5
|
+
export type KjAlign = 'start' | 'center' | 'end';
|
|
6
|
+
|
|
7
|
+
/** Resolved (post-flip / post-shift) placement. */
|
|
8
|
+
export interface KjPlacement {
|
|
9
|
+
side: KjSide;
|
|
10
|
+
align: KjAlign;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Lifecycle state of an overlay. */
|
|
14
|
+
export type KjOverlayState = 'closed' | 'opening' | 'open' | 'closing';
|
|
15
|
+
|
|
16
|
+
/** ARIA role applied to the panel element. */
|
|
17
|
+
export type KjPanelRole =
|
|
18
|
+
| 'dialog' | 'alertdialog'
|
|
19
|
+
| 'tooltip'
|
|
20
|
+
| 'menu' | 'listbox' | 'tree'
|
|
21
|
+
| 'status' | 'alert';
|
|
22
|
+
|
|
23
|
+
/** Reason a close was requested. */
|
|
24
|
+
export type KjCloseReason = 'esc' | 'outside' | 'programmatic';
|