@askrjs/askr 0.0.8 → 0.0.9
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/foundations/index.d.ts +362 -12
- package/dist/foundations/index.js +223 -22
- package/dist/foundations/index.js.map +1 -1
- package/dist/router/index.js +1 -1
- package/dist/router/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -94,6 +94,133 @@ interface Portal<T = unknown> {
|
|
|
94
94
|
declare function definePortal<T = unknown>(): Portal<T>;
|
|
95
95
|
declare const DefaultPortal: Portal<unknown>;
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* createCollection
|
|
99
|
+
*
|
|
100
|
+
* Ordered descendant registry for coordinating items without DOM queries.
|
|
101
|
+
*
|
|
102
|
+
* INVARIANTS:
|
|
103
|
+
* 1. Registration order determines item order (no DOM queries)
|
|
104
|
+
* 2. Stable ordering across renders (insertion order preserved)
|
|
105
|
+
* 3. Each item may have metadata (type-safe, user-defined)
|
|
106
|
+
* 4. No implicit global state (explicit collection instances)
|
|
107
|
+
* 5. No automatic cleanup (caller controls lifecycle)
|
|
108
|
+
*
|
|
109
|
+
* DESIGN:
|
|
110
|
+
* - Returns a registry API ({ register, items, clear })
|
|
111
|
+
* - Items are stored in insertion order
|
|
112
|
+
* - Registration returns an unregister function
|
|
113
|
+
* - No side effects on registration (pure data structure)
|
|
114
|
+
*
|
|
115
|
+
* USAGE:
|
|
116
|
+
* const collection = createCollection<HTMLElement, { disabled: boolean }>();
|
|
117
|
+
* const unregister = collection.register(element, { disabled: false });
|
|
118
|
+
* const allItems = collection.items();
|
|
119
|
+
* unregister();
|
|
120
|
+
*/
|
|
121
|
+
type CollectionItem<TNode, TMetadata = unknown> = {
|
|
122
|
+
node: TNode;
|
|
123
|
+
metadata: TMetadata;
|
|
124
|
+
};
|
|
125
|
+
interface Collection<TNode, TMetadata = unknown> {
|
|
126
|
+
/**
|
|
127
|
+
* Register a node with optional metadata.
|
|
128
|
+
* Returns an unregister function.
|
|
129
|
+
*/
|
|
130
|
+
register(node: TNode, metadata: TMetadata): () => void;
|
|
131
|
+
/**
|
|
132
|
+
* Get all registered items in insertion order.
|
|
133
|
+
*/
|
|
134
|
+
items(): ReadonlyArray<CollectionItem<TNode, TMetadata>>;
|
|
135
|
+
/**
|
|
136
|
+
* Clear all registered items.
|
|
137
|
+
*/
|
|
138
|
+
clear(): void;
|
|
139
|
+
/**
|
|
140
|
+
* Get the count of registered items.
|
|
141
|
+
*/
|
|
142
|
+
size(): number;
|
|
143
|
+
}
|
|
144
|
+
declare function createCollection<TNode, TMetadata = unknown>(): Collection<TNode, TMetadata>;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* createLayer
|
|
148
|
+
*
|
|
149
|
+
* Manages stacking order and coordination for overlays (modals, popovers, etc).
|
|
150
|
+
*
|
|
151
|
+
* INVARIANTS:
|
|
152
|
+
* 1. Layers are ordered by registration time (FIFO)
|
|
153
|
+
* 2. Only the top layer handles Escape key
|
|
154
|
+
* 3. Only the top layer handles outside pointer events
|
|
155
|
+
* 4. Nested layers are supported
|
|
156
|
+
* 5. Does not implement portals (orthogonal concern)
|
|
157
|
+
* 6. No automatic DOM insertion (caller controls mounting)
|
|
158
|
+
*
|
|
159
|
+
* DESIGN:
|
|
160
|
+
* - Returns a layer manager with register/unregister API
|
|
161
|
+
* - Each layer has a unique ID and can query if it's the top layer
|
|
162
|
+
* - Escape and outside pointer coordination via callbacks
|
|
163
|
+
* - No z-index management (CSS concern)
|
|
164
|
+
*
|
|
165
|
+
* USAGE:
|
|
166
|
+
* const manager = createLayer();
|
|
167
|
+
*
|
|
168
|
+
* const layer = manager.register({
|
|
169
|
+
* onEscape: () => { ... },
|
|
170
|
+
* onOutsidePointer: () => { ... }
|
|
171
|
+
* });
|
|
172
|
+
*
|
|
173
|
+
* layer.isTop(); // true if this is the topmost layer
|
|
174
|
+
* layer.unregister();
|
|
175
|
+
*/
|
|
176
|
+
interface LayerOptions {
|
|
177
|
+
/**
|
|
178
|
+
* Called when Escape is pressed and this is the top layer
|
|
179
|
+
*/
|
|
180
|
+
onEscape?: () => void;
|
|
181
|
+
/**
|
|
182
|
+
* Called when pointer event occurs outside and this is the top layer
|
|
183
|
+
*/
|
|
184
|
+
onOutsidePointer?: (e: PointerEvent) => void;
|
|
185
|
+
/**
|
|
186
|
+
* Optional node reference for outside pointer detection
|
|
187
|
+
*/
|
|
188
|
+
node?: Node | null;
|
|
189
|
+
}
|
|
190
|
+
interface Layer {
|
|
191
|
+
/**
|
|
192
|
+
* Unique layer ID
|
|
193
|
+
*/
|
|
194
|
+
id: number;
|
|
195
|
+
/**
|
|
196
|
+
* Check if this layer is the topmost
|
|
197
|
+
*/
|
|
198
|
+
isTop(): boolean;
|
|
199
|
+
/**
|
|
200
|
+
* Remove this layer from the stack
|
|
201
|
+
*/
|
|
202
|
+
unregister(): void;
|
|
203
|
+
}
|
|
204
|
+
interface LayerManager {
|
|
205
|
+
/**
|
|
206
|
+
* Register a new layer
|
|
207
|
+
*/
|
|
208
|
+
register(options: LayerOptions): Layer;
|
|
209
|
+
/**
|
|
210
|
+
* Get all active layers in order
|
|
211
|
+
*/
|
|
212
|
+
layers(): ReadonlyArray<Layer>;
|
|
213
|
+
/**
|
|
214
|
+
* Manually trigger escape handling on the top layer
|
|
215
|
+
*/
|
|
216
|
+
handleEscape(): void;
|
|
217
|
+
/**
|
|
218
|
+
* Manually trigger outside pointer handling on the top layer
|
|
219
|
+
*/
|
|
220
|
+
handleOutsidePointer(e: PointerEvent): void;
|
|
221
|
+
}
|
|
222
|
+
declare function createLayer(): LayerManager;
|
|
223
|
+
|
|
97
224
|
/**
|
|
98
225
|
* composeHandlers
|
|
99
226
|
*
|
|
@@ -319,22 +446,59 @@ declare function pressable({ disabled, onPress, isNativeButton, }: PressableOpti
|
|
|
319
446
|
/**
|
|
320
447
|
* dismissable
|
|
321
448
|
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
449
|
+
* THE dismissal primitive. Handles Escape key and outside interactions.
|
|
450
|
+
*
|
|
451
|
+
* INVARIANTS:
|
|
452
|
+
* 1. Returns props that compose via mergeProps (no factories)
|
|
453
|
+
* 2. Disabled state respected exactly once, here
|
|
454
|
+
* 3. No side effects - pure props generation
|
|
455
|
+
* 4. Outside detection requires explicit node reference
|
|
456
|
+
* 5. This is the ONLY dismissal primitive - do not create alternatives
|
|
457
|
+
*
|
|
458
|
+
* DESIGN:
|
|
459
|
+
* - Returns standard event handler props (onKeyDown, onPointerDownCapture)
|
|
460
|
+
* - Composable via mergeProps with other foundations
|
|
461
|
+
* - Caller provides node reference for outside detection
|
|
462
|
+
* - Single onDismiss callback for all dismiss triggers
|
|
463
|
+
*
|
|
464
|
+
* PIT OF SUCCESS:
|
|
465
|
+
* ✓ Can't accidentally bypass (only way to get dismiss behavior)
|
|
466
|
+
* ✓ Can't duplicate (disabled checked once)
|
|
467
|
+
* ✓ Composes via mergeProps (standard props)
|
|
468
|
+
* ✓ Wrong usage is hard (no factories to misuse)
|
|
469
|
+
*
|
|
470
|
+
* USAGE:
|
|
471
|
+
* const props = dismissable({
|
|
472
|
+
* node: elementRef,
|
|
473
|
+
* disabled: false,
|
|
474
|
+
* onDismiss: () => close()
|
|
475
|
+
* });
|
|
476
|
+
*
|
|
477
|
+
* <div ref={elementRef} {...props}>Content</div>
|
|
478
|
+
*
|
|
479
|
+
* MISUSE EXAMPLE (PREVENTED):
|
|
480
|
+
* ❌ Can't forget to check disabled - checked inside dismissable
|
|
481
|
+
* ❌ Can't create custom escape handler - this is the only one
|
|
482
|
+
* ❌ Can't bypass via direct event listeners - mergeProps composes correctly
|
|
329
483
|
*/
|
|
330
484
|
interface DismissableOptions {
|
|
331
|
-
|
|
485
|
+
/**
|
|
486
|
+
* Reference to the element for outside click detection
|
|
487
|
+
*/
|
|
488
|
+
node?: Node | null;
|
|
489
|
+
/**
|
|
490
|
+
* Whether dismiss is disabled
|
|
491
|
+
*/
|
|
332
492
|
disabled?: boolean;
|
|
493
|
+
/**
|
|
494
|
+
* Called when dismiss is triggered (Escape or outside click)
|
|
495
|
+
*/
|
|
496
|
+
onDismiss?: (trigger: 'escape' | 'outside') => void;
|
|
333
497
|
}
|
|
334
498
|
|
|
335
|
-
declare function dismissable({
|
|
336
|
-
onKeyDown: (
|
|
337
|
-
|
|
499
|
+
declare function dismissable({ node, disabled, onDismiss, }: DismissableOptions): {
|
|
500
|
+
onKeyDown: (e: KeyboardLikeEvent) => void;
|
|
501
|
+
onPointerDownCapture: (e: PointerLikeEvent) => void;
|
|
338
502
|
};
|
|
339
503
|
|
|
340
504
|
/**
|
|
@@ -371,4 +535,190 @@ interface HoverableResult {
|
|
|
371
535
|
}
|
|
372
536
|
declare function hoverable({ disabled, onEnter, onLeave, }: HoverableOptions): HoverableResult;
|
|
373
537
|
|
|
374
|
-
|
|
538
|
+
/**
|
|
539
|
+
* rovingFocus
|
|
540
|
+
*
|
|
541
|
+
* Single tab stop navigation with arrow-key control.
|
|
542
|
+
*
|
|
543
|
+
* INVARIANTS:
|
|
544
|
+
* 1. Only one item in the group is reachable via Tab (single tab stop)
|
|
545
|
+
* 2. Arrow keys move focus within the group
|
|
546
|
+
* 3. Orientation determines which arrow keys are active
|
|
547
|
+
* 4. Looping is opt-in
|
|
548
|
+
* 5. Disabled items are skipped
|
|
549
|
+
* 6. Returns props objects, never factories (composes via mergeProps)
|
|
550
|
+
*
|
|
551
|
+
* DESIGN:
|
|
552
|
+
* - Container gets onKeyDown for arrow navigation
|
|
553
|
+
* - Each item gets tabIndex based on current selection
|
|
554
|
+
* - Navigation logic is pure - caller controls focus application
|
|
555
|
+
* - Disabled check happens per-item via predicate
|
|
556
|
+
*
|
|
557
|
+
* PIT OF SUCCESS:
|
|
558
|
+
* ✓ Can't accidentally break tab order (tabIndex assigned correctly)
|
|
559
|
+
* ✓ Can't duplicate navigation logic (single source)
|
|
560
|
+
* ✓ Composes via mergeProps (all standard props)
|
|
561
|
+
* ✓ Type-safe - invalid indices caught at call site
|
|
562
|
+
*
|
|
563
|
+
* USAGE:
|
|
564
|
+
* const nav = rovingFocus({
|
|
565
|
+
* currentIndex: 0,
|
|
566
|
+
* itemCount: 3,
|
|
567
|
+
* orientation: 'horizontal',
|
|
568
|
+
* onNavigate: setIndex
|
|
569
|
+
* });
|
|
570
|
+
*
|
|
571
|
+
* <div {...nav.container}>
|
|
572
|
+
* <button {...nav.item(0)}>First</button>
|
|
573
|
+
* <button {...nav.item(1)}>Second</button>
|
|
574
|
+
* </div>
|
|
575
|
+
*
|
|
576
|
+
* MISUSE EXAMPLE (PREVENTED):
|
|
577
|
+
* ❌ Can't forget to set tabIndex - returned in item props
|
|
578
|
+
* ❌ Can't create conflicting arrow handlers - mergeProps composes
|
|
579
|
+
* ❌ Can't skip disabled items incorrectly - logic is internal
|
|
580
|
+
*/
|
|
581
|
+
|
|
582
|
+
type Orientation = 'horizontal' | 'vertical' | 'both';
|
|
583
|
+
interface RovingFocusOptions {
|
|
584
|
+
/**
|
|
585
|
+
* Current focused index
|
|
586
|
+
*/
|
|
587
|
+
currentIndex: number;
|
|
588
|
+
/**
|
|
589
|
+
* Total number of items
|
|
590
|
+
*/
|
|
591
|
+
itemCount: number;
|
|
592
|
+
/**
|
|
593
|
+
* Navigation orientation
|
|
594
|
+
* - horizontal: ArrowLeft/ArrowRight
|
|
595
|
+
* - vertical: ArrowUp/ArrowDown
|
|
596
|
+
* - both: all arrow keys
|
|
597
|
+
*/
|
|
598
|
+
orientation?: Orientation;
|
|
599
|
+
/**
|
|
600
|
+
* Whether to loop when reaching the end
|
|
601
|
+
*/
|
|
602
|
+
loop?: boolean;
|
|
603
|
+
/**
|
|
604
|
+
* Callback when navigation occurs
|
|
605
|
+
*/
|
|
606
|
+
onNavigate?: (index: number) => void;
|
|
607
|
+
/**
|
|
608
|
+
* Optional disabled state check per index
|
|
609
|
+
*/
|
|
610
|
+
isDisabled?: (index: number) => boolean;
|
|
611
|
+
}
|
|
612
|
+
interface RovingFocusResult {
|
|
613
|
+
/**
|
|
614
|
+
* Props for the container element (composes via mergeProps)
|
|
615
|
+
*/
|
|
616
|
+
container: {
|
|
617
|
+
onKeyDown: (e: KeyboardLikeEvent) => void;
|
|
618
|
+
};
|
|
619
|
+
/**
|
|
620
|
+
* Generate props for an item at the given index (composes via mergeProps)
|
|
621
|
+
*/
|
|
622
|
+
item: (index: number) => {
|
|
623
|
+
tabIndex: number;
|
|
624
|
+
'data-roving-index': number;
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
declare function rovingFocus(options: RovingFocusOptions): RovingFocusResult;
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* INTERACTION POLICY (FRAMEWORK LAW)
|
|
631
|
+
*
|
|
632
|
+
* This is THE ONLY way to create interactive elements. Components MUST NOT
|
|
633
|
+
* implement interaction logic directly.
|
|
634
|
+
*
|
|
635
|
+
* INVARIANTS (ENFORCED):
|
|
636
|
+
* 1. "Press" is the semantic. Click/touch/Enter/Space are implementation details.
|
|
637
|
+
* 2. Disabled is enforced exactly once, here. Components may not check disabled.
|
|
638
|
+
* 3. Keyboard handling is automatic. Components may not add onKeyDown for activation.
|
|
639
|
+
* 4. Native elements opt out of polyfills, not semantics.
|
|
640
|
+
* 5. asChild may replace the host element, not interaction behavior.
|
|
641
|
+
* 6. This policy is the SINGLE SOURCE OF TRUTH for interactive behavior.
|
|
642
|
+
*
|
|
643
|
+
* DESIGN:
|
|
644
|
+
* - Single public function: applyInteractionPolicy
|
|
645
|
+
* - Returns props that compose via mergeProps
|
|
646
|
+
* - Delegates to pressable for mechanics
|
|
647
|
+
* - Enforces disabled once and only once
|
|
648
|
+
* - No configuration beyond disabled and native element type
|
|
649
|
+
*
|
|
650
|
+
* PIT OF SUCCESS:
|
|
651
|
+
* ✓ Can't bypass policy (only way to get interaction behavior)
|
|
652
|
+
* ✓ Can't duplicate disabled checks (enforced once, here)
|
|
653
|
+
* ✓ Can't write custom keyboard handlers for buttons (policy owns it)
|
|
654
|
+
* ✓ Composes via mergeProps (standard props)
|
|
655
|
+
* ✓ Wrong usage is impossible (no escape hatch)
|
|
656
|
+
*
|
|
657
|
+
* USAGE:
|
|
658
|
+
* function Button({ onPress, disabled }) {
|
|
659
|
+
* const interaction = applyInteractionPolicy({
|
|
660
|
+
* isNative: true,
|
|
661
|
+
* disabled,
|
|
662
|
+
* onPress
|
|
663
|
+
* });
|
|
664
|
+
*
|
|
665
|
+
* return <button {...interaction}>Click me</button>;
|
|
666
|
+
* }
|
|
667
|
+
*
|
|
668
|
+
* MISUSE EXAMPLE (PREVENTED):
|
|
669
|
+
* ❌ Button checking disabled again:
|
|
670
|
+
* function Button({ disabled, onPress }) {
|
|
671
|
+
* if (disabled) return; // NO! Policy handles this
|
|
672
|
+
* const interaction = applyInteractionPolicy(...);
|
|
673
|
+
* }
|
|
674
|
+
*
|
|
675
|
+
* ❌ Custom keyboard handler:
|
|
676
|
+
* function Button({ onPress }) {
|
|
677
|
+
* const interaction = applyInteractionPolicy(...);
|
|
678
|
+
* return <button {...interaction} onKeyDown={...}>; // NO! Policy owns this
|
|
679
|
+
* }
|
|
680
|
+
*
|
|
681
|
+
* ❌ Direct event handler:
|
|
682
|
+
* <button onClick={onPress}>; // NO! Use applyInteractionPolicy
|
|
683
|
+
*/
|
|
684
|
+
interface InteractionPolicyInput {
|
|
685
|
+
/** Whether the host element is a native interactive element (button, a, etc) */
|
|
686
|
+
isNative: boolean;
|
|
687
|
+
/** Disabled state - checked ONLY here, never in components */
|
|
688
|
+
disabled: boolean;
|
|
689
|
+
/** User-provided press handler - semantic action, not DOM event */
|
|
690
|
+
onPress?: (e: Event) => void;
|
|
691
|
+
/** Optional ref to compose */
|
|
692
|
+
ref?: any;
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* THE interaction policy. Components MUST use this, NEVER implement
|
|
696
|
+
* interaction logic directly.
|
|
697
|
+
*/
|
|
698
|
+
declare function applyInteractionPolicy({ isNative, disabled, onPress, ref, }: InteractionPolicyInput): {
|
|
699
|
+
disabled: true | undefined;
|
|
700
|
+
onClick: (e: Event) => void;
|
|
701
|
+
ref: any;
|
|
702
|
+
} | {
|
|
703
|
+
'aria-disabled': true | undefined;
|
|
704
|
+
tabIndex: number;
|
|
705
|
+
ref: any;
|
|
706
|
+
onClick: (e: DefaultPreventable & PropagationStoppable) => void;
|
|
707
|
+
disabled?: true;
|
|
708
|
+
role?: "button";
|
|
709
|
+
onKeyDown?: (e: KeyboardLikeEvent) => void;
|
|
710
|
+
onKeyUp?: (e: KeyboardLikeEvent) => void;
|
|
711
|
+
};
|
|
712
|
+
/**
|
|
713
|
+
* Merge rule for Slot / asChild
|
|
714
|
+
*
|
|
715
|
+
* Precedence:
|
|
716
|
+
* policy → user → child
|
|
717
|
+
*
|
|
718
|
+
* Event handlers are composed (policy first).
|
|
719
|
+
* Refs are always composed.
|
|
720
|
+
* Policy props MUST take precedence to enforce invariants.
|
|
721
|
+
*/
|
|
722
|
+
declare function mergeInteractionProps(childProps: Record<string, any>, policyProps: Record<string, any>, userProps?: Record<string, any>): Record<string, any>;
|
|
723
|
+
|
|
724
|
+
export { type Collection, type CollectionItem, type ComposeHandlersOptions, type ControllableState, DefaultPortal, type DismissableOptions, type FocusableOptions, type FocusableResult, type HoverableOptions, type HoverableResult, type InteractionPolicyInput, JSXElement, type Layer, type LayerManager, type LayerOptions, type Orientation, type Portal, Presence, type PresenceProps, type PressableOptions, type PressableResult, type Ref, type RovingFocusOptions, type RovingFocusResult, Slot, type SlotProps, applyInteractionPolicy, ariaDisabled, ariaExpanded, ariaSelected, composeHandlers, composeRefs, controllableState, createCollection, createLayer, definePortal, dismissable, focusable, hoverable, isControlled, makeControllable, mergeInteractionProps, mergeProps, pressable, resolveControllable, rovingFocus, setRef, useId };
|
|
@@ -98,7 +98,95 @@ var DefaultPortal = (() => {
|
|
|
98
98
|
return Host;
|
|
99
99
|
})();
|
|
100
100
|
|
|
101
|
-
// src/foundations/
|
|
101
|
+
// src/foundations/structures/collection.ts
|
|
102
|
+
function createCollection() {
|
|
103
|
+
const registry = /* @__PURE__ */ new Map();
|
|
104
|
+
function register(node, metadata) {
|
|
105
|
+
const item = { node, metadata };
|
|
106
|
+
registry.set(node, item);
|
|
107
|
+
return () => {
|
|
108
|
+
registry.delete(node);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function items() {
|
|
112
|
+
return Array.from(registry.values());
|
|
113
|
+
}
|
|
114
|
+
function clear() {
|
|
115
|
+
registry.clear();
|
|
116
|
+
}
|
|
117
|
+
function size() {
|
|
118
|
+
return registry.size;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
register,
|
|
122
|
+
items,
|
|
123
|
+
clear,
|
|
124
|
+
size
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/foundations/structures/layer.ts
|
|
129
|
+
function createLayer() {
|
|
130
|
+
const stack = [];
|
|
131
|
+
let nextId = 1;
|
|
132
|
+
function register(options) {
|
|
133
|
+
const id = nextId++;
|
|
134
|
+
const entry = { id, options };
|
|
135
|
+
stack.push(entry);
|
|
136
|
+
function isTop() {
|
|
137
|
+
return stack[stack.length - 1]?.id === id;
|
|
138
|
+
}
|
|
139
|
+
function unregister() {
|
|
140
|
+
const index = stack.findIndex((e) => e.id === id);
|
|
141
|
+
if (index !== -1) {
|
|
142
|
+
stack.splice(index, 1);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
id,
|
|
147
|
+
isTop,
|
|
148
|
+
unregister
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function layers() {
|
|
152
|
+
return stack.map((entry) => ({
|
|
153
|
+
id: entry.id,
|
|
154
|
+
isTop: () => stack[stack.length - 1]?.id === entry.id,
|
|
155
|
+
unregister: () => {
|
|
156
|
+
const index = stack.findIndex((e) => e.id === entry.id);
|
|
157
|
+
if (index !== -1) {
|
|
158
|
+
stack.splice(index, 1);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
function handleEscape() {
|
|
164
|
+
const top = stack[stack.length - 1];
|
|
165
|
+
if (top) {
|
|
166
|
+
top.options.onEscape?.();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function handleOutsidePointer(e) {
|
|
170
|
+
const top = stack[stack.length - 1];
|
|
171
|
+
if (!top) return;
|
|
172
|
+
const node = top.options.node;
|
|
173
|
+
if (node && e.target instanceof Node) {
|
|
174
|
+
if (!node.contains(e.target)) {
|
|
175
|
+
top.options.onOutsidePointer?.(e);
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
top.options.onOutsidePointer?.(e);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
register,
|
|
183
|
+
layers,
|
|
184
|
+
handleEscape,
|
|
185
|
+
handleOutsidePointer
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/foundations/utilities/compose-handlers.ts
|
|
102
190
|
function isDefaultPrevented(value) {
|
|
103
191
|
return typeof value === "object" && value !== null && "defaultPrevented" in value && value.defaultPrevented === true;
|
|
104
192
|
}
|
|
@@ -120,7 +208,7 @@ function composeHandlers(first, second, options) {
|
|
|
120
208
|
function noop() {
|
|
121
209
|
}
|
|
122
210
|
|
|
123
|
-
// src/foundations/utilities/
|
|
211
|
+
// src/foundations/utilities/merge-props.ts
|
|
124
212
|
function isEventHandlerKey(key) {
|
|
125
213
|
return key.startsWith("on");
|
|
126
214
|
}
|
|
@@ -156,7 +244,7 @@ function ariaSelected(selected) {
|
|
|
156
244
|
return selected === void 0 ? {} : { "aria-selected": String(selected) };
|
|
157
245
|
}
|
|
158
246
|
|
|
159
|
-
// src/foundations/utilities/
|
|
247
|
+
// src/foundations/utilities/compose-ref.ts
|
|
160
248
|
function setRef(ref, value) {
|
|
161
249
|
if (!ref) return;
|
|
162
250
|
if (typeof ref === "function") {
|
|
@@ -174,7 +262,7 @@ function composeRefs(...refs) {
|
|
|
174
262
|
};
|
|
175
263
|
}
|
|
176
264
|
|
|
177
|
-
// src/foundations/utilities/
|
|
265
|
+
// src/foundations/utilities/use-id.ts
|
|
178
266
|
function useId(options) {
|
|
179
267
|
const prefix = options.prefix ?? "askr";
|
|
180
268
|
return `${prefix}-${String(options.id)}`;
|
|
@@ -1020,24 +1108,32 @@ function pressable({
|
|
|
1020
1108
|
}
|
|
1021
1109
|
|
|
1022
1110
|
// src/foundations/interactions/dismissable.ts
|
|
1023
|
-
function dismissable({
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
outsideListener: disabled ? void 0 : (isInside) => (e) => {
|
|
1035
|
-
if (!isInside(e.target)) {
|
|
1036
|
-
e.preventDefault?.();
|
|
1037
|
-
e.stopPropagation?.();
|
|
1038
|
-
onDismiss?.();
|
|
1039
|
-
}
|
|
1111
|
+
function dismissable({
|
|
1112
|
+
node,
|
|
1113
|
+
disabled,
|
|
1114
|
+
onDismiss
|
|
1115
|
+
}) {
|
|
1116
|
+
function handleKeyDown(e) {
|
|
1117
|
+
if (disabled) return;
|
|
1118
|
+
if (e.key === "Escape") {
|
|
1119
|
+
e.preventDefault?.();
|
|
1120
|
+
e.stopPropagation?.();
|
|
1121
|
+
onDismiss?.("escape");
|
|
1040
1122
|
}
|
|
1123
|
+
}
|
|
1124
|
+
function handlePointerDownCapture(e) {
|
|
1125
|
+
if (disabled) return;
|
|
1126
|
+
const target = e.target;
|
|
1127
|
+
if (!(target instanceof Node)) return;
|
|
1128
|
+
if (!node) return;
|
|
1129
|
+
if (!node.contains(target)) {
|
|
1130
|
+
onDismiss?.("outside");
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return {
|
|
1134
|
+
onKeyDown: handleKeyDown,
|
|
1135
|
+
// Use capture phase to catch events before they bubble
|
|
1136
|
+
onPointerDownCapture: handlePointerDownCapture
|
|
1041
1137
|
};
|
|
1042
1138
|
}
|
|
1043
1139
|
|
|
@@ -1068,6 +1164,111 @@ function hoverable({
|
|
|
1068
1164
|
};
|
|
1069
1165
|
}
|
|
1070
1166
|
|
|
1071
|
-
|
|
1167
|
+
// src/foundations/interactions/roving-focus.ts
|
|
1168
|
+
function rovingFocus(options) {
|
|
1169
|
+
const {
|
|
1170
|
+
currentIndex,
|
|
1171
|
+
itemCount,
|
|
1172
|
+
orientation = "horizontal",
|
|
1173
|
+
loop = false,
|
|
1174
|
+
onNavigate,
|
|
1175
|
+
isDisabled
|
|
1176
|
+
} = options;
|
|
1177
|
+
function findNextIndex(from, direction) {
|
|
1178
|
+
let next = from + direction;
|
|
1179
|
+
if (loop) {
|
|
1180
|
+
if (next < 0) next = itemCount - 1;
|
|
1181
|
+
if (next >= itemCount) next = 0;
|
|
1182
|
+
} else {
|
|
1183
|
+
if (next < 0 || next >= itemCount) return void 0;
|
|
1184
|
+
}
|
|
1185
|
+
if (isDisabled?.(next)) {
|
|
1186
|
+
if (next === from) return void 0;
|
|
1187
|
+
return findNextIndex(next, direction);
|
|
1188
|
+
}
|
|
1189
|
+
return next;
|
|
1190
|
+
}
|
|
1191
|
+
function handleKeyDown(e) {
|
|
1192
|
+
const { key } = e;
|
|
1193
|
+
let direction;
|
|
1194
|
+
if (orientation === "horizontal" || orientation === "both") {
|
|
1195
|
+
if (key === "ArrowRight") direction = 1;
|
|
1196
|
+
if (key === "ArrowLeft") direction = -1;
|
|
1197
|
+
}
|
|
1198
|
+
if (orientation === "vertical" || orientation === "both") {
|
|
1199
|
+
if (key === "ArrowDown") direction = 1;
|
|
1200
|
+
if (key === "ArrowUp") direction = -1;
|
|
1201
|
+
}
|
|
1202
|
+
if (direction === void 0) return;
|
|
1203
|
+
const nextIndex = findNextIndex(currentIndex, direction);
|
|
1204
|
+
if (nextIndex === void 0) return;
|
|
1205
|
+
e.preventDefault?.();
|
|
1206
|
+
e.stopPropagation?.();
|
|
1207
|
+
onNavigate?.(nextIndex);
|
|
1208
|
+
}
|
|
1209
|
+
return {
|
|
1210
|
+
container: {
|
|
1211
|
+
onKeyDown: handleKeyDown
|
|
1212
|
+
},
|
|
1213
|
+
item: (index) => ({
|
|
1214
|
+
tabIndex: index === currentIndex ? 0 : -1,
|
|
1215
|
+
"data-roving-index": index
|
|
1216
|
+
})
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// src/foundations/interactions/interaction-policy.ts
|
|
1221
|
+
function applyInteractionPolicy({
|
|
1222
|
+
isNative,
|
|
1223
|
+
disabled,
|
|
1224
|
+
onPress,
|
|
1225
|
+
ref
|
|
1226
|
+
}) {
|
|
1227
|
+
function invokePress(e) {
|
|
1228
|
+
if (disabled) {
|
|
1229
|
+
e.preventDefault?.();
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
onPress?.(e);
|
|
1233
|
+
}
|
|
1234
|
+
if (isNative) {
|
|
1235
|
+
return {
|
|
1236
|
+
disabled: disabled || void 0,
|
|
1237
|
+
onClick: (e) => invokePress(e),
|
|
1238
|
+
ref
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
const interaction = pressable({
|
|
1242
|
+
disabled,
|
|
1243
|
+
isNativeButton: false,
|
|
1244
|
+
onPress: (e) => invokePress(e)
|
|
1245
|
+
});
|
|
1246
|
+
return {
|
|
1247
|
+
...interaction,
|
|
1248
|
+
"aria-disabled": disabled || void 0,
|
|
1249
|
+
tabIndex: disabled ? -1 : interaction.tabIndex ?? 0,
|
|
1250
|
+
ref
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
function mergeInteractionProps(childProps, policyProps, userProps) {
|
|
1254
|
+
let out = mergeProps(childProps, policyProps);
|
|
1255
|
+
if (userProps) out = mergeProps(out, userProps);
|
|
1256
|
+
for (const k in out) {
|
|
1257
|
+
if (!k.startsWith("on")) continue;
|
|
1258
|
+
const policyHandler = policyProps?.[k];
|
|
1259
|
+
const userHandler = userProps?.[k];
|
|
1260
|
+
const childHandler = childProps?.[k];
|
|
1261
|
+
if (policyHandler || userHandler || childHandler) {
|
|
1262
|
+
out[k] = composeHandlers(
|
|
1263
|
+
policyHandler,
|
|
1264
|
+
composeHandlers(userHandler, childHandler)
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
out.ref = composeRefs(childProps?.ref, userProps?.ref, policyProps?.ref);
|
|
1269
|
+
return out;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
export { DefaultPortal, Presence, Slot, applyInteractionPolicy, ariaDisabled, ariaExpanded, ariaSelected, composeHandlers, composeRefs, controllableState, createCollection, createLayer, definePortal, dismissable, focusable, hoverable, isControlled, layout, makeControllable, mergeInteractionProps, mergeProps, pressable, resolveControllable, rovingFocus, setRef, useId };
|
|
1072
1273
|
//# sourceMappingURL=index.js.map
|
|
1073
1274
|
//# sourceMappingURL=index.js.map
|