foldkit-primitives 0.15.4 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/device/fullscreen.d.ts +28 -0
- package/dist/device/fullscreen.d.ts.map +1 -0
- package/dist/device/fullscreen.js +69 -0
- package/dist/device/fullscreen.js.map +1 -0
- package/dist/device/geolocation.d.ts +56 -0
- package/dist/device/geolocation.d.ts.map +1 -0
- package/dist/device/geolocation.js +87 -0
- package/dist/device/geolocation.js.map +1 -0
- package/dist/device/index.d.ts +5 -0
- package/dist/device/index.js +6 -0
- package/dist/device/media-devices.d.ts +88 -0
- package/dist/device/media-devices.d.ts.map +1 -0
- package/dist/device/media-devices.js +117 -0
- package/dist/device/media-devices.js.map +1 -0
- package/dist/device/media-stream.d.ts +93 -0
- package/dist/device/media-stream.d.ts.map +1 -0
- package/dist/device/media-stream.js +106 -0
- package/dist/device/media-stream.js.map +1 -0
- package/dist/device/permissions.d.ts +106 -0
- package/dist/device/permissions.d.ts.map +1 -0
- package/dist/device/permissions.js +143 -0
- package/dist/device/permissions.js.map +1 -0
- package/dist/dom/autofocus.d.ts +15 -0
- package/dist/dom/autofocus.d.ts.map +1 -0
- package/dist/dom/autofocus.js +22 -0
- package/dist/dom/autofocus.js.map +1 -0
- package/dist/dom/clipboard.d.ts +30 -0
- package/dist/dom/clipboard.d.ts.map +1 -0
- package/dist/dom/clipboard.js +50 -0
- package/dist/dom/clipboard.js.map +1 -0
- package/dist/dom/editable-text.d.ts +72 -0
- package/dist/dom/editable-text.d.ts.map +1 -0
- package/dist/dom/editable-text.js +232 -0
- package/dist/dom/editable-text.js.map +1 -0
- package/dist/dom/fluid-hover.d.ts +93 -0
- package/dist/dom/fluid-hover.d.ts.map +1 -0
- package/dist/dom/fluid-hover.js +294 -0
- package/dist/dom/fluid-hover.js.map +1 -0
- package/dist/dom/focus-scope.d.ts +21 -0
- package/dist/dom/focus-scope.d.ts.map +1 -0
- package/dist/dom/focus-scope.js +93 -0
- package/dist/dom/focus-scope.js.map +1 -0
- package/dist/dom/follow-tab-stop.d.ts +11 -0
- package/dist/dom/follow-tab-stop.d.ts.map +1 -0
- package/dist/dom/follow-tab-stop.js +54 -0
- package/dist/dom/follow-tab-stop.js.map +1 -0
- package/dist/dom/image-decode.d.ts +27 -0
- package/dist/dom/image-decode.d.ts.map +1 -0
- package/dist/dom/image-decode.js +50 -0
- package/dist/dom/image-decode.js.map +1 -0
- package/dist/dom/index.d.ts +24 -0
- package/dist/dom/index.js +25 -0
- package/dist/dom/input-mask.d.ts +35 -0
- package/dist/dom/input-mask.d.ts.map +1 -0
- package/dist/dom/input-mask.js +90 -0
- package/dist/dom/input-mask.js.map +1 -0
- package/dist/dom/keep-in-view.d.ts +16 -0
- package/dist/dom/keep-in-view.d.ts.map +1 -0
- package/dist/dom/keep-in-view.js +38 -0
- package/dist/dom/keep-in-view.js.map +1 -0
- package/dist/dom/keyboard-inset.d.ts +29 -0
- package/dist/dom/keyboard-inset.d.ts.map +1 -0
- package/dist/dom/keyboard-inset.js +45 -0
- package/dist/dom/keyboard-inset.js.map +1 -0
- package/dist/dom/layers.d.ts +6 -0
- package/dist/dom/layers.d.ts.map +1 -0
- package/dist/dom/layers.js +35 -0
- package/dist/dom/layers.js.map +1 -0
- package/dist/dom/measure.d.ts +42 -0
- package/dist/dom/measure.d.ts.map +1 -0
- package/dist/dom/measure.js +123 -0
- package/dist/dom/measure.js.map +1 -0
- package/dist/dom/move.d.ts +45 -0
- package/dist/dom/move.d.ts.map +1 -0
- package/dist/dom/move.js +96 -0
- package/dist/dom/move.js.map +1 -0
- package/dist/dom/pointed-links.d.ts +13 -0
- package/dist/dom/pointed-links.d.ts.map +1 -0
- package/dist/dom/pointed-links.js +59 -0
- package/dist/dom/pointed-links.js.map +1 -0
- package/dist/dom/pointer-drag.d.ts +132 -0
- package/dist/dom/pointer-drag.d.ts.map +1 -0
- package/dist/dom/pointer-drag.js +307 -0
- package/dist/dom/pointer-drag.js.map +1 -0
- package/dist/dom/pointer-position.d.ts +133 -0
- package/dist/dom/pointer-position.d.ts.map +1 -0
- package/dist/dom/pointer-position.js +173 -0
- package/dist/dom/pointer-position.js.map +1 -0
- package/dist/dom/script-loader.d.ts +12 -0
- package/dist/dom/script-loader.d.ts.map +1 -0
- package/dist/dom/script-loader.js +54 -0
- package/dist/dom/script-loader.js.map +1 -0
- package/dist/dom/scroll-bars.d.ts +52 -0
- package/dist/dom/scroll-bars.d.ts.map +1 -0
- package/dist/dom/scroll-bars.js +259 -0
- package/dist/dom/scroll-bars.js.map +1 -0
- package/dist/dom/scroll-settle.d.ts +33 -0
- package/dist/dom/scroll-settle.d.ts.map +1 -0
- package/dist/dom/scroll-settle.js +84 -0
- package/dist/dom/scroll-settle.js.map +1 -0
- package/dist/dom/scroll.d.ts +21 -0
- package/dist/dom/scroll.d.ts.map +1 -0
- package/dist/dom/scroll.js +141 -0
- package/dist/dom/scroll.js.map +1 -0
- package/dist/dom/share.d.ts +19 -0
- package/dist/dom/share.d.ts.map +1 -0
- package/dist/dom/share.js +41 -0
- package/dist/dom/share.js.map +1 -0
- package/dist/dom/swipe.d.ts +121 -0
- package/dist/dom/swipe.d.ts.map +1 -0
- package/dist/dom/swipe.js +291 -0
- package/dist/dom/swipe.js.map +1 -0
- package/dist/dom/targets.d.ts +45 -0
- package/dist/dom/targets.d.ts.map +1 -0
- package/dist/dom/targets.js +70 -0
- package/dist/dom/targets.js.map +1 -0
- package/dist/dom/track-box.d.ts +61 -0
- package/dist/dom/track-box.d.ts.map +1 -0
- package/dist/dom/track-box.js +131 -0
- package/dist/dom/track-box.js.map +1 -0
- package/dist/events/active-element.d.ts +15 -0
- package/dist/events/active-element.d.ts.map +1 -0
- package/dist/events/active-element.js +33 -0
- package/dist/events/active-element.js.map +1 -0
- package/dist/events/hotkeys.d.ts +22 -0
- package/dist/events/hotkeys.d.ts.map +1 -0
- package/dist/events/hotkeys.js +56 -0
- package/dist/events/hotkeys.js.map +1 -0
- package/dist/events/idle.d.ts +29 -0
- package/dist/events/idle.d.ts.map +1 -0
- package/dist/events/idle.js +51 -0
- package/dist/events/idle.js.map +1 -0
- package/dist/events/index.d.ts +9 -0
- package/dist/events/index.js +10 -0
- package/dist/{input-modality-l7InESYh.d.mts → events/input-modality.d.ts} +9 -10
- package/dist/events/input-modality.d.ts.map +1 -0
- package/dist/{input-modality-XwQOFLip.mjs → events/input-modality.js} +2 -2
- package/dist/events/input-modality.js.map +1 -0
- package/dist/events/keyboard.d.ts +30 -0
- package/dist/events/keyboard.d.ts.map +1 -0
- package/dist/events/keyboard.js +55 -0
- package/dist/events/keyboard.js.map +1 -0
- package/dist/events/pointer.d.ts +15 -0
- package/dist/events/pointer.d.ts.map +1 -0
- package/dist/events/pointer.js +28 -0
- package/dist/events/pointer.js.map +1 -0
- package/dist/events/scroll.d.ts +16 -0
- package/dist/events/scroll.d.ts.map +1 -0
- package/dist/events/scroll.js +29 -0
- package/dist/events/scroll.js.map +1 -0
- package/dist/events/visibility.d.ts +25 -0
- package/dist/events/visibility.d.ts.map +1 -0
- package/dist/events/visibility.js +28 -0
- package/dist/events/visibility.js.map +1 -0
- package/dist/events/window-size.d.ts +30 -0
- package/dist/events/window-size.d.ts.map +1 -0
- package/dist/events/window-size.js +63 -0
- package/dist/events/window-size.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +10 -0
- package/dist/interaction/bundle-behavior.d.ts +37 -0
- package/dist/interaction/bundle-behavior.d.ts.map +1 -0
- package/dist/interaction/bundle-behavior.js +34 -0
- package/dist/interaction/bundle-behavior.js.map +1 -0
- package/dist/interaction/dismiss-layer.d.ts +100 -0
- package/dist/interaction/dismiss-layer.d.ts.map +1 -0
- package/dist/interaction/dismiss-layer.js +140 -0
- package/dist/interaction/dismiss-layer.js.map +1 -0
- package/dist/interaction/editable-text.d.ts +13 -0
- package/dist/interaction/editable-text.d.ts.map +1 -0
- package/dist/interaction/editable-text.js +17 -0
- package/dist/interaction/editable-text.js.map +1 -0
- package/dist/interaction/focus-scope.d.ts +21 -0
- package/dist/interaction/focus-scope.d.ts.map +1 -0
- package/dist/interaction/focus-scope.js +25 -0
- package/dist/interaction/focus-scope.js.map +1 -0
- package/dist/interaction/focus-visible.d.ts +9 -0
- package/dist/interaction/focus-visible.d.ts.map +1 -0
- package/dist/interaction/focus-visible.js +10 -0
- package/dist/interaction/focus-visible.js.map +1 -0
- package/dist/interaction/grid-navigation.d.ts +73 -0
- package/dist/interaction/grid-navigation.d.ts.map +1 -0
- package/dist/interaction/grid-navigation.js +137 -0
- package/dist/interaction/grid-navigation.js.map +1 -0
- package/dist/interaction/index.d.ts +21 -0
- package/dist/interaction/index.js +21 -0
- package/dist/interaction/layers.d.ts +12 -0
- package/dist/interaction/layers.d.ts.map +1 -0
- package/dist/interaction/layers.js +14 -0
- package/dist/interaction/layers.js.map +1 -0
- package/dist/interaction/list-navigation.d.ts +105 -0
- package/dist/interaction/list-navigation.d.ts.map +1 -0
- package/dist/interaction/list-navigation.js +168 -0
- package/dist/interaction/list-navigation.js.map +1 -0
- package/dist/interaction/live-announce.d.ts +70 -0
- package/dist/interaction/live-announce.d.ts.map +1 -0
- package/dist/interaction/live-announce.js +115 -0
- package/dist/interaction/live-announce.js.map +1 -0
- package/dist/interaction/long-press.d.ts +102 -0
- package/dist/interaction/long-press.d.ts.map +1 -0
- package/dist/interaction/long-press.js +115 -0
- package/dist/interaction/long-press.js.map +1 -0
- package/dist/interaction/move.d.ts +10 -0
- package/dist/interaction/move.d.ts.map +1 -0
- package/dist/interaction/move.js +17 -0
- package/dist/interaction/move.js.map +1 -0
- package/dist/interaction/overlay.d.ts +45 -0
- package/dist/interaction/overlay.d.ts.map +1 -0
- package/dist/interaction/overlay.js +57 -0
- package/dist/interaction/overlay.js.map +1 -0
- package/dist/interaction/placing.d.ts +86 -0
- package/dist/interaction/placing.d.ts.map +1 -0
- package/dist/interaction/placing.js +239 -0
- package/dist/interaction/placing.js.map +1 -0
- package/dist/interaction/pointer-drag.d.ts +23 -0
- package/dist/interaction/pointer-drag.d.ts.map +1 -0
- package/dist/interaction/pointer-drag.js +21 -0
- package/dist/interaction/pointer-drag.js.map +1 -0
- package/dist/interaction/press.d.ts +152 -0
- package/dist/interaction/press.d.ts.map +1 -0
- package/dist/interaction/press.js +229 -0
- package/dist/interaction/press.js.map +1 -0
- package/dist/interaction/roving-tabindex.d.ts +85 -0
- package/dist/interaction/roving-tabindex.d.ts.map +1 -0
- package/dist/interaction/roving-tabindex.js +142 -0
- package/dist/interaction/roving-tabindex.js.map +1 -0
- package/dist/interaction/selection.d.ts +72 -0
- package/dist/interaction/selection.d.ts.map +1 -0
- package/dist/interaction/selection.js +129 -0
- package/dist/interaction/selection.js.map +1 -0
- package/dist/interaction/targets.d.ts +15 -0
- package/dist/interaction/targets.d.ts.map +1 -0
- package/dist/interaction/targets.js +20 -0
- package/dist/interaction/targets.js.map +1 -0
- package/dist/interaction/tree-navigation.d.ts +111 -0
- package/dist/interaction/tree-navigation.d.ts.map +1 -0
- package/dist/interaction/tree-navigation.js +207 -0
- package/dist/interaction/tree-navigation.js.map +1 -0
- package/dist/interaction/typeahead.d.ts +73 -0
- package/dist/interaction/typeahead.d.ts.map +1 -0
- package/dist/interaction/typeahead.js +113 -0
- package/dist/interaction/typeahead.js.map +1 -0
- package/dist/internal.d.ts +16 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/{internal-C4gHodLc.mjs → internal.js} +7 -5
- package/dist/internal.js.map +1 -0
- package/dist/media/breakpoints.d.ts +37 -0
- package/dist/media/breakpoints.d.ts.map +1 -0
- package/dist/media/breakpoints.js +64 -0
- package/dist/media/breakpoints.js.map +1 -0
- package/dist/media/index.d.ts +3 -0
- package/dist/media/index.js +4 -0
- package/dist/media/mediaquery.d.ts +40 -0
- package/dist/media/mediaquery.d.ts.map +1 -0
- package/dist/media/mediaquery.js +42 -0
- package/dist/media/mediaquery.js.map +1 -0
- package/dist/media/platform.d.ts +30 -0
- package/dist/media/platform.d.ts.map +1 -0
- package/dist/media/platform.js +50 -0
- package/dist/media/platform.js.map +1 -0
- package/dist/motion/index.d.ts +5 -0
- package/dist/motion/index.js +5 -0
- package/dist/motion/motion.d.ts +16 -0
- package/dist/motion/motion.d.ts.map +1 -0
- package/dist/motion/motion.js +25 -0
- package/dist/motion/motion.js.map +1 -0
- package/dist/motion/presence.d.ts +37 -0
- package/dist/motion/presence.d.ts.map +1 -0
- package/dist/motion/presence.js +65 -0
- package/dist/motion/presence.js.map +1 -0
- package/dist/motion/spring.d.ts +50 -0
- package/dist/motion/spring.d.ts.map +1 -0
- package/dist/motion/spring.js +92 -0
- package/dist/motion/spring.js.map +1 -0
- package/dist/motion/tween.d.ts +44 -0
- package/dist/motion/tween.d.ts.map +1 -0
- package/dist/motion/tween.js +65 -0
- package/dist/motion/tween.js.map +1 -0
- package/dist/net/broadcast.d.ts +39 -0
- package/dist/net/broadcast.d.ts.map +1 -0
- package/dist/net/broadcast.js +73 -0
- package/dist/net/broadcast.js.map +1 -0
- package/dist/net/index.d.ts +5 -0
- package/dist/net/index.js +6 -0
- package/dist/net/online.d.ts +24 -0
- package/dist/net/online.d.ts.map +1 -0
- package/dist/net/online.js +26 -0
- package/dist/net/online.js.map +1 -0
- package/dist/net/sharedHost.d.ts +64 -0
- package/dist/net/sharedHost.d.ts.map +1 -0
- package/dist/net/sharedHost.js +124 -0
- package/dist/net/sharedHost.js.map +1 -0
- package/dist/net/sse.d.ts +115 -0
- package/dist/net/sse.d.ts.map +1 -0
- package/dist/net/sse.js +138 -0
- package/dist/net/sse.js.map +1 -0
- package/dist/net/websocket.d.ts +172 -0
- package/dist/net/websocket.d.ts.map +1 -0
- package/dist/net/websocket.js +199 -0
- package/dist/net/websocket.js.map +1 -0
- package/dist/observers/bounds.d.ts +24 -0
- package/dist/observers/bounds.d.ts.map +1 -0
- package/dist/observers/bounds.js +51 -0
- package/dist/observers/bounds.js.map +1 -0
- package/dist/observers/index.d.ts +4 -0
- package/dist/observers/index.js +5 -0
- package/dist/observers/intersection.d.ts +18 -0
- package/dist/observers/intersection.d.ts.map +1 -0
- package/dist/observers/intersection.js +33 -0
- package/dist/observers/intersection.js.map +1 -0
- package/dist/observers/mutation.d.ts +25 -0
- package/dist/observers/mutation.d.ts.map +1 -0
- package/dist/observers/mutation.js +53 -0
- package/dist/observers/mutation.js.map +1 -0
- package/dist/observers/resize.d.ts +18 -0
- package/dist/observers/resize.d.ts.map +1 -0
- package/dist/observers/resize.js +33 -0
- package/dist/observers/resize.js.map +1 -0
- package/dist/state/history.d.ts +115 -0
- package/dist/state/history.d.ts.map +1 -0
- package/dist/state/history.js +167 -0
- package/dist/state/history.js.map +1 -0
- package/dist/state/index.d.ts +6 -0
- package/dist/state/index.js +7 -0
- package/dist/state/locale.d.ts +27 -0
- package/dist/state/locale.d.ts.map +1 -0
- package/dist/state/locale.js +25 -0
- package/dist/state/locale.js.map +1 -0
- package/dist/state/pagination.d.ts +53 -0
- package/dist/state/pagination.d.ts.map +1 -0
- package/dist/state/pagination.js +72 -0
- package/dist/state/pagination.js.map +1 -0
- package/dist/state/range.d.ts +9 -0
- package/dist/state/range.d.ts.map +1 -0
- package/dist/state/range.js +18 -0
- package/dist/state/range.js.map +1 -0
- package/dist/state/selection.d.ts +47 -0
- package/dist/state/selection.d.ts.map +1 -0
- package/dist/state/selection.js +39 -0
- package/dist/state/selection.js.map +1 -0
- package/dist/state/virtual.d.ts +201 -0
- package/dist/state/virtual.d.ts.map +1 -0
- package/dist/state/{index.mjs → virtual.js} +10 -306
- package/dist/state/virtual.js.map +1 -0
- package/dist/time/debounce.d.ts +43 -0
- package/dist/time/debounce.d.ts.map +1 -0
- package/dist/time/debounce.js +65 -0
- package/dist/time/debounce.js.map +1 -0
- package/dist/time/index.d.ts +6 -0
- package/dist/time/index.js +7 -0
- package/dist/time/interval.d.ts +37 -0
- package/dist/time/interval.d.ts.map +1 -0
- package/dist/time/interval.js +52 -0
- package/dist/time/interval.js.map +1 -0
- package/dist/time/relative.d.ts +8 -0
- package/dist/time/relative.d.ts.map +1 -0
- package/dist/time/relative.js +27 -0
- package/dist/time/relative.js.map +1 -0
- package/dist/time/throttle.d.ts +38 -0
- package/dist/time/throttle.d.ts.map +1 -0
- package/dist/time/throttle.js +39 -0
- package/dist/time/throttle.js.map +1 -0
- package/dist/time/ticks.d.ts +21 -0
- package/dist/time/ticks.d.ts.map +1 -0
- package/dist/time/ticks.js +30 -0
- package/dist/time/ticks.js.map +1 -0
- package/dist/time/timer.d.ts +32 -0
- package/dist/time/timer.d.ts.map +1 -0
- package/dist/time/timer.js +50 -0
- package/dist/time/timer.js.map +1 -0
- package/package.json +31 -31
- package/dist/device/index.d.mts +0 -335
- package/dist/device/index.d.mts.map +0 -1
- package/dist/device/index.mjs +0 -493
- package/dist/device/index.mjs.map +0 -1
- package/dist/dom/index.d.mts +0 -714
- package/dist/dom/index.d.mts.map +0 -1
- package/dist/dom/index.mjs +0 -1840
- package/dist/dom/index.mjs.map +0 -1
- package/dist/editable-text-Bg7vil7h.d.mts +0 -307
- package/dist/editable-text-Bg7vil7h.d.mts.map +0 -1
- package/dist/events/index.d.mts +0 -168
- package/dist/events/index.d.mts.map +0 -1
- package/dist/events/index.mjs +0 -307
- package/dist/events/index.mjs.map +0 -1
- package/dist/follow-tab-stop-DM5EI4Ci.mjs +0 -856
- package/dist/follow-tab-stop-DM5EI4Ci.mjs.map +0 -1
- package/dist/index-Bomdwb7e.d.mts +0 -142
- package/dist/index-Bomdwb7e.d.mts.map +0 -1
- package/dist/index.d.mts +0 -12
- package/dist/index.mjs +0 -12
- package/dist/input-modality-XwQOFLip.mjs.map +0 -1
- package/dist/input-modality-l7InESYh.d.mts.map +0 -1
- package/dist/interaction/index.d.mts +0 -1193
- package/dist/interaction/index.d.mts.map +0 -1
- package/dist/interaction/index.mjs +0 -1992
- package/dist/interaction/index.mjs.map +0 -1
- package/dist/internal-C4gHodLc.mjs.map +0 -1
- package/dist/media/index.d.mts +0 -103
- package/dist/media/index.d.mts.map +0 -1
- package/dist/media/index.mjs +0 -146
- package/dist/media/index.mjs.map +0 -1
- package/dist/motion/index.d.mts +0 -2
- package/dist/motion/index.mjs +0 -1
- package/dist/motion-BmlJCjn6.mjs +0 -236
- package/dist/motion-BmlJCjn6.mjs.map +0 -1
- package/dist/net/index.d.mts +0 -338
- package/dist/net/index.d.mts.map +0 -1
- package/dist/net/index.mjs +0 -530
- package/dist/net/index.mjs.map +0 -1
- package/dist/observers/index.d.mts +0 -76
- package/dist/observers/index.d.mts.map +0 -1
- package/dist/observers/index.mjs +0 -155
- package/dist/observers/index.mjs.map +0 -1
- package/dist/rolldown-runtime-D8jQpDG7.mjs +0 -12
- package/dist/state/index.d.mts +0 -432
- package/dist/state/index.d.mts.map +0 -1
- package/dist/state/index.mjs.map +0 -1
- package/dist/time/index.d.mts +0 -167
- package/dist/time/index.d.mts.map +0 -1
- package/dist/time/index.mjs +0 -236
- package/dist/time/index.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roving-tabindex.js","names":[],"sources":["../../src/interaction/roving-tabindex.ts"],"sourcesContent":["/**\n * One tab stop for a set of items (a toolbar, a tab list, a listbox): arrows\n * move focus between them, the rest stay out of the tab order. The current\n * item's id is the Model slice, so a reorder keeps the same item current and a\n * resumed page knows where focus was. Which item comes next is a pure\n * function of the items and the key; the Behavior wires it to the slots.\n */\nimport { Option, Schema } from 'effect'\nimport type { Attribute, HtmlBuilder, KeyboardModifiers } from 'foldkit/html'\nimport { defineMessageUnion } from 'foldkit/message'\nimport { Bundle } from 'foldkit-bundle'\nimport { Behavior, Behaviors, Capability, type SlotItem } from 'foldkit-mixins'\nimport type { Declared } from 'foldkit-bundle'\nimport { perInput } from '../internal.js'\n\nexport const Orientation = Schema.Literals(['vertical', 'horizontal', 'both'])\nexport type Orientation = typeof Orientation.Type\n\nexport const Model = Schema.Struct({\n /** The current item's id, or `null` before any item has been focused. */\n current: Schema.NullOr(Schema.String),\n})\nexport type Model = typeof Model.Type\n\nexport const Message = defineMessageUnion({\n /** An item became current: the user focused it, or a key moved there. */\n Focused: { id: Schema.String },\n})\nexport type Message = typeof Message.Type\n\nexport const Args = Schema.Struct({\n orientation: Orientation,\n /** Wrap from the last enabled item to the first, and back. */\n loop: Schema.Boolean,\n /** Keep DOM focus on the container and point at the current item with\n * `aria-activedescendant` instead of moving focus. */\n virtual: Schema.Boolean,\n})\nexport type Args = typeof Args.Type\n\nexport const bundle = Bundle.make('RovingTabindex', {\n Model,\n Message,\n args: Args,\n init: () => ({ model: { current: null } }),\n // Moving focus by key dispatches `Focused`, and the item's `OnFocus` reports it again.\n update: (model, message) =>\n model.current === message.id ? { model } : { model: { current: message.id } },\n})\n\nexport type Direction = 'ltr' | 'rtl'\n\nexport interface MoveOptions {\n readonly orientation: Orientation\n readonly loop: boolean\n readonly direction: Direction\n /** How many enabled items PageUp and PageDown move; absent, they are not handled. */\n readonly page?: number\n}\n\nconst isPlain = (modifiers: KeyboardModifiers): boolean =>\n !modifiers.ctrlKey && !modifiers.altKey && !modifiers.metaKey\n\n/**\n * The index the key moves to from `current`, over the enabled indices only, or\n * `undefined` when the key is not a navigation key here. `current` may be\n * `-1` (nothing current yet): arrows then land on the first or last enabled\n * item. Under `rtl`, left and right swap. PageUp and PageDown move by `page`\n * and clamp at the ends, never wrapping.\n */\nexport const move = (\n enabled: ReadonlyArray<number>,\n current: number,\n key: string,\n modifiers: KeyboardModifiers,\n options: MoveOptions,\n): number | undefined => {\n if (enabled.length === 0 || !isPlain(modifiers)) return undefined\n const first = enabled[0]!\n const last = enabled[enabled.length - 1]!\n if (key === 'Home') return first\n if (key === 'End') return last\n if (options.page !== undefined && (key === 'PageUp' || key === 'PageDown')) {\n const position = enabled.indexOf(current)\n if (position === -1) return key === 'PageDown' ? first : last\n const jumped = position + (key === 'PageDown' ? options.page : -options.page)\n return enabled[Math.min(enabled.length - 1, Math.max(0, jumped))]\n }\n const vertical = options.orientation !== 'horizontal'\n const horizontal = options.orientation !== 'vertical'\n const forwardKey = options.direction === 'rtl' ? 'ArrowLeft' : 'ArrowRight'\n const backwardKey = options.direction === 'rtl' ? 'ArrowRight' : 'ArrowLeft'\n const step =\n (vertical && key === 'ArrowDown') || (horizontal && key === forwardKey)\n ? 1\n : (vertical && key === 'ArrowUp') || (horizontal && key === backwardKey)\n ? -1\n : 0\n if (step === 0) return undefined\n const position = enabled.indexOf(current)\n if (position === -1) return step === 1 ? first : last\n const next = position + step\n if (next < 0) return options.loop ? last : first\n if (next >= enabled.length) return options.loop ? first : last\n return enabled[next]\n}\n\n/** The item that holds the tab stop: the current one, else the first enabled. */\nexport const tabStop = (\n items: Behaviors.Collection.Items<unknown>,\n current: string | null,\n): number => {\n const index = current === null ? -1 : items.indexOf(current)\n if (index !== -1 && !items.isDisabled(index)) return index\n return items.enabled[0] ?? -1\n}\n\nexport interface BehaviorOptions<Input, Slots> {\n /** The slot that receives the arrow keys. */\n readonly container: keyof Slots & string\n /** The slot rendered once per item; must carry the item's id (see `Behaviors.Collection`). */\n readonly item: keyof Slots & string\n readonly items: (input: Input) => Behaviors.Collection.Items<unknown>\n /** Default `'ltr'`; return `'rtl'` to swap left and right. */\n readonly direction?: (input: Input) => Direction\n}\n\n/** A selector for an element by id, safe for any id. */\nexport const idSelector = (id: string): string => `[id=\"${id.replace(/[\"\\\\]/g, '\\\\$&')}\"]`\n\n/**\n * The attributes one item gets: `tabindex` 0 when it holds the tab stop and\n * -1 otherwise (none under `virtual`), and `OnFocus` reporting it current.\n * Shared with `ListNavigation`.\n */\nexport const itemAttributes = <ParentMessage>(\n h: HtmlBuilder<ParentMessage>,\n items: Behaviors.Collection.Items<unknown>,\n current: string | null,\n item: SlotItem,\n virtual: boolean,\n focused: (id: string) => ParentMessage,\n): ReadonlyArray<Attribute<ParentMessage>> => {\n const id = item.id ?? items.ids[item.index]\n if (id === undefined) return []\n const isStop = tabStop(items, current) === item.index\n return [...(virtual ? [] : [h.Tabindex(isStop ? 0 : -1)]), h.OnFocus(focused(id))]\n}\n\n/**\n * Wires a placed `RovingTabindex` to the slots: the container's arrow, Home\n * and End keys focus the next item (synchronously, then dispatch), each item\n * gets `tabindex` 0 or -1 and reports `Focused` when focused. Under `virtual`\n * DOM focus never leaves the container: the key only dispatches, the\n * container carries `aria-activedescendant`, and items keep no `tabindex`.\n *\n * Every handled key is default-prevented. Nothing is written on dispose: the\n * attributes are data, so a view that no longer attaches this leaves no\n * `tabindex` behind.\n */\nexport const behavior =\n <Field extends string>(declared: Declared<typeof bundle, Field>, args: Args) =>\n <Slots>(slots: Slots) =>\n <Input extends { readonly [K in Field]: Model }, ParentMessage>(\n options: BehaviorOptions<Input, Slots>,\n ): Behavior.NamedBehavior<Slots, Input, ParentMessage> => {\n const wrap = (id: string): ParentMessage =>\n declared.wrapper.make(Message.Focused({ id })) as unknown as ParentMessage\n const slice = (input: Input): Model => input[declared.field]\n const itemsOf = perInput(options.items)\n return Behavior.forSlots(slots)<Input, ParentMessage>(\n {\n [options.container]: Behavior.slot({\n requires: { capability: Capability.Interactive },\n attributes: ({\n input,\n h,\n }: {\n readonly input: Input\n readonly h: HtmlBuilder<ParentMessage>\n }) => {\n const items = itemsOf(input)\n const current = slice(input).current\n const from = current === null ? -1 : items.indexOf(current)\n const direction = options.direction?.(input) ?? 'ltr'\n const stop = tabStop(items, current)\n const stopId = stop === -1 ? undefined : items.ids[stop]\n const target = (key: string, modifiers: KeyboardModifiers): string | undefined => {\n const next = move(items.enabled, from, key, modifiers, { ...args, direction })\n return next === undefined ? undefined : items.ids[next]\n }\n // Virtual: DOM focus stays on the container, only the pointer moves.\n if (args.virtual) {\n return [\n h.OnKeyDownPreventDefault((key, modifiers) =>\n Option.map(Option.fromNullishOr(target(key, modifiers)), wrap),\n ),\n ...(stopId === undefined ? [] : [h.AriaActiveDescendant(stopId)]),\n ]\n }\n return [\n h.OnKeyDownFocus((key, modifiers) =>\n Option.map(Option.fromNullishOr(target(key, modifiers)), id => ({\n focusSelector: idSelector(id),\n message: wrap(id),\n })),\n ),\n ]\n },\n }),\n [options.item]: Behavior.slot({\n requires: { capability: Capability.Focusable },\n attributes: ({\n input,\n h,\n item,\n }: {\n readonly input: Input\n readonly h: HtmlBuilder<ParentMessage>\n readonly item?: SlotItem\n }) => {\n if (item === undefined) return []\n return itemAttributes(h, itemsOf(input), slice(input).current, item, args.virtual, wrap)\n },\n }),\n // Keyed by values the caller chose; `forSlots` checks both keys exist.\n } as unknown as Behavior.BehaviorSpec<Slots, Input, ParentMessage>,\n { name: 'RovingTabindex' },\n )\n }\n"],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { Bundle, Declared } from "foldkit-bundle";
|
|
3
|
+
import { Behavior, Behaviors } from "foldkit-mixins";
|
|
4
|
+
export declare const Mode: Schema.Literals<readonly ["single", "multiple", "none"]>;
|
|
5
|
+
export type Mode = typeof Mode.Type;
|
|
6
|
+
export declare const Model: Schema.Struct<{
|
|
7
|
+
/** In selection order. */
|
|
8
|
+
readonly selected: Schema.$Array<Schema.String>;
|
|
9
|
+
/** Where the last plain selection happened; a range extends from it. */
|
|
10
|
+
readonly anchor: Schema.NullOr<Schema.String>;
|
|
11
|
+
}>;
|
|
12
|
+
export type Model = typeof Model.Type;
|
|
13
|
+
export declare const Message: import("foldkit/message").MessageUnion<{
|
|
14
|
+
/** A plain activation: replaces in single mode, toggles in multiple mode. */
|
|
15
|
+
readonly Activated: {
|
|
16
|
+
id: Schema.String;
|
|
17
|
+
};
|
|
18
|
+
/** A Shift activation: selects everything between the anchor and `id` in `order`. */
|
|
19
|
+
readonly Ranged: {
|
|
20
|
+
id: Schema.String;
|
|
21
|
+
order: Schema.$Array<Schema.String>;
|
|
22
|
+
};
|
|
23
|
+
readonly Replaced: {
|
|
24
|
+
ids: Schema.$Array<Schema.String>;
|
|
25
|
+
};
|
|
26
|
+
readonly Cleared: {};
|
|
27
|
+
}>;
|
|
28
|
+
export type Message = typeof Message.Type;
|
|
29
|
+
export declare const Args: Schema.Struct<{
|
|
30
|
+
readonly mode: Schema.Literals<readonly ["single", "multiple", "none"]>;
|
|
31
|
+
/** In single mode, whether activating the selected item deselects it. */
|
|
32
|
+
readonly allowEmpty: Schema.Boolean;
|
|
33
|
+
}>;
|
|
34
|
+
export type Args = typeof Args.Type;
|
|
35
|
+
/** The ids between `from` and `to` in `order`, inclusive, in `order`'s order. */
|
|
36
|
+
export declare const between: (order: ReadonlyArray<string>, from: string | null, to: string) => ReadonlyArray<string>;
|
|
37
|
+
export declare const bundle: Bundle.Bundle<"Selection", {
|
|
38
|
+
readonly mode: "multiple" | "none" | "single";
|
|
39
|
+
readonly allowEmpty: boolean;
|
|
40
|
+
}, {
|
|
41
|
+
readonly selected: readonly string[];
|
|
42
|
+
readonly anchor: string | null;
|
|
43
|
+
}, {
|
|
44
|
+
readonly _tag: "Activated";
|
|
45
|
+
readonly id: string;
|
|
46
|
+
} | {
|
|
47
|
+
readonly _tag: "Cleared";
|
|
48
|
+
} | {
|
|
49
|
+
readonly _tag: "Ranged";
|
|
50
|
+
readonly id: string;
|
|
51
|
+
readonly order: readonly string[];
|
|
52
|
+
} | {
|
|
53
|
+
readonly _tag: "Replaced";
|
|
54
|
+
readonly ids: readonly string[];
|
|
55
|
+
}, never, never, never, void, {}, {}>;
|
|
56
|
+
export interface BehaviorOptions<Input, Slots> {
|
|
57
|
+
/** The slot holding the items, for `aria-multiselectable`. */
|
|
58
|
+
readonly container?: keyof Slots & string;
|
|
59
|
+
/** The slot rendered once per item; must carry the item's id. */
|
|
60
|
+
readonly item: keyof Slots & string;
|
|
61
|
+
readonly items: (input: Input) => Behaviors.Collection.Items<unknown>;
|
|
62
|
+
/** Wire a plain click on each item to `Activated`. Default `true`; turn off
|
|
63
|
+
* when `Press` or the view owns the click. */
|
|
64
|
+
readonly click?: boolean;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Writes `aria-selected` on each item from the placed slice, and
|
|
68
|
+
* `aria-multiselectable` on the container in multiple mode; wires `OnClick`
|
|
69
|
+
* to `Activated` unless told not to. A disabled item gets no click.
|
|
70
|
+
*/
|
|
71
|
+
export declare const behavior: <Field extends string>(declared: Declared<typeof bundle, Field>, args: Args) => <Slots>(slots: Slots) => <Input extends { readonly [K in Field]: Model; }, ParentMessage>(options: BehaviorOptions<Input, Slots>) => Behavior.NamedBehavior<Slots, Input, ParentMessage>;
|
|
72
|
+
//# sourceMappingURL=selection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selection.d.ts","names":[],"sources":["../../src/interaction/selection.ts"],"mappings":";;;qBAgBa,MAAI,OAAA;YACL,cAAc,KAAK;qBAElB,OAAK,OAAA;;;;;;YAMN,eAAe,MAAM;qBAEpB,mCAAO;;;IAEL,IAAE,OAAA;;;;IAEL,IAAE,OAAA;IAAiB,OAAK,OAAA,OAAA,OAAA;;;IACtB,KAAG,OAAA,OAAA,OAAA;;;;YAGL,iBAAiB,QAAQ;qBAExB,MAAI,OAAA;;;;;YAKL,cAAc,KAAK;;qBAKlB,UAAO,OACX,uBAAqB,qBACT,eAElB;qBAQU,QAAM,OAAA;;;;;;;;;;;;;;;;;;;iBA8CF,gBAAgB,OAAO;;WAE7B,kBAAkB;;WAElB,YAAY;WACZ,QAAQ,OAAO,UAAU,UAAU,WAAW;;;WAG9C;;;;;;;qBAQE,WACV,sBAAoB,UAAY,gBAAgB,QAAQ,QAAM,MAAQ,UACtE,OAAK,OAAS,WACd,0BAA0B,KAAK,QAAQ,UAAS,eAAa,SACnD,gBAAgB,OAAO,WAC/B,SAAS,cAAc,OAAO,OAAO"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { defineMessageUnion } from "foldkit/message";
|
|
3
|
+
import { Bundle } from "foldkit-bundle";
|
|
4
|
+
import { perInput, sameIds } from "../internal.js";
|
|
5
|
+
import { Capability } from "foldkit-mixins";
|
|
6
|
+
import { BundleBehavior } from "./bundle-behavior.js";
|
|
7
|
+
// src/interaction/selection.ts
|
|
8
|
+
/**
|
|
9
|
+
* Selection over a set of items: which ids are selected, and the anchor a
|
|
10
|
+
* range extends from. `mode` is `'single'` (a click replaces), `'multiple'`
|
|
11
|
+
* (a click toggles), or `'none'`. Single mode keeps one item selected unless
|
|
12
|
+
* `allowEmpty`. A range needs the items' order, which the view knows and the
|
|
13
|
+
* Bundle does not, so `Ranged` carries it. The Behavior says the state in
|
|
14
|
+
* ARIA and wires a plain click; a Shift range comes from `Press`, whose
|
|
15
|
+
* `Pressed` carries `shiftKey`.
|
|
16
|
+
*/
|
|
17
|
+
const Mode = Schema.Literals([
|
|
18
|
+
"single",
|
|
19
|
+
"multiple",
|
|
20
|
+
"none"
|
|
21
|
+
]);
|
|
22
|
+
const Model = Schema.Struct({
|
|
23
|
+
/** In selection order. */
|
|
24
|
+
selected: Schema.Array(Schema.String),
|
|
25
|
+
/** Where the last plain selection happened; a range extends from it. */
|
|
26
|
+
anchor: Schema.NullOr(Schema.String)
|
|
27
|
+
});
|
|
28
|
+
const Message = defineMessageUnion({
|
|
29
|
+
/** A plain activation: replaces in single mode, toggles in multiple mode. */
|
|
30
|
+
Activated: { id: Schema.String },
|
|
31
|
+
/** A Shift activation: selects everything between the anchor and `id` in `order`. */
|
|
32
|
+
Ranged: {
|
|
33
|
+
id: Schema.String,
|
|
34
|
+
order: Schema.Array(Schema.String)
|
|
35
|
+
},
|
|
36
|
+
Replaced: { ids: Schema.Array(Schema.String) },
|
|
37
|
+
Cleared: {}
|
|
38
|
+
});
|
|
39
|
+
const Args = Schema.Struct({
|
|
40
|
+
mode: Mode,
|
|
41
|
+
/** In single mode, whether activating the selected item deselects it. */
|
|
42
|
+
allowEmpty: Schema.Boolean
|
|
43
|
+
});
|
|
44
|
+
const without = (ids, id) => ids.filter((other) => other !== id);
|
|
45
|
+
/** The ids between `from` and `to` in `order`, inclusive, in `order`'s order. */
|
|
46
|
+
const between = (order, from, to) => {
|
|
47
|
+
const end = order.indexOf(to);
|
|
48
|
+
if (end === -1) return [];
|
|
49
|
+
const start = from === null ? -1 : order.indexOf(from);
|
|
50
|
+
if (start === -1) return [to];
|
|
51
|
+
return order.slice(Math.min(start, end), Math.max(start, end) + 1);
|
|
52
|
+
};
|
|
53
|
+
const bundle = Bundle.make("Selection", {
|
|
54
|
+
Model,
|
|
55
|
+
Message,
|
|
56
|
+
args: Args,
|
|
57
|
+
init: () => ({ model: {
|
|
58
|
+
selected: [],
|
|
59
|
+
anchor: null
|
|
60
|
+
} }),
|
|
61
|
+
update: (model, message, args) => Message.match(message, {
|
|
62
|
+
Activated: ({ id }) => {
|
|
63
|
+
if (args.mode === "none") return { model };
|
|
64
|
+
const isSelected = model.selected.includes(id);
|
|
65
|
+
if (args.mode === "single") return isSelected ? args.allowEmpty ? { model: {
|
|
66
|
+
selected: [],
|
|
67
|
+
anchor: id
|
|
68
|
+
} } : { model } : { model: {
|
|
69
|
+
selected: [id],
|
|
70
|
+
anchor: id
|
|
71
|
+
} };
|
|
72
|
+
return { model: {
|
|
73
|
+
selected: isSelected ? without(model.selected, id) : [...model.selected, id],
|
|
74
|
+
anchor: id
|
|
75
|
+
} };
|
|
76
|
+
},
|
|
77
|
+
Ranged: ({ id, order }) => {
|
|
78
|
+
if (args.mode !== "multiple") return { model };
|
|
79
|
+
const range = between(order, model.anchor, id);
|
|
80
|
+
const inRange = new Set(range);
|
|
81
|
+
const selected = [...model.selected.filter((other) => !inRange.has(other)), ...range];
|
|
82
|
+
const anchor = model.anchor ?? id;
|
|
83
|
+
return sameIds(selected, model.selected) && anchor === model.anchor ? { model } : { model: {
|
|
84
|
+
selected,
|
|
85
|
+
anchor
|
|
86
|
+
} };
|
|
87
|
+
},
|
|
88
|
+
Replaced: ({ ids }) => {
|
|
89
|
+
const selected = args.mode === "none" ? [] : [...new Set(ids)];
|
|
90
|
+
return sameIds(selected, model.selected) ? { model } : { model: {
|
|
91
|
+
selected,
|
|
92
|
+
anchor: model.anchor
|
|
93
|
+
} };
|
|
94
|
+
},
|
|
95
|
+
Cleared: () => model.selected.length === 0 ? { model } : { model: {
|
|
96
|
+
selected: [],
|
|
97
|
+
anchor: model.anchor
|
|
98
|
+
} }
|
|
99
|
+
})
|
|
100
|
+
});
|
|
101
|
+
/**
|
|
102
|
+
* Writes `aria-selected` on each item from the placed slice, and
|
|
103
|
+
* `aria-multiselectable` on the container in multiple mode; wires `OnClick`
|
|
104
|
+
* to `Activated` unless told not to. A disabled item gets no click.
|
|
105
|
+
*/
|
|
106
|
+
const behavior = (declared, args) => (slots) => (options) => {
|
|
107
|
+
const itemsOf = perInput(options.items);
|
|
108
|
+
const selectedOf = perInput((input) => new Set(input[declared.field].selected));
|
|
109
|
+
return BundleBehavior.fromBundle(declared)(slots)({
|
|
110
|
+
...options.container === void 0 ? {} : { [options.container]: {
|
|
111
|
+
requires: { capability: Capability.Container },
|
|
112
|
+
attributes: ({ h }) => [h.AriaMultiSelectable(args.mode === "multiple")]
|
|
113
|
+
} },
|
|
114
|
+
[options.item]: {
|
|
115
|
+
requires: { capability: Capability.Interactive },
|
|
116
|
+
attributes: ({ input, h, item, send }) => {
|
|
117
|
+
if (item === void 0) return [];
|
|
118
|
+
const items = itemsOf(input);
|
|
119
|
+
const id = item.id ?? items.ids[item.index];
|
|
120
|
+
if (id === void 0) return [];
|
|
121
|
+
const clickable = options.click !== false && args.mode !== "none" && !items.isDisabled(item.index);
|
|
122
|
+
return [h.AriaSelected(selectedOf(input).has(id)), ...clickable ? [h.OnClick(send(Message.Activated({ id })))] : []];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}, { name: "Selection" });
|
|
126
|
+
};
|
|
127
|
+
export { Args, Message, Mode, Model, behavior, between, bundle };
|
|
128
|
+
|
|
129
|
+
//# sourceMappingURL=selection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selection.js","names":[],"sources":["../../src/interaction/selection.ts"],"sourcesContent":["/**\n * Selection over a set of items: which ids are selected, and the anchor a\n * range extends from. `mode` is `'single'` (a click replaces), `'multiple'`\n * (a click toggles), or `'none'`. Single mode keeps one item selected unless\n * `allowEmpty`. A range needs the items' order, which the view knows and the\n * Bundle does not, so `Ranged` carries it. The Behavior says the state in\n * ARIA and wires a plain click; a Shift range comes from `Press`, whose\n * `Pressed` carries `shiftKey`.\n */\nimport { Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport { Bundle, type Declared } from 'foldkit-bundle'\nimport { Behavior, Behaviors, Capability } from 'foldkit-mixins'\nimport { perInput, sameIds } from '../internal.js'\nimport { BundleBehavior } from './bundle-behavior.js'\n\nexport const Mode = Schema.Literals(['single', 'multiple', 'none'])\nexport type Mode = typeof Mode.Type\n\nexport const Model = Schema.Struct({\n /** In selection order. */\n selected: Schema.Array(Schema.String),\n /** Where the last plain selection happened; a range extends from it. */\n anchor: Schema.NullOr(Schema.String),\n})\nexport type Model = typeof Model.Type\n\nexport const Message = defineMessageUnion({\n /** A plain activation: replaces in single mode, toggles in multiple mode. */\n Activated: { id: Schema.String },\n /** A Shift activation: selects everything between the anchor and `id` in `order`. */\n Ranged: { id: Schema.String, order: Schema.Array(Schema.String) },\n Replaced: { ids: Schema.Array(Schema.String) },\n Cleared: {},\n})\nexport type Message = typeof Message.Type\n\nexport const Args = Schema.Struct({\n mode: Mode,\n /** In single mode, whether activating the selected item deselects it. */\n allowEmpty: Schema.Boolean,\n})\nexport type Args = typeof Args.Type\n\nconst without = (ids: ReadonlyArray<string>, id: string) => ids.filter(other => other !== id)\n\n/** The ids between `from` and `to` in `order`, inclusive, in `order`'s order. */\nexport const between = (\n order: ReadonlyArray<string>,\n from: string | null,\n to: string,\n): ReadonlyArray<string> => {\n const end = order.indexOf(to)\n if (end === -1) return []\n const start = from === null ? -1 : order.indexOf(from)\n if (start === -1) return [to]\n return order.slice(Math.min(start, end), Math.max(start, end) + 1)\n}\n\nexport const bundle = Bundle.make('Selection', {\n Model,\n Message,\n args: Args,\n init: () => ({ model: { selected: [], anchor: null } }),\n update: (model, message, args) =>\n Message.match(message, {\n Activated: ({ id }) => {\n if (args.mode === 'none') return { model }\n const isSelected = model.selected.includes(id)\n if (args.mode === 'single') {\n return isSelected\n ? args.allowEmpty\n ? { model: { selected: [], anchor: id } }\n : { model }\n : { model: { selected: [id], anchor: id } }\n }\n return {\n model: {\n selected: isSelected ? without(model.selected, id) : [...model.selected, id],\n anchor: id,\n },\n }\n },\n Ranged: ({ id, order }) => {\n if (args.mode !== 'multiple') return { model }\n const range = between(order, model.anchor, id)\n const inRange = new Set(range)\n const selected = [...model.selected.filter(other => !inRange.has(other)), ...range]\n const anchor = model.anchor ?? id\n // Shift-clicking the same item twice ranges over what is selected already.\n return sameIds(selected, model.selected) && anchor === model.anchor\n ? { model }\n : { model: { selected, anchor } }\n },\n Replaced: ({ ids }) => {\n const selected = args.mode === 'none' ? [] : [...new Set(ids)]\n return sameIds(selected, model.selected)\n ? { model }\n : { model: { selected, anchor: model.anchor } }\n },\n Cleared: () =>\n model.selected.length === 0 ? { model } : { model: { selected: [], anchor: model.anchor } },\n }),\n})\n\nexport interface BehaviorOptions<Input, Slots> {\n /** The slot holding the items, for `aria-multiselectable`. */\n readonly container?: keyof Slots & string\n /** The slot rendered once per item; must carry the item's id. */\n readonly item: keyof Slots & string\n readonly items: (input: Input) => Behaviors.Collection.Items<unknown>\n /** Wire a plain click on each item to `Activated`. Default `true`; turn off\n * when `Press` or the view owns the click. */\n readonly click?: boolean\n}\n\n/**\n * Writes `aria-selected` on each item from the placed slice, and\n * `aria-multiselectable` on the container in multiple mode; wires `OnClick`\n * to `Activated` unless told not to. A disabled item gets no click.\n */\nexport const behavior =\n <Field extends string>(declared: Declared<typeof bundle, Field>, args: Args) =>\n <Slots>(slots: Slots) =>\n <Input extends { readonly [K in Field]: Model }, ParentMessage>(\n options: BehaviorOptions<Input, Slots>,\n ): Behavior.NamedBehavior<Slots, Input, ParentMessage> => {\n const itemsOf = perInput(options.items)\n const selectedOf = perInput((input: Input) => new Set(input[declared.field].selected))\n return BundleBehavior.fromBundle(declared)(slots)<Input, ParentMessage>(\n {\n ...(options.container === undefined\n ? {}\n : {\n [options.container]: {\n requires: { capability: Capability.Container },\n attributes: ({ h }) => [h.AriaMultiSelectable(args.mode === 'multiple')],\n },\n }),\n [options.item]: {\n requires: { capability: Capability.Interactive },\n attributes: ({ input, h, item, send }) => {\n if (item === undefined) return []\n const items = itemsOf(input)\n const id = item.id ?? items.ids[item.index]\n if (id === undefined) return []\n const clickable =\n options.click !== false && args.mode !== 'none' && !items.isDisabled(item.index)\n return [\n h.AriaSelected(selectedOf(input).has(id)),\n ...(clickable ? [h.OnClick(send(Message.Activated({ id })))] : []),\n ]\n },\n },\n },\n { name: 'Selection' },\n )\n }\n"],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { TargetFact, TargetFact as TargetFact$1, TargetHovered, TargetPressed, Targets as mount, targetOf } from "../dom/targets.js";
|
|
2
|
+
import { Behavior } from "foldkit-mixins";
|
|
3
|
+
export interface BehaviorOptions<Slots, ParentMessage> {
|
|
4
|
+
/** The slot holding the marked descendants. */
|
|
5
|
+
readonly container: keyof Slots & string;
|
|
6
|
+
/** The attribute that marks a descendant, holding its id. */
|
|
7
|
+
readonly attribute: string;
|
|
8
|
+
/** Prevent a press's default, such as a link navigating. Default `false`. */
|
|
9
|
+
readonly preventDefault?: boolean;
|
|
10
|
+
/** Turns each fact into the view's Message. */
|
|
11
|
+
readonly toMessage: (fact: TargetFact$1) => ParentMessage;
|
|
12
|
+
}
|
|
13
|
+
export declare const behavior: <Slots>(slots: Slots) => <Input, ParentMessage>(options: BehaviorOptions<Slots, ParentMessage>) => Behavior.NamedBehavior<Slots, Input, ParentMessage>;
|
|
14
|
+
export { type TargetFact, TargetHovered, TargetPressed, mount, targetOf };
|
|
15
|
+
//# sourceMappingURL=targets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"targets.d.ts","names":["TargetFact"],"sources":["../../src/interaction/targets.ts"],"mappings":";;iBAkBiB,gBAAgB,OAAO;;WAE7B,iBAAiB;;WAEjB;;WAEA;;WAEA,YAAY,MAAMA,iBAAe;;qBAG/B,WACV,OAAK,OAAS,WACd,OAAO,eAAa,SACV,gBAAgB,OAAO,mBAC/B,SAAS,cAAc,OAAO,OAAO"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as Mount from "foldkit/mount";
|
|
2
|
+
import { TargetHovered, TargetPressed, Targets, Targets as mount, targetOf } from "../dom/targets.js";
|
|
3
|
+
import { Behavior, Capability } from "foldkit-mixins";
|
|
4
|
+
// src/interaction/targets.ts
|
|
5
|
+
/**
|
|
6
|
+
* `Targets` as a Behavior: attaches the `foldkit-primitives/dom` Mount to a
|
|
7
|
+
* container slot and maps what it reports into the view's Messages. No Bundle:
|
|
8
|
+
* which descendant is under the pointer, or was pressed, is already a fact, and
|
|
9
|
+
* what it means is the parent's `update`.
|
|
10
|
+
*/
|
|
11
|
+
const behavior = (slots) => (options) => Behavior.forSlots(slots)({ [options.container]: Behavior.slot({
|
|
12
|
+
requires: { capability: Capability.Container },
|
|
13
|
+
mount: () => Mount.mapMessage(Targets({
|
|
14
|
+
attribute: options.attribute,
|
|
15
|
+
preventDefault: options.preventDefault ?? false
|
|
16
|
+
}), options.toMessage)
|
|
17
|
+
}) }, { name: "Targets" });
|
|
18
|
+
export { TargetHovered, TargetPressed, behavior, mount, targetOf };
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=targets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"targets.js","names":["TargetsMount"],"sources":["../../src/interaction/targets.ts"],"sourcesContent":["/**\n * `Targets` as a Behavior: attaches the `foldkit-primitives/dom` Mount to a\n * container slot and maps what it reports into the view's Messages. No Bundle:\n * which descendant is under the pointer, or was pressed, is already a fact, and\n * what it means is the parent's `update`.\n */\nimport * as Mount from 'foldkit/mount'\nimport { Behavior, Capability } from 'foldkit-mixins'\nimport { Targets as TargetsMount, type TargetFact } from '../dom/targets.js'\n\nexport {\n TargetHovered,\n TargetPressed,\n Targets as mount,\n targetOf,\n type TargetFact,\n} from '../dom/targets.js'\n\nexport interface BehaviorOptions<Slots, ParentMessage> {\n /** The slot holding the marked descendants. */\n readonly container: keyof Slots & string\n /** The attribute that marks a descendant, holding its id. */\n readonly attribute: string\n /** Prevent a press's default, such as a link navigating. Default `false`. */\n readonly preventDefault?: boolean\n /** Turns each fact into the view's Message. */\n readonly toMessage: (fact: TargetFact) => ParentMessage\n}\n\nexport const behavior =\n <Slots>(slots: Slots) =>\n <Input, ParentMessage>(\n options: BehaviorOptions<Slots, ParentMessage>,\n ): Behavior.NamedBehavior<Slots, Input, ParentMessage> =>\n Behavior.forSlots(slots)<Input, ParentMessage>(\n {\n [options.container]: Behavior.slot({\n requires: { capability: Capability.Container },\n mount: () =>\n Mount.mapMessage(\n TargetsMount({\n attribute: options.attribute,\n preventDefault: options.preventDefault ?? false,\n }),\n options.toMessage,\n ),\n }),\n // Keyed by a value the caller chose; `forSlots` checks the key exists.\n } as unknown as Behavior.BehaviorSpec<Slots, Input, ParentMessage>,\n { name: 'Targets' },\n )\n"],"mappings":";;;;;;;;;;AA6BA,MAAa,YACH,WAEN,YAEA,SAAS,SAAS,KAAK,CAAC,CACtB,GACG,QAAQ,YAAY,SAAS,KAAK;CACjC,UAAU,EAAE,YAAY,WAAW,UAAU;CAC7C,aACE,MAAM,WACJA,QAAa;EACX,WAAW,QAAQ;EACnB,gBAAgB,QAAQ,kBAAkB;CAC5C,CAAC,GACD,QAAQ,SACV;AACJ,CAAC,EAEH,GACA,EAAE,MAAM,UAAU,CACpB"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { Bundle, Declared } from "foldkit-bundle";
|
|
3
|
+
import { Behavior } from "foldkit-mixins";
|
|
4
|
+
import { KeyboardModifiers } from "foldkit/html";
|
|
5
|
+
export declare const Model: Schema.Struct<{
|
|
6
|
+
/** The current row's id, or `null` before any row has been focused. */
|
|
7
|
+
readonly current: Schema.NullOr<Schema.String>;
|
|
8
|
+
/** The rows whose open state differs from `openByDefault`. */
|
|
9
|
+
readonly toggled: Schema.$Array<Schema.String>;
|
|
10
|
+
}>;
|
|
11
|
+
export type Model = typeof Model.Type;
|
|
12
|
+
export declare const Message: import("foldkit/message").MessageUnion<{
|
|
13
|
+
/** A row became current: the user focused it, or a key moved there. */
|
|
14
|
+
readonly Focused: {
|
|
15
|
+
id: Schema.String;
|
|
16
|
+
};
|
|
17
|
+
readonly Opened: {
|
|
18
|
+
id: Schema.String;
|
|
19
|
+
};
|
|
20
|
+
readonly Closed: {
|
|
21
|
+
id: Schema.String;
|
|
22
|
+
};
|
|
23
|
+
}>;
|
|
24
|
+
export type Message = typeof Message.Type;
|
|
25
|
+
export declare const Args: Schema.Struct<{
|
|
26
|
+
/** Whether a row with children starts open. */
|
|
27
|
+
readonly openByDefault: Schema.Boolean;
|
|
28
|
+
}>;
|
|
29
|
+
export type Args = typeof Args.Type;
|
|
30
|
+
/** Whether a row is open, given the default and what was toggled. */
|
|
31
|
+
export declare const isOpen: (model: Model, args: Args, id: string) => boolean;
|
|
32
|
+
export declare const bundle: Bundle.Bundle<"TreeNavigation", {
|
|
33
|
+
readonly openByDefault: boolean;
|
|
34
|
+
}, {
|
|
35
|
+
readonly current: string | null;
|
|
36
|
+
readonly toggled: readonly string[];
|
|
37
|
+
}, {
|
|
38
|
+
readonly _tag: "Closed";
|
|
39
|
+
readonly id: string;
|
|
40
|
+
} | {
|
|
41
|
+
readonly _tag: "Focused";
|
|
42
|
+
readonly id: string;
|
|
43
|
+
} | {
|
|
44
|
+
readonly _tag: "Opened";
|
|
45
|
+
readonly id: string;
|
|
46
|
+
}, never, never, never, void, {}, {}>;
|
|
47
|
+
/** One row of the tree, in tree order: a parent comes before its children. */
|
|
48
|
+
export interface Row {
|
|
49
|
+
readonly id: string;
|
|
50
|
+
/** The parent's id, or `null` for a top-level row. */
|
|
51
|
+
readonly parent: string | null;
|
|
52
|
+
/** Whether it has children, so it can be opened and closed. */
|
|
53
|
+
readonly branch: boolean;
|
|
54
|
+
readonly disabled?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/** A row as it is showing: where it sits, for its ARIA attributes. */
|
|
57
|
+
export interface Shown extends Row {
|
|
58
|
+
/** 1 for a top-level row. */
|
|
59
|
+
readonly level: number;
|
|
60
|
+
/** Its place among its siblings, from 1, and how many there are. */
|
|
61
|
+
readonly position: number;
|
|
62
|
+
readonly siblings: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The rows that are showing: every row whose ancestors are all open, with its
|
|
66
|
+
* level and its place among its siblings. A row whose parent is not a row is
|
|
67
|
+
* treated as top-level.
|
|
68
|
+
*/
|
|
69
|
+
export declare const shown: (rows: ReadonlyArray<Row>, model: Model, args: Args) => ReadonlyArray<Shown>;
|
|
70
|
+
/** What a key does: move to a row, or open or close one. */
|
|
71
|
+
export type Action = {
|
|
72
|
+
readonly _tag: 'Focus';
|
|
73
|
+
readonly id: string;
|
|
74
|
+
} | {
|
|
75
|
+
readonly _tag: 'Open';
|
|
76
|
+
readonly id: string;
|
|
77
|
+
} | {
|
|
78
|
+
readonly _tag: 'Close';
|
|
79
|
+
readonly id: string;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* What a key does from the current row, over the rows that are showing, or
|
|
83
|
+
* `undefined` when it is not a key the tree handles. Disabled rows are stepped
|
|
84
|
+
* over. With nothing current, Down and Home land on the first row, Up and End
|
|
85
|
+
* on the last. Right and Left swap under `rtl`.
|
|
86
|
+
*/
|
|
87
|
+
export declare const move: (rows: ReadonlyArray<Shown>, current: string | null, key: string, modifiers: KeyboardModifiers, model: Model, args: Args, direction?: 'ltr' | 'rtl') => Action | undefined;
|
|
88
|
+
/** The row that holds the tab stop: the current one while it is showing, else the first. */
|
|
89
|
+
export declare const tabStop: (rows: ReadonlyArray<Shown>, current: string | null) => string | undefined;
|
|
90
|
+
export interface BehaviorOptions<Input, Slots> {
|
|
91
|
+
/** The slot that receives the keys: the element with `role="tree"`. */
|
|
92
|
+
readonly container: keyof Slots & string;
|
|
93
|
+
/** The slot rendered once per showing row, with `item.id` or `item.index` into `shown`. */
|
|
94
|
+
readonly item: keyof Slots & string;
|
|
95
|
+
/** Every row, open or not, in tree order. */
|
|
96
|
+
readonly rows: (input: Input) => ReadonlyArray<Row>;
|
|
97
|
+
/** The DOM id a row's element carries. Default: the row's own id. */
|
|
98
|
+
readonly domId?: (id: string) => string;
|
|
99
|
+
readonly direction?: (input: Input) => 'ltr' | 'rtl';
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Wires a placed `TreeNavigation` to the slots. The container's keys move focus
|
|
103
|
+
* to the row they pick, or open or close the current row with focus left where
|
|
104
|
+
* it is. Each showing row gets its id, `role="treeitem"`, `aria-level` (and
|
|
105
|
+
* the same level as `--fk-tree-level`, so one `calc` indents any depth),
|
|
106
|
+
* `aria-posinset`, `aria-setsize`, `aria-expanded` when it has children,
|
|
107
|
+
* `aria-disabled` when disabled, a roving `tabindex`, and `OnFocus` reporting
|
|
108
|
+
* it current. Every handled key is default-prevented.
|
|
109
|
+
*/
|
|
110
|
+
export declare const behavior: <Field extends string>(declared: Declared<typeof bundle, Field>, args: Args) => <Slots>(slots: Slots) => <Input extends { readonly [K in Field]: Model; }, ParentMessage>(options: BehaviorOptions<Input, Slots>) => Behavior.NamedBehavior<Slots, Input, ParentMessage>;
|
|
111
|
+
//# sourceMappingURL=tree-navigation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree-navigation.d.ts","names":[],"sources":["../../src/interaction/tree-navigation.ts"],"mappings":";;;;qBAyBa,OAAK,OAAA;;;;;;YAMN,eAAe,MAAM;qBAEpB,mCAAO;;;IAEP,IAAE,OAAA;;;IACH,IAAE,OAAA;;;IACF,IAAE,OAAA;;;YAEF,iBAAiB,QAAQ;qBAExB,MAAI,OAAA;;;;YAIL,cAAc,KAAK;;qBAGlB,SAAM,OAAW,OAAK,MAAQ,MAAI;qBAalC,QAAM,OAAA;;;;;;;;;;;;;;;;iBAqBF;WACN;;WAEA;;WAEA;WACA;;;iBAIM,cAAc;;WAEpB;;WAEA;WACA;;;;;;;qBAQE,QAAK,MAAU,cAAc,MAAI,OAAS,OAAK,MAAQ,SAAO,cAAc;;YAiC7E;WACG;WAAwB;;WACxB;WAAuB;;WACvB;WAAwB;;;;;;;;qBAW1B,OAAI,MACT,cAAc,QAAM,wBACJ,aACX,WACA,mBAAiB,OACrB,OAAK,MACN,MAAI,8BAET;;qBA6BU,UAAO,MAAU,cAAc,QAAM;iBAKjC,gBAAgB,OAAO;;WAE7B,iBAAiB;;WAEjB,YAAY;;WAEZ,OAAO,OAAO,UAAU,cAAc;;WAEtC,SAAS;WACT,aAAa,OAAO;;;;;;;;;;;qBAqBlB,WACV,sBAAoB,UAAY,gBAAgB,QAAQ,QAAM,MAAQ,UACtE,OAAK,OAAS,WACd,0BAA0B,KAAK,QAAQ,UAAS,eAAa,SACnD,gBAAgB,OAAO,WAC/B,SAAS,cAAc,OAAO,OAAO"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Option, Schema } from "effect";
|
|
2
|
+
import { defineMessageUnion } from "foldkit/message";
|
|
3
|
+
import { Bundle } from "foldkit-bundle";
|
|
4
|
+
import { perInput } from "../internal.js";
|
|
5
|
+
import { FollowTabStop } from "../dom/follow-tab-stop.js";
|
|
6
|
+
import { Behavior, Capability } from "foldkit-mixins";
|
|
7
|
+
import { idSelector } from "./roving-tabindex.js";
|
|
8
|
+
// src/interaction/tree-navigation.ts
|
|
9
|
+
/**
|
|
10
|
+
* Keyboard navigation of a tree (a file explorer, a page's layers, a nested
|
|
11
|
+
* menu), after the WAI-ARIA tree pattern: one tab stop, Up and Down through the
|
|
12
|
+
* rows that are showing, Right to open a row or step into it, Left to close it
|
|
13
|
+
* or step out to its parent, Home and End to the first and last row.
|
|
14
|
+
*
|
|
15
|
+
* The Model slice is the current row's id and which rows are open. Openness is
|
|
16
|
+
* stored as the rows `toggled` away from a default, so a layers panel that
|
|
17
|
+
* starts with everything open and a file tree that starts closed are the same
|
|
18
|
+
* bundle with a different `openByDefault`, and neither stores every row.
|
|
19
|
+
*
|
|
20
|
+
* Which rows exist is the view's to say, each with its parent, in tree order.
|
|
21
|
+
* What a key does is a pure function of those rows and the Model; the Behavior
|
|
22
|
+
* wires it to the slots and writes the tree's ARIA attributes.
|
|
23
|
+
*/
|
|
24
|
+
const Model = Schema.Struct({
|
|
25
|
+
/** The current row's id, or `null` before any row has been focused. */
|
|
26
|
+
current: Schema.NullOr(Schema.String),
|
|
27
|
+
/** The rows whose open state differs from `openByDefault`. */
|
|
28
|
+
toggled: Schema.Array(Schema.String)
|
|
29
|
+
});
|
|
30
|
+
const Message = defineMessageUnion({
|
|
31
|
+
/** A row became current: the user focused it, or a key moved there. */
|
|
32
|
+
Focused: { id: Schema.String },
|
|
33
|
+
Opened: { id: Schema.String },
|
|
34
|
+
Closed: { id: Schema.String }
|
|
35
|
+
});
|
|
36
|
+
const Args = Schema.Struct({
|
|
37
|
+
/** Whether a row with children starts open. */
|
|
38
|
+
openByDefault: Schema.Boolean });
|
|
39
|
+
/** Whether a row is open, given the default and what was toggled. */
|
|
40
|
+
const isOpen = (model, args, id) => args.openByDefault !== model.toggled.includes(id);
|
|
41
|
+
const setOpen = (model, args, id, open) => isOpen(model, args, id) === open ? model : {
|
|
42
|
+
...model,
|
|
43
|
+
toggled: model.toggled.includes(id) ? model.toggled.filter((toggled) => toggled !== id) : [...model.toggled, id]
|
|
44
|
+
};
|
|
45
|
+
const bundle = Bundle.make("TreeNavigation", {
|
|
46
|
+
Model,
|
|
47
|
+
Message,
|
|
48
|
+
args: Args,
|
|
49
|
+
init: () => ({ model: {
|
|
50
|
+
current: null,
|
|
51
|
+
toggled: []
|
|
52
|
+
} }),
|
|
53
|
+
update: (model, message, args) => {
|
|
54
|
+
switch (message._tag) {
|
|
55
|
+
case "Focused": return model.current === message.id ? { model } : { model: {
|
|
56
|
+
...model,
|
|
57
|
+
current: message.id
|
|
58
|
+
} };
|
|
59
|
+
case "Opened": return { model: setOpen(model, args, message.id, true) };
|
|
60
|
+
case "Closed": return { model: setOpen(model, args, message.id, false) };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
/**
|
|
65
|
+
* The rows that are showing: every row whose ancestors are all open, with its
|
|
66
|
+
* level and its place among its siblings. A row whose parent is not a row is
|
|
67
|
+
* treated as top-level.
|
|
68
|
+
*/
|
|
69
|
+
const shown = (rows, model, args) => {
|
|
70
|
+
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
71
|
+
const siblingsOf = /* @__PURE__ */ new Map();
|
|
72
|
+
const positionOf = /* @__PURE__ */ new Map();
|
|
73
|
+
for (const row of rows) {
|
|
74
|
+
const parent = row.parent !== null && byId.has(row.parent) ? row.parent : null;
|
|
75
|
+
const count = (siblingsOf.get(parent) ?? 0) + 1;
|
|
76
|
+
siblingsOf.set(parent, count);
|
|
77
|
+
positionOf.set(row.id, count);
|
|
78
|
+
}
|
|
79
|
+
const levels = /* @__PURE__ */ new Map();
|
|
80
|
+
const showing = /* @__PURE__ */ new Map();
|
|
81
|
+
const result = [];
|
|
82
|
+
for (const row of rows) {
|
|
83
|
+
const parent = row.parent !== null && byId.has(row.parent) ? row.parent : null;
|
|
84
|
+
const level = parent === null ? 1 : (levels.get(parent) ?? 0) + 1;
|
|
85
|
+
levels.set(row.id, level);
|
|
86
|
+
const visible = parent === null || showing.get(parent) === true && isOpen(model, args, parent);
|
|
87
|
+
showing.set(row.id, visible);
|
|
88
|
+
if (!visible) continue;
|
|
89
|
+
result.push({
|
|
90
|
+
...row,
|
|
91
|
+
level,
|
|
92
|
+
position: positionOf.get(row.id) ?? 1,
|
|
93
|
+
siblings: siblingsOf.get(parent) ?? 1
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
};
|
|
98
|
+
const isPlain = (modifiers) => !modifiers.ctrlKey && !modifiers.altKey && !modifiers.metaKey;
|
|
99
|
+
/**
|
|
100
|
+
* What a key does from the current row, over the rows that are showing, or
|
|
101
|
+
* `undefined` when it is not a key the tree handles. Disabled rows are stepped
|
|
102
|
+
* over. With nothing current, Down and Home land on the first row, Up and End
|
|
103
|
+
* on the last. Right and Left swap under `rtl`.
|
|
104
|
+
*/
|
|
105
|
+
const move = (rows, current, key, modifiers, model, args, direction = "ltr") => {
|
|
106
|
+
const enabled = rows.filter((row) => row.disabled !== true);
|
|
107
|
+
if (enabled.length === 0 || !isPlain(modifiers)) return void 0;
|
|
108
|
+
const focus = (row) => row === void 0 ? void 0 : {
|
|
109
|
+
_tag: "Focus",
|
|
110
|
+
id: row.id
|
|
111
|
+
};
|
|
112
|
+
const at = enabled.findIndex((row) => row.id === current);
|
|
113
|
+
const inward = direction === "rtl" ? "ArrowLeft" : "ArrowRight";
|
|
114
|
+
const outward = direction === "rtl" ? "ArrowRight" : "ArrowLeft";
|
|
115
|
+
if (key === "Home") return focus(enabled[0]);
|
|
116
|
+
if (key === "End") return focus(enabled[enabled.length - 1]);
|
|
117
|
+
if (key === "ArrowDown") return focus(at === -1 ? enabled[0] : enabled[Math.min(at + 1, enabled.length - 1)]);
|
|
118
|
+
if (key === "ArrowUp") return focus(at === -1 ? enabled[enabled.length - 1] : enabled[Math.max(at - 1, 0)]);
|
|
119
|
+
const row = enabled[at];
|
|
120
|
+
if (row === void 0) return key === inward || key === outward ? focus(enabled[0]) : void 0;
|
|
121
|
+
if (key === inward) {
|
|
122
|
+
if (!row.branch) return void 0;
|
|
123
|
+
if (!isOpen(model, args, row.id)) return {
|
|
124
|
+
_tag: "Open",
|
|
125
|
+
id: row.id
|
|
126
|
+
};
|
|
127
|
+
return focus(enabled.find((child) => child.parent === row.id));
|
|
128
|
+
}
|
|
129
|
+
if (key === outward) {
|
|
130
|
+
if (row.branch && isOpen(model, args, row.id)) return {
|
|
131
|
+
_tag: "Close",
|
|
132
|
+
id: row.id
|
|
133
|
+
};
|
|
134
|
+
return focus(enabled.find((parent) => parent.id === row.parent));
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
/** The row that holds the tab stop: the current one while it is showing, else the first. */
|
|
138
|
+
const tabStop = (rows, current) => {
|
|
139
|
+
return (rows.find((row) => row.id === current && row.disabled !== true) ?? rows.find((row) => row.disabled !== true))?.id;
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* Wires a placed `TreeNavigation` to the slots. The container's keys move focus
|
|
143
|
+
* to the row they pick, or open or close the current row with focus left where
|
|
144
|
+
* it is. Each showing row gets its id, `role="treeitem"`, `aria-level` (and
|
|
145
|
+
* the same level as `--fk-tree-level`, so one `calc` indents any depth),
|
|
146
|
+
* `aria-posinset`, `aria-setsize`, `aria-expanded` when it has children,
|
|
147
|
+
* `aria-disabled` when disabled, a roving `tabindex`, and `OnFocus` reporting
|
|
148
|
+
* it current. Every handled key is default-prevented.
|
|
149
|
+
*/
|
|
150
|
+
const behavior = (declared, args) => (slots) => (options) => {
|
|
151
|
+
const wrap = (message) => declared.wrapper.make(message);
|
|
152
|
+
const slice = (input) => input[declared.field];
|
|
153
|
+
const domId = options.domId ?? ((id) => id);
|
|
154
|
+
const prepare = perInput((input) => {
|
|
155
|
+
const model = slice(input);
|
|
156
|
+
const rows = shown(options.rows(input), model, args);
|
|
157
|
+
return {
|
|
158
|
+
model,
|
|
159
|
+
rows,
|
|
160
|
+
byId: new Map(rows.map((row) => [row.id, row])),
|
|
161
|
+
stop: tabStop(rows, model.current)
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
return Behavior.forSlots(slots)({
|
|
165
|
+
[options.container]: Behavior.slot({
|
|
166
|
+
requires: { capability: Capability.Interactive },
|
|
167
|
+
mount: () => FollowTabStop(),
|
|
168
|
+
attributes: ({ input, h }) => {
|
|
169
|
+
const { model, rows } = prepare(input);
|
|
170
|
+
const direction = options.direction?.(input) ?? "ltr";
|
|
171
|
+
return [h.OnKeyDownFocus((key, modifiers) => {
|
|
172
|
+
const action = move(rows, model.current, key, modifiers, model, args, direction);
|
|
173
|
+
if (action === void 0) return Option.none();
|
|
174
|
+
const message = action._tag === "Focus" ? Message.Focused({ id: action.id }) : action._tag === "Open" ? Message.Opened({ id: action.id }) : Message.Closed({ id: action.id });
|
|
175
|
+
return Option.some({
|
|
176
|
+
focusSelector: idSelector(domId(action.id)),
|
|
177
|
+
message: wrap(message)
|
|
178
|
+
});
|
|
179
|
+
})];
|
|
180
|
+
}
|
|
181
|
+
}),
|
|
182
|
+
[options.item]: Behavior.slot({
|
|
183
|
+
requires: { capability: Capability.Focusable },
|
|
184
|
+
attributes: ({ input, h, item }) => {
|
|
185
|
+
if (item === void 0) return [];
|
|
186
|
+
const { model, rows, byId, stop } = prepare(input);
|
|
187
|
+
const row = item.id === void 0 ? rows[item.index] : byId.get(item.id);
|
|
188
|
+
if (row === void 0) return [];
|
|
189
|
+
return [
|
|
190
|
+
h.Id(domId(row.id)),
|
|
191
|
+
h.Role("treeitem"),
|
|
192
|
+
h.AriaLevel(row.level),
|
|
193
|
+
h.Style({ "--fk-tree-level": String(row.level) }),
|
|
194
|
+
h.AriaPosinset(row.position),
|
|
195
|
+
h.AriaSetsize(row.siblings),
|
|
196
|
+
...row.branch ? [h.AriaExpanded(isOpen(model, args, row.id))] : [],
|
|
197
|
+
...row.disabled === true ? [h.AriaDisabled(true)] : [],
|
|
198
|
+
h.Tabindex(stop === row.id ? 0 : -1),
|
|
199
|
+
h.OnFocus(wrap(Message.Focused({ id: row.id })))
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
}, { name: "TreeNavigation" });
|
|
204
|
+
};
|
|
205
|
+
export { Args, Message, Model, behavior, bundle, isOpen, move, shown, tabStop };
|
|
206
|
+
|
|
207
|
+
//# sourceMappingURL=tree-navigation.js.map
|