foldkit 0.23.0 → 0.24.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.
@@ -0,0 +1,8 @@
1
+ /** A contiguous segment of items sharing the same group key. */
2
+ export type Segment<A> = Readonly<{
3
+ key: string;
4
+ items: ReadonlyArray<A>;
5
+ }>;
6
+ /** Groups items into contiguous segments by a key function. Adjacent items with the same key are collected into a single segment. */
7
+ export declare const groupContiguous: <A>(items: ReadonlyArray<A>, toKey: (item: A, index: number) => string) => ReadonlyArray<Segment<A>>;
8
+ //# sourceMappingURL=group.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group.d.ts","sourceRoot":"","sources":["../../src/ui/group.ts"],"names":[],"mappings":"AAEA,gEAAgE;AAChE,MAAM,MAAM,OAAO,CAAC,CAAC,IAAI,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,CAAA;CAAE,CAAC,CAAA;AAE3E,qIAAqI;AACrI,eAAO,MAAM,eAAe,GAAI,CAAC,EAC/B,OAAO,aAAa,CAAC,CAAC,CAAC,EACvB,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,KACxC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAW1B,CAAA"}
@@ -0,0 +1,13 @@
1
+ import { Array } from 'effect';
2
+ /** Groups items into contiguous segments by a key function. Adjacent items with the same key are collected into a single segment. */
3
+ export const groupContiguous = (items, toKey) => {
4
+ const tagged = Array.map(items, (item, index) => ({
5
+ key: toKey(item, index),
6
+ item,
7
+ }));
8
+ return Array.chop(tagged, nonEmpty => {
9
+ const key = Array.headNonEmpty(nonEmpty).key;
10
+ const [matching, rest] = Array.span(nonEmpty, tagged => tagged.key === key);
11
+ return [{ key, items: Array.map(matching, ({ item }) => item) }, rest];
12
+ });
13
+ };
@@ -1,5 +1,6 @@
1
1
  export * as Dialog from './dialog/public';
2
2
  export * as Disclosure from './disclosure/public';
3
+ export * as Listbox from './listbox/public';
3
4
  export * as Menu from './menu/public';
4
5
  export * as Tabs from './tabs/public';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAA;AACzC,OAAO,KAAK,UAAU,MAAM,qBAAqB,CAAA;AACjD,OAAO,KAAK,IAAI,MAAM,eAAe,CAAA;AACrC,OAAO,KAAK,IAAI,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAA;AACzC,OAAO,KAAK,UAAU,MAAM,qBAAqB,CAAA;AACjD,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAA;AAC3C,OAAO,KAAK,IAAI,MAAM,eAAe,CAAA;AACrC,OAAO,KAAK,IAAI,MAAM,eAAe,CAAA"}
package/dist/ui/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * as Dialog from './dialog/public';
2
2
  export * as Disclosure from './disclosure/public';
3
+ export * as Listbox from './listbox/public';
3
4
  export * as Menu from './menu/public';
4
5
  export * as Tabs from './tabs/public';
@@ -1,3 +1,5 @@
1
+ /** Whether a keyboard event key is a single printable character (not a named key like "Enter" or "ArrowDown"). */
2
+ export declare const isPrintableKey: (key: string) => boolean;
1
3
  export declare const wrapIndex: (index: number, length: number) => number;
2
4
  export declare const findFirstEnabledIndex: (itemCount: number, focusedIndex: number, isDisabled: (index: number) => boolean) => (startIndex: number, direction: 1 | -1) => number;
3
5
  export declare const keyToIndex: (nextKey: string, previousKey: string, itemCount: number, focusedIndex: number, isDisabled: (index: number) => boolean) => ((key: string) => number);
@@ -1 +1 @@
1
- {"version":3,"file":"keyboard.d.ts","sourceRoot":"","sources":["../../src/ui/keyboard.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS,GAAI,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAG,MACpB,CAAA;AAEtC,eAAO,MAAM,qBAAqB,GAE9B,WAAW,MAAM,EACjB,cAAc,MAAM,EACpB,YAAY,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,MAEvC,YAAY,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,KAAG,MAMtC,CAAA;AAEL,eAAO,MAAM,UAAU,GACrB,SAAS,MAAM,EACf,aAAa,MAAM,EACnB,WAAW,MAAM,EACjB,cAAc,MAAM,EACpB,YAAY,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,KACrC,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAW1B,CAAA"}
1
+ {"version":3,"file":"keyboard.d.ts","sourceRoot":"","sources":["../../src/ui/keyboard.ts"],"names":[],"mappings":"AAEA,kHAAkH;AAClH,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,OAA2B,CAAA;AAExE,eAAO,MAAM,SAAS,GAAI,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAG,MACpB,CAAA;AAEtC,eAAO,MAAM,qBAAqB,GAE9B,WAAW,MAAM,EACjB,cAAc,MAAM,EACpB,YAAY,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,MAEvC,YAAY,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,KAAG,MAMtC,CAAA;AAEL,eAAO,MAAM,UAAU,GACrB,SAAS,MAAM,EACf,aAAa,MAAM,EACnB,WAAW,MAAM,EACjB,cAAc,MAAM,EACpB,YAAY,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,KACrC,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAW1B,CAAA"}
@@ -1,4 +1,6 @@
1
1
  import { Array, Match as M, Option, Predicate, pipe } from 'effect';
2
+ /** Whether a keyboard event key is a single printable character (not a named key like "Enter" or "ArrowDown"). */
3
+ export const isPrintableKey = (key) => key.length === 1;
2
4
  export const wrapIndex = (index, length) => ((index % length) + length) % length;
3
5
  export const findFirstEnabledIndex = (itemCount, focusedIndex, isDisabled) => (startIndex, direction) => pipe(itemCount, Array.makeBy(step => wrapIndex(startIndex + step * direction, itemCount)), Array.findFirst(Predicate.not(isDisabled)), Option.getOrElse(() => focusedIndex));
4
6
  export const keyToIndex = (nextKey, previousKey, itemCount, focusedIndex, isDisabled) => {
@@ -0,0 +1,182 @@
1
+ import { Schema as S } from 'effect';
2
+ import type { Command } from '../../command';
3
+ import { type Html } from '../../html';
4
+ import type { AnchorConfig } from '../menu/anchor';
5
+ import { resolveTypeaheadMatch } from '../typeahead';
6
+ export { resolveTypeaheadMatch };
7
+ /** Schema for the activation trigger — whether the user interacted via mouse or keyboard. */
8
+ export declare const ActivationTrigger: S.Literal<["Pointer", "Keyboard"]>;
9
+ export type ActivationTrigger = typeof ActivationTrigger.Type;
10
+ /** Schema for the transition animation state, tracking enter/leave phases for CSS transition coordination. */
11
+ export declare const TransitionState: S.Literal<["Idle", "EnterStart", "EnterAnimating", "LeaveStart", "LeaveAnimating"]>;
12
+ export type TransitionState = typeof TransitionState.Type;
13
+ /** Schema for the listbox orientation — whether items flow vertically or horizontally. */
14
+ export declare const Orientation: S.Literal<["Vertical", "Horizontal"]>;
15
+ export type Orientation = typeof Orientation.Type;
16
+ /** Schema for the listbox component's state, tracking open/closed status, active item, selected items, activation trigger, and typeahead search. */
17
+ export declare const Model: S.Struct<{
18
+ id: typeof S.String;
19
+ isOpen: typeof S.Boolean;
20
+ isAnimated: typeof S.Boolean;
21
+ isModal: typeof S.Boolean;
22
+ isMultiple: typeof S.Boolean;
23
+ orientation: S.Literal<["Vertical", "Horizontal"]>;
24
+ transitionState: S.Literal<["Idle", "EnterStart", "EnterAnimating", "LeaveStart", "LeaveAnimating"]>;
25
+ maybeActiveItemIndex: S.OptionFromSelf<typeof S.Number>;
26
+ activationTrigger: S.Literal<["Pointer", "Keyboard"]>;
27
+ searchQuery: typeof S.String;
28
+ searchVersion: typeof S.Number;
29
+ selectedItems: S.Array$<typeof S.String>;
30
+ maybeLastPointerPosition: S.OptionFromSelf<S.Struct<{
31
+ screenX: typeof S.Number;
32
+ screenY: typeof S.Number;
33
+ }>>;
34
+ maybeLastButtonPointerType: S.OptionFromSelf<typeof S.String>;
35
+ }>;
36
+ export type Model = typeof Model.Type;
37
+ /** Sent when the listbox opens via button click or keyboard. Contains an optional initial active item index — None for pointer, Some for keyboard. */
38
+ export declare const Opened: import("../../schema").CallableTaggedStruct<"Opened", {
39
+ maybeActiveItemIndex: S.OptionFromSelf<typeof S.Number>;
40
+ }>;
41
+ /** Sent when the listbox closes via Escape key or backdrop click. */
42
+ export declare const Closed: import("../../schema").CallableTaggedStruct<"Closed", {}>;
43
+ /** Sent when focus leaves the listbox items container via Tab key or blur. */
44
+ export declare const ClosedByTab: import("../../schema").CallableTaggedStruct<"ClosedByTab", {}>;
45
+ /** Sent when an item is highlighted via arrow keys or mouse hover. Includes activation trigger. */
46
+ export declare const ActivatedItem: import("../../schema").CallableTaggedStruct<"ActivatedItem", {
47
+ index: typeof S.Number;
48
+ activationTrigger: S.Literal<["Pointer", "Keyboard"]>;
49
+ }>;
50
+ /** Sent when the mouse leaves an enabled item. */
51
+ export declare const DeactivatedItem: import("../../schema").CallableTaggedStruct<"DeactivatedItem", {}>;
52
+ /** Sent when an item is selected via Enter, Space, or click. Contains the item's string value. */
53
+ export declare const SelectedItem: import("../../schema").CallableTaggedStruct<"SelectedItem", {
54
+ item: typeof S.String;
55
+ }>;
56
+ /** Sent when Enter or Space is pressed on the active item, triggering a programmatic click on the DOM element. */
57
+ export declare const RequestedItemClick: import("../../schema").CallableTaggedStruct<"RequestedItemClick", {
58
+ index: typeof S.Number;
59
+ }>;
60
+ /** Sent when a printable character is typed for typeahead search. */
61
+ export declare const Searched: import("../../schema").CallableTaggedStruct<"Searched", {
62
+ key: typeof S.String;
63
+ maybeTargetIndex: S.OptionFromSelf<typeof S.Number>;
64
+ }>;
65
+ /** Sent after the search debounce period to clear the accumulated query. */
66
+ export declare const ClearedSearch: import("../../schema").CallableTaggedStruct<"ClearedSearch", {
67
+ version: typeof S.Number;
68
+ }>;
69
+ /** Sent when the pointer moves over a listbox item, carrying screen coordinates for tracked-pointer comparison. */
70
+ export declare const MovedPointerOverItem: import("../../schema").CallableTaggedStruct<"MovedPointerOverItem", {
71
+ index: typeof S.Number;
72
+ screenX: typeof S.Number;
73
+ screenY: typeof S.Number;
74
+ }>;
75
+ /** Placeholder message used when no action is needed. */
76
+ export declare const NoOp: import("../../schema").CallableTaggedStruct<"NoOp", {}>;
77
+ /** Sent internally when a double-rAF completes, advancing the transition to its animating phase. */
78
+ export declare const AdvancedTransitionFrame: import("../../schema").CallableTaggedStruct<"AdvancedTransitionFrame", {}>;
79
+ /** Sent internally when all CSS transitions on the listbox items container have completed. */
80
+ export declare const EndedTransition: import("../../schema").CallableTaggedStruct<"EndedTransition", {}>;
81
+ /** Sent internally when the listbox button moves in the viewport during a leave transition, cancelling the animation. */
82
+ export declare const DetectedButtonMovement: import("../../schema").CallableTaggedStruct<"DetectedButtonMovement", {}>;
83
+ /** Sent when the user presses a pointer device on the listbox button. Records pointer type for click handling. */
84
+ export declare const PressedPointerOnButton: import("../../schema").CallableTaggedStruct<"PressedPointerOnButton", {
85
+ pointerType: typeof S.String;
86
+ button: typeof S.Number;
87
+ }>;
88
+ /** Union of all messages the listbox component can produce. */
89
+ export declare const Message: S.Union<[import("../../schema").CallableTaggedStruct<"Opened", {
90
+ maybeActiveItemIndex: S.OptionFromSelf<typeof S.Number>;
91
+ }>, import("../../schema").CallableTaggedStruct<"Closed", {}>, import("../../schema").CallableTaggedStruct<"ClosedByTab", {}>, import("../../schema").CallableTaggedStruct<"ActivatedItem", {
92
+ index: typeof S.Number;
93
+ activationTrigger: S.Literal<["Pointer", "Keyboard"]>;
94
+ }>, import("../../schema").CallableTaggedStruct<"DeactivatedItem", {}>, import("../../schema").CallableTaggedStruct<"SelectedItem", {
95
+ item: typeof S.String;
96
+ }>, import("../../schema").CallableTaggedStruct<"MovedPointerOverItem", {
97
+ index: typeof S.Number;
98
+ screenX: typeof S.Number;
99
+ screenY: typeof S.Number;
100
+ }>, import("../../schema").CallableTaggedStruct<"RequestedItemClick", {
101
+ index: typeof S.Number;
102
+ }>, import("../../schema").CallableTaggedStruct<"Searched", {
103
+ key: typeof S.String;
104
+ maybeTargetIndex: S.OptionFromSelf<typeof S.Number>;
105
+ }>, import("../../schema").CallableTaggedStruct<"ClearedSearch", {
106
+ version: typeof S.Number;
107
+ }>, import("../../schema").CallableTaggedStruct<"NoOp", {}>, import("../../schema").CallableTaggedStruct<"AdvancedTransitionFrame", {}>, import("../../schema").CallableTaggedStruct<"EndedTransition", {}>, import("../../schema").CallableTaggedStruct<"DetectedButtonMovement", {}>, import("../../schema").CallableTaggedStruct<"PressedPointerOnButton", {
108
+ pointerType: typeof S.String;
109
+ button: typeof S.Number;
110
+ }>]>;
111
+ export type Opened = typeof Opened.Type;
112
+ export type Closed = typeof Closed.Type;
113
+ export type ClosedByTab = typeof ClosedByTab.Type;
114
+ export type ActivatedItem = typeof ActivatedItem.Type;
115
+ export type DeactivatedItem = typeof DeactivatedItem.Type;
116
+ export type SelectedItem = typeof SelectedItem.Type;
117
+ export type MovedPointerOverItem = typeof MovedPointerOverItem.Type;
118
+ export type RequestedItemClick = typeof RequestedItemClick.Type;
119
+ export type Searched = typeof Searched.Type;
120
+ export type ClearedSearch = typeof ClearedSearch.Type;
121
+ export type NoOp = typeof NoOp.Type;
122
+ export type AdvancedTransitionFrame = typeof AdvancedTransitionFrame.Type;
123
+ export type EndedTransition = typeof EndedTransition.Type;
124
+ export type DetectedButtonMovement = typeof DetectedButtonMovement.Type;
125
+ export type PressedPointerOnButton = typeof PressedPointerOnButton.Type;
126
+ export type Message = typeof Message.Type;
127
+ /** Configuration for creating a listbox model with `init`. `isAnimated` enables CSS transition coordination (default `false`). `isModal` locks page scroll and inerts other elements when open (default `false`). `isMultiple` enables multi-select with toggle behavior (default `false`). `selectedItems` sets the initial selection (default `[]`). */
128
+ export type InitConfig = Readonly<{
129
+ id: string;
130
+ isAnimated?: boolean;
131
+ isModal?: boolean;
132
+ isMultiple?: boolean;
133
+ orientation?: Orientation;
134
+ selectedItems?: ReadonlyArray<string>;
135
+ }>;
136
+ /** Creates an initial listbox model from a config. Defaults to closed with no active item and no selection. */
137
+ export declare const init: (config: InitConfig) => Model;
138
+ type UpdateReturn = [Model, ReadonlyArray<Command<Message>>];
139
+ /** Processes a listbox message and returns the next model and commands. */
140
+ export declare const update: (model: Model, message: Message) => UpdateReturn;
141
+ /** Configuration for an individual listbox item's appearance. */
142
+ export type ItemConfig = Readonly<{
143
+ className: string;
144
+ content: Html;
145
+ }>;
146
+ /** Configuration for a group heading rendered above a group of items. */
147
+ export type GroupHeading = Readonly<{
148
+ content: Html;
149
+ className: string;
150
+ }>;
151
+ /** Configuration for rendering a listbox with `view`. */
152
+ export type ViewConfig<Message, Item> = Readonly<{
153
+ model: Model;
154
+ toMessage: (message: Opened | Closed | ClosedByTab | ActivatedItem | DeactivatedItem | SelectedItem | MovedPointerOverItem | RequestedItemClick | Searched | PressedPointerOnButton | NoOp) => Message;
155
+ items: ReadonlyArray<Item>;
156
+ itemToConfig: (item: Item, context: Readonly<{
157
+ isActive: boolean;
158
+ isDisabled: boolean;
159
+ isSelected: boolean;
160
+ }>) => ItemConfig;
161
+ isItemDisabled?: (item: Item, index: number) => boolean;
162
+ itemToSearchText?: (item: Item, index: number) => string;
163
+ itemToValue?: (item: Item) => string;
164
+ isButtonDisabled?: boolean;
165
+ buttonContent: Html;
166
+ buttonClassName: string;
167
+ itemsClassName: string;
168
+ backdropClassName: string;
169
+ className?: string;
170
+ itemGroupKey?: (item: Item, index: number) => string;
171
+ groupToHeading?: (groupKey: string) => GroupHeading | undefined;
172
+ groupClassName?: string;
173
+ separatorClassName?: string;
174
+ anchor?: AnchorConfig;
175
+ name?: string;
176
+ form?: string;
177
+ isDisabled?: boolean;
178
+ isInvalid?: boolean;
179
+ }>;
180
+ /** Renders a headless listbox with typeahead search, keyboard navigation, selection tracking, and aria-activedescendant focus management. */
181
+ export declare const view: <Message, Item>(config: ViewConfig<Message, Item>) => Html;
182
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/listbox/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,509 @@
1
+ import { Array, Effect, Match as M, Option, Predicate, Schema as S, String as Str, pipe, } from 'effect';
2
+ import { OptionExt } from '../../effectExtensions';
3
+ import { html } from '../../html';
4
+ import { m } from '../../message';
5
+ import { evo } from '../../struct';
6
+ import * as Task from '../../task';
7
+ import { groupContiguous } from '../group';
8
+ import { findFirstEnabledIndex, isPrintableKey, keyToIndex } from '../keyboard';
9
+ import { anchorHooks } from '../menu/anchor';
10
+ import { resolveTypeaheadMatch } from '../typeahead';
11
+ export { resolveTypeaheadMatch };
12
+ // MODEL
13
+ /** Schema for the activation trigger — whether the user interacted via mouse or keyboard. */
14
+ export const ActivationTrigger = S.Literal('Pointer', 'Keyboard');
15
+ /** Schema for the transition animation state, tracking enter/leave phases for CSS transition coordination. */
16
+ export const TransitionState = S.Literal('Idle', 'EnterStart', 'EnterAnimating', 'LeaveStart', 'LeaveAnimating');
17
+ /** Schema for the listbox orientation — whether items flow vertically or horizontally. */
18
+ export const Orientation = S.Literal('Vertical', 'Horizontal');
19
+ /** Schema for the listbox component's state, tracking open/closed status, active item, selected items, activation trigger, and typeahead search. */
20
+ export const Model = S.Struct({
21
+ id: S.String,
22
+ isOpen: S.Boolean,
23
+ isAnimated: S.Boolean,
24
+ isModal: S.Boolean,
25
+ isMultiple: S.Boolean,
26
+ orientation: Orientation,
27
+ transitionState: TransitionState,
28
+ maybeActiveItemIndex: S.OptionFromSelf(S.Number),
29
+ activationTrigger: ActivationTrigger,
30
+ searchQuery: S.String,
31
+ searchVersion: S.Number,
32
+ selectedItems: S.Array(S.String),
33
+ maybeLastPointerPosition: S.OptionFromSelf(S.Struct({ screenX: S.Number, screenY: S.Number })),
34
+ maybeLastButtonPointerType: S.OptionFromSelf(S.String),
35
+ });
36
+ // MESSAGE
37
+ /** Sent when the listbox opens via button click or keyboard. Contains an optional initial active item index — None for pointer, Some for keyboard. */
38
+ export const Opened = m('Opened', {
39
+ maybeActiveItemIndex: S.OptionFromSelf(S.Number),
40
+ });
41
+ /** Sent when the listbox closes via Escape key or backdrop click. */
42
+ export const Closed = m('Closed');
43
+ /** Sent when focus leaves the listbox items container via Tab key or blur. */
44
+ export const ClosedByTab = m('ClosedByTab');
45
+ /** Sent when an item is highlighted via arrow keys or mouse hover. Includes activation trigger. */
46
+ export const ActivatedItem = m('ActivatedItem', {
47
+ index: S.Number,
48
+ activationTrigger: ActivationTrigger,
49
+ });
50
+ /** Sent when the mouse leaves an enabled item. */
51
+ export const DeactivatedItem = m('DeactivatedItem');
52
+ /** Sent when an item is selected via Enter, Space, or click. Contains the item's string value. */
53
+ export const SelectedItem = m('SelectedItem', { item: S.String });
54
+ /** Sent when Enter or Space is pressed on the active item, triggering a programmatic click on the DOM element. */
55
+ export const RequestedItemClick = m('RequestedItemClick', {
56
+ index: S.Number,
57
+ });
58
+ /** Sent when a printable character is typed for typeahead search. */
59
+ export const Searched = m('Searched', {
60
+ key: S.String,
61
+ maybeTargetIndex: S.OptionFromSelf(S.Number),
62
+ });
63
+ /** Sent after the search debounce period to clear the accumulated query. */
64
+ export const ClearedSearch = m('ClearedSearch', { version: S.Number });
65
+ /** Sent when the pointer moves over a listbox item, carrying screen coordinates for tracked-pointer comparison. */
66
+ export const MovedPointerOverItem = m('MovedPointerOverItem', {
67
+ index: S.Number,
68
+ screenX: S.Number,
69
+ screenY: S.Number,
70
+ });
71
+ /** Placeholder message used when no action is needed. */
72
+ export const NoOp = m('NoOp');
73
+ /** Sent internally when a double-rAF completes, advancing the transition to its animating phase. */
74
+ export const AdvancedTransitionFrame = m('AdvancedTransitionFrame');
75
+ /** Sent internally when all CSS transitions on the listbox items container have completed. */
76
+ export const EndedTransition = m('EndedTransition');
77
+ /** Sent internally when the listbox button moves in the viewport during a leave transition, cancelling the animation. */
78
+ export const DetectedButtonMovement = m('DetectedButtonMovement');
79
+ /** Sent when the user presses a pointer device on the listbox button. Records pointer type for click handling. */
80
+ export const PressedPointerOnButton = m('PressedPointerOnButton', {
81
+ pointerType: S.String,
82
+ button: S.Number,
83
+ });
84
+ /** Union of all messages the listbox component can produce. */
85
+ export const Message = S.Union(Opened, Closed, ClosedByTab, ActivatedItem, DeactivatedItem, SelectedItem, MovedPointerOverItem, RequestedItemClick, Searched, ClearedSearch, NoOp, AdvancedTransitionFrame, EndedTransition, DetectedButtonMovement, PressedPointerOnButton);
86
+ // INIT
87
+ const SEARCH_DEBOUNCE_MILLISECONDS = 350;
88
+ const LEFT_MOUSE_BUTTON = 0;
89
+ /** Creates an initial listbox model from a config. Defaults to closed with no active item and no selection. */
90
+ export const init = (config) => ({
91
+ id: config.id,
92
+ isOpen: false,
93
+ isAnimated: config.isAnimated ?? false,
94
+ isModal: config.isModal ?? false,
95
+ isMultiple: config.isMultiple ?? false,
96
+ orientation: config.orientation ?? 'Vertical',
97
+ transitionState: 'Idle',
98
+ maybeActiveItemIndex: Option.none(),
99
+ activationTrigger: 'Keyboard',
100
+ searchQuery: '',
101
+ searchVersion: 0,
102
+ selectedItems: config.selectedItems ?? [],
103
+ maybeLastPointerPosition: Option.none(),
104
+ maybeLastButtonPointerType: Option.none(),
105
+ });
106
+ // UPDATE
107
+ const closedModel = (model) => evo(model, {
108
+ isOpen: () => false,
109
+ transitionState: () => (model.isAnimated ? 'LeaveStart' : 'Idle'),
110
+ maybeActiveItemIndex: () => Option.none(),
111
+ activationTrigger: () => 'Keyboard',
112
+ searchQuery: () => '',
113
+ searchVersion: () => 0,
114
+ maybeLastPointerPosition: () => Option.none(),
115
+ maybeLastButtonPointerType: () => Option.none(),
116
+ });
117
+ const buttonSelector = (id) => `#${id}-button`;
118
+ const itemsSelector = (id) => `#${id}-items`;
119
+ const itemSelector = (id, index) => `#${id}-item-${index}`;
120
+ const withUpdateReturn = M.withReturnType();
121
+ /** Processes a listbox message and returns the next model and commands. */
122
+ export const update = (model, message) => {
123
+ const maybeNextFrame = OptionExt.when(model.isAnimated, Task.nextFrame.pipe(Effect.as(AdvancedTransitionFrame())));
124
+ const maybeLockScroll = OptionExt.when(model.isModal, Task.lockScroll.pipe(Effect.as(NoOp())));
125
+ const maybeUnlockScroll = OptionExt.when(model.isModal, Task.unlockScroll.pipe(Effect.as(NoOp())));
126
+ const maybeInertOthers = OptionExt.when(model.isModal, Task.inertOthers(model.id, [
127
+ buttonSelector(model.id),
128
+ itemsSelector(model.id),
129
+ ]).pipe(Effect.as(NoOp())));
130
+ const maybeRestoreInert = OptionExt.when(model.isModal, Task.restoreInert(model.id).pipe(Effect.as(NoOp())));
131
+ const focusButton = Task.focus(buttonSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp()));
132
+ return M.value(message).pipe(withUpdateReturn, M.tagsExhaustive({
133
+ Opened: ({ maybeActiveItemIndex }) => {
134
+ const nextModel = evo(model, {
135
+ isOpen: () => true,
136
+ transitionState: () => (model.isAnimated ? 'EnterStart' : 'Idle'),
137
+ maybeActiveItemIndex: () => maybeActiveItemIndex,
138
+ activationTrigger: () => Option.match(maybeActiveItemIndex, {
139
+ onNone: () => 'Pointer',
140
+ onSome: () => 'Keyboard',
141
+ }),
142
+ searchQuery: () => '',
143
+ searchVersion: () => 0,
144
+ maybeLastPointerPosition: () => Option.none(),
145
+ });
146
+ return [
147
+ nextModel,
148
+ pipe(Array.getSomes([maybeNextFrame, maybeLockScroll, maybeInertOthers]), Array.prepend(Task.focus(itemsSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
149
+ ];
150
+ },
151
+ Closed: () => [
152
+ closedModel(model),
153
+ pipe(Array.getSomes([
154
+ maybeNextFrame,
155
+ maybeUnlockScroll,
156
+ maybeRestoreInert,
157
+ ]), Array.prepend(focusButton)),
158
+ ],
159
+ ClosedByTab: () => [
160
+ closedModel(model),
161
+ Array.getSomes([maybeNextFrame, maybeUnlockScroll, maybeRestoreInert]),
162
+ ],
163
+ ActivatedItem: ({ index, activationTrigger }) => [
164
+ evo(model, {
165
+ maybeActiveItemIndex: () => Option.some(index),
166
+ activationTrigger: () => activationTrigger,
167
+ }),
168
+ activationTrigger === 'Keyboard'
169
+ ? [
170
+ Task.scrollIntoView(itemSelector(model.id, index)).pipe(Effect.ignore, Effect.as(NoOp())),
171
+ ]
172
+ : [],
173
+ ],
174
+ MovedPointerOverItem: ({ index, screenX, screenY }) => {
175
+ const isSamePosition = Option.exists(model.maybeLastPointerPosition, position => position.screenX === screenX && position.screenY === screenY);
176
+ if (isSamePosition) {
177
+ return [model, []];
178
+ }
179
+ return [
180
+ evo(model, {
181
+ maybeActiveItemIndex: () => Option.some(index),
182
+ activationTrigger: () => 'Pointer',
183
+ maybeLastPointerPosition: () => Option.some({ screenX, screenY }),
184
+ }),
185
+ [],
186
+ ];
187
+ },
188
+ DeactivatedItem: () => model.activationTrigger === 'Pointer'
189
+ ? [evo(model, { maybeActiveItemIndex: () => Option.none() }), []]
190
+ : [model, []],
191
+ SelectedItem: ({ item }) => {
192
+ if (model.isMultiple) {
193
+ const nextSelectedItems = Array.contains(model.selectedItems, item)
194
+ ? Array.filter(model.selectedItems, selected => selected !== item)
195
+ : Array.append(model.selectedItems, item);
196
+ return [evo(model, { selectedItems: () => nextSelectedItems }), []];
197
+ }
198
+ return [
199
+ evo(closedModel(model), {
200
+ selectedItems: () => [item],
201
+ }),
202
+ pipe(Array.getSomes([
203
+ maybeNextFrame,
204
+ maybeUnlockScroll,
205
+ maybeRestoreInert,
206
+ ]), Array.prepend(focusButton)),
207
+ ];
208
+ },
209
+ RequestedItemClick: ({ index }) => [
210
+ model,
211
+ [
212
+ Task.clickElement(itemSelector(model.id, index)).pipe(Effect.ignore, Effect.as(NoOp())),
213
+ ],
214
+ ],
215
+ Searched: ({ key, maybeTargetIndex }) => {
216
+ const nextSearchQuery = model.searchQuery + key;
217
+ const nextSearchVersion = model.searchVersion + 1;
218
+ return [
219
+ evo(model, {
220
+ searchQuery: () => nextSearchQuery,
221
+ searchVersion: () => nextSearchVersion,
222
+ maybeActiveItemIndex: () => Option.orElse(maybeTargetIndex, () => model.maybeActiveItemIndex),
223
+ }),
224
+ [
225
+ Task.delay(SEARCH_DEBOUNCE_MILLISECONDS).pipe(Effect.as(ClearedSearch({ version: nextSearchVersion }))),
226
+ ],
227
+ ];
228
+ },
229
+ ClearedSearch: ({ version }) => {
230
+ if (version !== model.searchVersion) {
231
+ return [model, []];
232
+ }
233
+ return [evo(model, { searchQuery: () => '' }), []];
234
+ },
235
+ AdvancedTransitionFrame: () => M.value(model.transitionState).pipe(withUpdateReturn, M.when('EnterStart', () => [
236
+ evo(model, { transitionState: () => 'EnterAnimating' }),
237
+ [
238
+ Task.waitForTransitions(itemsSelector(model.id)).pipe(Effect.as(EndedTransition())),
239
+ ],
240
+ ]), M.when('LeaveStart', () => [
241
+ evo(model, { transitionState: () => 'LeaveAnimating' }),
242
+ [
243
+ Effect.raceFirst(Task.detectElementMovement(buttonSelector(model.id)).pipe(Effect.as(DetectedButtonMovement())), Task.waitForTransitions(itemsSelector(model.id)).pipe(Effect.as(EndedTransition()))),
244
+ ],
245
+ ]), M.orElse(() => [model, []])),
246
+ EndedTransition: () => M.value(model.transitionState).pipe(withUpdateReturn, M.whenOr('EnterAnimating', 'LeaveAnimating', () => [
247
+ evo(model, { transitionState: () => 'Idle' }),
248
+ [],
249
+ ]), M.orElse(() => [model, []])),
250
+ DetectedButtonMovement: () => M.value(model.transitionState).pipe(withUpdateReturn, M.when('LeaveAnimating', () => [
251
+ evo(model, { transitionState: () => 'Idle' }),
252
+ [],
253
+ ]), M.orElse(() => [model, []])),
254
+ PressedPointerOnButton: ({ pointerType, button }) => {
255
+ const withPointerType = evo(model, {
256
+ maybeLastButtonPointerType: () => Option.some(pointerType),
257
+ });
258
+ if (pointerType !== 'mouse' || button !== LEFT_MOUSE_BUTTON) {
259
+ return [withPointerType, []];
260
+ }
261
+ if (model.isOpen) {
262
+ return [
263
+ closedModel(withPointerType),
264
+ pipe(Array.getSomes([
265
+ maybeNextFrame,
266
+ maybeUnlockScroll,
267
+ maybeRestoreInert,
268
+ ]), Array.prepend(focusButton)),
269
+ ];
270
+ }
271
+ const nextModel = evo(withPointerType, {
272
+ isOpen: () => true,
273
+ transitionState: () => (model.isAnimated ? 'EnterStart' : 'Idle'),
274
+ maybeActiveItemIndex: () => Option.none(),
275
+ activationTrigger: () => 'Pointer',
276
+ searchQuery: () => '',
277
+ searchVersion: () => 0,
278
+ maybeLastPointerPosition: () => Option.none(),
279
+ });
280
+ return [
281
+ nextModel,
282
+ pipe(Array.getSomes([maybeNextFrame, maybeLockScroll, maybeInertOthers]), Array.prepend(Task.focus(itemsSelector(model.id)).pipe(Effect.ignore, Effect.as(NoOp())))),
283
+ ];
284
+ },
285
+ NoOp: () => [model, []],
286
+ }));
287
+ };
288
+ const itemId = (id, index) => `${id}-item-${index}`;
289
+ /** Renders a headless listbox with typeahead search, keyboard navigation, selection tracking, and aria-activedescendant focus management. */
290
+ export const view = (config) => {
291
+ const { div, input, AriaActiveDescendant, AriaControls, AriaDisabled, AriaExpanded, AriaHasPopup, AriaLabelledBy, AriaOrientation, AriaSelected, Attribute, Class, DataAttribute, Id, Name, OnBlur, OnClick, OnDestroy, OnInsert, OnKeyDownPreventDefault, OnKeyUpPreventDefault, OnPointerDown, OnPointerLeave, OnPointerMove, Role, Style, Tabindex, Type, Value, keyed, } = html();
292
+ const { model: { id, isOpen, isMultiple, orientation, transitionState, maybeActiveItemIndex, selectedItems, searchQuery, maybeLastButtonPointerType, }, toMessage, items, itemToConfig, isItemDisabled, isButtonDisabled, buttonContent, buttonClassName, itemsClassName, backdropClassName, className, itemGroupKey, groupToHeading, groupClassName, separatorClassName, anchor, name, form, isDisabled, isInvalid, } = config;
293
+ const itemToValue = config.itemToValue ?? (item => String(item));
294
+ const itemToSearchText = config.itemToSearchText ?? (item => itemToValue(item));
295
+ const isLeaving = transitionState === 'LeaveStart' || transitionState === 'LeaveAnimating';
296
+ const isVisible = isOpen || isLeaving;
297
+ const transitionAttributes = M.value(transitionState).pipe(M.when('EnterStart', () => [
298
+ DataAttribute('closed', ''),
299
+ DataAttribute('enter', ''),
300
+ DataAttribute('transition', ''),
301
+ ]), M.when('EnterAnimating', () => [
302
+ DataAttribute('enter', ''),
303
+ DataAttribute('transition', ''),
304
+ ]), M.when('LeaveStart', () => [
305
+ DataAttribute('leave', ''),
306
+ DataAttribute('transition', ''),
307
+ ]), M.when('LeaveAnimating', () => [
308
+ DataAttribute('closed', ''),
309
+ DataAttribute('leave', ''),
310
+ DataAttribute('transition', ''),
311
+ ]), M.orElse(() => []));
312
+ const isItemDisabledByIndex = (index) => Predicate.isNotUndefined(isItemDisabled) &&
313
+ pipe(items, Array.get(index), Option.exists(item => isItemDisabled(item, index)));
314
+ const isButtonEffectivelyDisabled = isDisabled || isButtonDisabled;
315
+ const nextKey = orientation === 'Horizontal' ? 'ArrowRight' : 'ArrowDown';
316
+ const previousKey = orientation === 'Horizontal' ? 'ArrowLeft' : 'ArrowUp';
317
+ const navigationKeys = [
318
+ nextKey,
319
+ previousKey,
320
+ 'Home',
321
+ 'End',
322
+ 'PageUp',
323
+ 'PageDown',
324
+ ];
325
+ const isNavigationKey = (key) => Array.contains(navigationKeys, key);
326
+ const firstEnabledIndex = findFirstEnabledIndex(items.length, 0, isItemDisabledByIndex)(0, 1);
327
+ const lastEnabledIndex = findFirstEnabledIndex(items.length, 0, isItemDisabledByIndex)(items.length - 1, -1);
328
+ const selectedItemIndex = pipe(Array.head(selectedItems), Option.flatMap(selectedItem => Array.findFirstIndex(items, item => itemToValue(item) === selectedItem)));
329
+ const handleButtonKeyDown = (key) => M.value(key).pipe(M.whenOr('Enter', ' ', 'ArrowDown', () => Option.some(toMessage(Opened({
330
+ maybeActiveItemIndex: Option.orElse(selectedItemIndex, () => Option.some(firstEnabledIndex)),
331
+ })))), M.when('ArrowUp', () => Option.some(toMessage(Opened({
332
+ maybeActiveItemIndex: Option.orElse(selectedItemIndex, () => Option.some(lastEnabledIndex)),
333
+ })))), M.orElse(() => Option.none()));
334
+ const handleButtonPointerDown = (pointerType, button) => Option.some(toMessage(PressedPointerOnButton({
335
+ pointerType,
336
+ button,
337
+ })));
338
+ const handleButtonClick = () => {
339
+ const isMouse = Option.exists(maybeLastButtonPointerType, type => type === 'mouse');
340
+ if (isMouse) {
341
+ return toMessage(NoOp());
342
+ }
343
+ else if (isOpen) {
344
+ return toMessage(Closed());
345
+ }
346
+ else {
347
+ return toMessage(Opened({ maybeActiveItemIndex: Option.none() }));
348
+ }
349
+ };
350
+ const handleSpaceKeyUp = (key) => OptionExt.when(key === ' ', toMessage(NoOp()));
351
+ const resolveActiveIndex = (key) => Option.match(maybeActiveItemIndex, {
352
+ onNone: () => M.value(key).pipe(M.whenOr(previousKey, 'End', 'PageDown', () => lastEnabledIndex), M.orElse(() => firstEnabledIndex)),
353
+ onSome: activeIndex => keyToIndex(nextKey, previousKey, items.length, activeIndex, isItemDisabledByIndex)(key),
354
+ });
355
+ const searchForKey = (key) => {
356
+ const nextQuery = searchQuery + key;
357
+ const maybeTargetIndex = resolveTypeaheadMatch(items, nextQuery, maybeActiveItemIndex, isItemDisabledByIndex, itemToSearchText, Str.isNonEmpty(searchQuery));
358
+ return Option.some(toMessage(Searched({ key, maybeTargetIndex })));
359
+ };
360
+ const handleItemsKeyDown = (key) => M.value(key).pipe(M.when('Escape', () => Option.some(toMessage(Closed()))), M.when('Enter', () => Option.map(maybeActiveItemIndex, index => toMessage(RequestedItemClick({ index })))), M.when(' ', () => Str.isNonEmpty(searchQuery)
361
+ ? searchForKey(' ')
362
+ : Option.map(maybeActiveItemIndex, index => toMessage(RequestedItemClick({ index })))), M.when(isNavigationKey, () => Option.some(toMessage(ActivatedItem({
363
+ index: resolveActiveIndex(key),
364
+ activationTrigger: 'Keyboard',
365
+ })))), M.when(isPrintableKey, () => searchForKey(key)), M.orElse(() => Option.none()));
366
+ const buttonAttributes = [
367
+ Id(`${id}-button`),
368
+ Type('button'),
369
+ Class(buttonClassName),
370
+ AriaHasPopup('listbox'),
371
+ AriaExpanded(isVisible),
372
+ AriaControls(`${id}-items`),
373
+ ...(isButtonEffectivelyDisabled
374
+ ? [AriaDisabled(true), DataAttribute('disabled', '')]
375
+ : [
376
+ OnPointerDown(handleButtonPointerDown),
377
+ OnKeyDownPreventDefault(handleButtonKeyDown),
378
+ OnKeyUpPreventDefault(handleSpaceKeyUp),
379
+ OnClick(handleButtonClick()),
380
+ ]),
381
+ ...(isVisible ? [DataAttribute('open', '')] : []),
382
+ ...(isInvalid ? [DataAttribute('invalid', '')] : []),
383
+ ];
384
+ const maybeActiveDescendant = Option.match(maybeActiveItemIndex, {
385
+ onNone: () => [],
386
+ onSome: index => [AriaActiveDescendant(itemId(id, index))],
387
+ });
388
+ const hooks = anchor
389
+ ? anchorHooks({ buttonId: `${id}-button`, anchor })
390
+ : undefined;
391
+ const anchorAttributes = hooks
392
+ ? [
393
+ Style({ position: 'absolute', margin: '0', visibility: 'hidden' }),
394
+ OnInsert(hooks.onInsert),
395
+ OnDestroy(hooks.onDestroy),
396
+ ]
397
+ : [];
398
+ const itemsContainerAttributes = [
399
+ Id(`${id}-items`),
400
+ Role('listbox'),
401
+ AriaOrientation(Str.toLowerCase(orientation)),
402
+ ...(isMultiple ? [Attribute('aria-multiselectable', 'true')] : []),
403
+ AriaLabelledBy(`${id}-button`),
404
+ ...maybeActiveDescendant,
405
+ Tabindex(0),
406
+ Class(itemsClassName),
407
+ ...anchorAttributes,
408
+ ...transitionAttributes,
409
+ ...(isLeaving
410
+ ? []
411
+ : [
412
+ OnKeyDownPreventDefault(handleItemsKeyDown),
413
+ OnKeyUpPreventDefault(handleSpaceKeyUp),
414
+ OnBlur(toMessage(ClosedByTab())),
415
+ ]),
416
+ ];
417
+ const listboxItems = Array.map(items, (item, index) => {
418
+ const isActiveItem = Option.exists(maybeActiveItemIndex, activeIndex => activeIndex === index);
419
+ const isDisabledItem = isItemDisabledByIndex(index);
420
+ const isSelectedItem = Array.contains(selectedItems, itemToValue(item));
421
+ const itemConfig = itemToConfig(item, {
422
+ isActive: isActiveItem,
423
+ isDisabled: isDisabledItem,
424
+ isSelected: isSelectedItem,
425
+ });
426
+ const isInteractive = !isDisabledItem && !isLeaving;
427
+ return keyed('div')(itemId(id, index), [
428
+ Id(itemId(id, index)),
429
+ Role('option'),
430
+ AriaSelected(isSelectedItem),
431
+ Class(itemConfig.className),
432
+ ...(isActiveItem ? [DataAttribute('active', '')] : []),
433
+ ...(isSelectedItem ? [DataAttribute('selected', '')] : []),
434
+ ...(isDisabledItem
435
+ ? [AriaDisabled(true), DataAttribute('disabled', '')]
436
+ : []),
437
+ ...(isInteractive
438
+ ? [
439
+ OnClick(toMessage(SelectedItem({ item: itemToValue(item) }))),
440
+ OnPointerMove((screenX, screenY, pointerType) => OptionExt.when(pointerType !== 'touch', toMessage(MovedPointerOverItem({ index, screenX, screenY })))),
441
+ OnPointerLeave(pointerType => OptionExt.when(pointerType !== 'touch', toMessage(DeactivatedItem()))),
442
+ ]
443
+ : []),
444
+ ], [itemConfig.content]);
445
+ });
446
+ const renderGroupedItems = () => {
447
+ if (!itemGroupKey) {
448
+ return listboxItems;
449
+ }
450
+ const segments = groupContiguous(listboxItems, (_, index) => Array.get(items, index).pipe(Option.match({
451
+ onNone: () => '',
452
+ onSome: item => itemGroupKey(item, index),
453
+ })));
454
+ return Array.flatMap(segments, (segment, segmentIndex) => {
455
+ const maybeHeading = Option.fromNullable(groupToHeading?.(segment.key));
456
+ const headingId = `${id}-heading-${segment.key}`;
457
+ const headingElement = Option.match(maybeHeading, {
458
+ onNone: () => [],
459
+ onSome: heading => [
460
+ keyed('div')(headingId, [Id(headingId), Role('presentation'), Class(heading.className)], [heading.content]),
461
+ ],
462
+ });
463
+ const groupContent = [...headingElement, ...segment.items];
464
+ const groupElement = keyed('div')(`${id}-group-${segment.key}`, [
465
+ Role('group'),
466
+ ...(Option.isSome(maybeHeading) ? [AriaLabelledBy(headingId)] : []),
467
+ ...(groupClassName ? [Class(groupClassName)] : []),
468
+ ], groupContent);
469
+ const separator = segmentIndex > 0 && separatorClassName
470
+ ? [
471
+ keyed('div')(`${id}-separator-${segmentIndex}`, [Role('separator'), Class(separatorClassName)], []),
472
+ ]
473
+ : [];
474
+ return [...separator, groupElement];
475
+ });
476
+ };
477
+ const backdrop = keyed('div')(`${id}-backdrop`, [
478
+ Class(backdropClassName),
479
+ ...(isLeaving ? [] : [OnClick(toMessage(Closed()))]),
480
+ ], []);
481
+ const renderedItems = renderGroupedItems();
482
+ const visibleContent = [
483
+ backdrop,
484
+ keyed('div')(`${id}-items-container`, itemsContainerAttributes, renderedItems),
485
+ ];
486
+ const formAttribute = form ? [Attribute('form', form)] : [];
487
+ const hiddenInputs = name
488
+ ? Array.match(selectedItems, {
489
+ onEmpty: () => [input([Type('hidden'), Name(name), ...formAttribute])],
490
+ onNonEmpty: Array.map(selectedItem => input([
491
+ Type('hidden'),
492
+ Name(name),
493
+ Value(selectedItem),
494
+ ...formAttribute,
495
+ ])),
496
+ })
497
+ : [];
498
+ const wrapperAttributes = [
499
+ ...(className ? [Class(className)] : []),
500
+ ...(isVisible ? [DataAttribute('open', '')] : []),
501
+ ...(isDisabled ? [DataAttribute('disabled', '')] : []),
502
+ ...(isInvalid ? [DataAttribute('invalid', '')] : []),
503
+ ];
504
+ return div(wrapperAttributes, [
505
+ keyed('button')(`${id}-button`, buttonAttributes, [buttonContent]),
506
+ ...hiddenInputs,
507
+ ...(isVisible ? visibleContent : []),
508
+ ]);
509
+ };
@@ -0,0 +1,4 @@
1
+ export { init, update, view, Model, Message, TransitionState, Orientation, } from './index';
2
+ export type { ActivationTrigger, Opened, Closed, ClosedByTab, ActivatedItem, DeactivatedItem, SelectedItem, MovedPointerOverItem, Searched, ClearedSearch, NoOp, AdvancedTransitionFrame, EndedTransition, InitConfig, ViewConfig, ItemConfig, GroupHeading, } from './index';
3
+ export type { AnchorConfig } from '../menu/anchor';
4
+ //# sourceMappingURL=public.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/ui/listbox/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,MAAM,EACN,IAAI,EACJ,KAAK,EACL,OAAO,EACP,eAAe,EACf,WAAW,GACZ,MAAM,SAAS,CAAA;AAEhB,YAAY,EACV,iBAAiB,EACjB,MAAM,EACN,MAAM,EACN,WAAW,EACX,aAAa,EACb,eAAe,EACf,YAAY,EACZ,oBAAoB,EACpB,QAAQ,EACR,aAAa,EACb,IAAI,EACJ,uBAAuB,EACvB,eAAe,EACf,UAAU,EACV,UAAU,EACV,UAAU,EACV,YAAY,GACb,MAAM,SAAS,CAAA;AAEhB,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA"}
@@ -0,0 +1 @@
1
+ export { init, update, view, Model, Message, TransitionState, Orientation, } from './index';
@@ -1,6 +1,8 @@
1
- import { Option, Schema as S } from 'effect';
1
+ import { Schema as S } from 'effect';
2
2
  import type { Command } from '../../command';
3
3
  import { type Html } from '../../html';
4
+ import { groupContiguous } from '../group';
5
+ import { resolveTypeaheadMatch } from '../typeahead';
4
6
  import type { AnchorConfig } from './anchor';
5
7
  /** Schema for the activation trigger — whether the user interacted via mouse or keyboard. */
6
8
  export declare const ActivationTrigger: S.Literal<["Pointer", "Keyboard"]>;
@@ -182,14 +184,7 @@ export type ViewConfig<Message, Item extends string> = Readonly<{
182
184
  separatorClassName?: string;
183
185
  anchor?: AnchorConfig;
184
186
  }>;
185
- type Segment<A> = Readonly<{
186
- key: string;
187
- items: ReadonlyArray<A>;
188
- }>;
189
- export declare const groupContiguous: <A>(items: ReadonlyArray<A>, toKey: (item: A, index: number) => string) => ReadonlyArray<Segment<A>>;
190
- /** Finds the first enabled item whose search text starts with the query, searching forward from the active item and wrapping around. On a fresh search, starts after the active item; on a refinement, includes the active item. */
191
- export declare const resolveTypeaheadMatch: <Item extends string>(items: ReadonlyArray<Item>, query: string, maybeActiveItemIndex: Option.Option<number>, isDisabled: (index: number) => boolean, itemToSearchText: (item: Item, index: number) => string, isRefinement: boolean) => Option.Option<number>;
187
+ export { groupContiguous, resolveTypeaheadMatch };
192
188
  /** Renders a headless menu with typeahead search, keyboard navigation, and aria-activedescendant focus management. */
193
189
  export declare const view: <Message, Item extends string>(config: ViewConfig<Message, Item>) => Html;
194
- export {};
195
190
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/menu/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/menu/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,MAAM,IAAI,CAAC,EAGZ,MAAM,QAAQ,CAAA;AAEf,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAE5C,OAAO,EAAE,KAAK,IAAI,EAAQ,MAAM,YAAY,CAAA;AAI5C,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAE1C,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AAEpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAI5C,6FAA6F;AAC7F,eAAO,MAAM,iBAAiB,oCAAmC,CAAA;AACjE,MAAM,MAAM,iBAAiB,GAAG,OAAO,iBAAiB,CAAC,IAAI,CAAA;AAE7D,8GAA8G;AAC9G,eAAO,MAAM,eAAe,qFAM3B,CAAA;AACD,MAAM,MAAM,eAAe,GAAG,OAAO,eAAe,CAAC,IAAI,CAAA;AAQzD,iIAAiI;AACjI,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;EAehB,CAAA;AAEF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAIrC,mJAAmJ;AACnJ,eAAO,MAAM,MAAM;;EAEjB,CAAA;AACF,kEAAkE;AAClE,eAAO,MAAM,MAAM,2DAAc,CAAA;AACjC,2EAA2E;AAC3E,eAAO,MAAM,WAAW,gEAAmB,CAAA;AAC3C,mGAAmG;AACnG,eAAO,MAAM,aAAa;;;EAGxB,CAAA;AACF,kDAAkD;AAClD,eAAO,MAAM,eAAe,oEAAuB,CAAA;AACnD,gEAAgE;AAChE,eAAO,MAAM,YAAY;;EAAyC,CAAA;AAClE,kHAAkH;AAClH,eAAO,MAAM,kBAAkB;;EAE7B,CAAA;AACF,qEAAqE;AACrE,eAAO,MAAM,QAAQ;;;EAGnB,CAAA;AACF,4EAA4E;AAC5E,eAAO,MAAM,aAAa;;EAA4C,CAAA;AACtE,gHAAgH;AAChH,eAAO,MAAM,oBAAoB;;;;EAI/B,CAAA;AACF,yDAAyD;AACzD,eAAO,MAAM,IAAI,yDAAY,CAAA;AAC7B,oGAAoG;AACpG,eAAO,MAAM,uBAAuB,4EAA+B,CAAA;AACnE,2FAA2F;AAC3F,eAAO,MAAM,eAAe,oEAAuB,CAAA;AACnD,sHAAsH;AACtH,eAAO,MAAM,sBAAsB,2EAA8B,CAAA;AACjE,kHAAkH;AAClH,eAAO,MAAM,sBAAsB;;;;;;EAMjC,CAAA;AACF,uGAAuG;AACvG,eAAO,MAAM,sBAAsB;;;;EAIjC,CAAA;AAEF,4DAA4D;AAC5D,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAiBnB,CAAA;AAED,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC,IAAI,CAAA;AACvC,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC,IAAI,CAAA;AACvC,MAAM,MAAM,WAAW,GAAG,OAAO,WAAW,CAAC,IAAI,CAAA;AACjD,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAC,IAAI,CAAA;AACrD,MAAM,MAAM,eAAe,GAAG,OAAO,eAAe,CAAC,IAAI,CAAA;AACzD,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC,IAAI,CAAA;AACnD,MAAM,MAAM,oBAAoB,GAAG,OAAO,oBAAoB,CAAC,IAAI,CAAA;AACnE,MAAM,MAAM,kBAAkB,GAAG,OAAO,kBAAkB,CAAC,IAAI,CAAA;AAC/D,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;AAC3C,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAC,IAAI,CAAA;AACrD,MAAM,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,IAAI,CAAA;AACnC,MAAM,MAAM,uBAAuB,GAAG,OAAO,uBAAuB,CAAC,IAAI,CAAA;AACzE,MAAM,MAAM,eAAe,GAAG,OAAO,eAAe,CAAC,IAAI,CAAA;AACzD,MAAM,MAAM,sBAAsB,GAAG,OAAO,sBAAsB,CAAC,IAAI,CAAA;AACvE,MAAM,MAAM,sBAAsB,GAAG,OAAO,sBAAsB,CAAC,IAAI,CAAA;AACvE,MAAM,MAAM,sBAAsB,GAAG,OAAO,sBAAsB,CAAC,IAAI,CAAA;AAEvE,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AASzC,kNAAkN;AAClN,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAC,CAAA;AAEF,2FAA2F;AAC3F,eAAO,MAAM,IAAI,GAAI,QAAQ,UAAU,KAAG,KAaxC,CAAA;AAsBF,KAAK,YAAY,GAAG,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;AAG5D,wEAAwE;AACxE,eAAO,MAAM,MAAM,GAAI,OAAO,KAAK,EAAE,SAAS,OAAO,KAAG,YAuUvD,CAAA;AAID,8DAA8D;AAC9D,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,IAAI,CAAA;CACd,CAAC,CAAA;AAEF,yEAAyE;AACzE,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC;IAClC,OAAO,EAAE,IAAI,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;CAClB,CAAC,CAAA;AAEF,sDAAsD;AACtD,MAAM,MAAM,UAAU,CAAC,OAAO,EAAE,IAAI,SAAS,MAAM,IAAI,QAAQ,CAAC;IAC9D,KAAK,EAAE,KAAK,CAAA;IACZ,SAAS,EAAE,CACT,OAAO,EACH,MAAM,GACN,MAAM,GACN,WAAW,GACX,aAAa,GACb,eAAe,GACf,YAAY,GACZ,oBAAoB,GACpB,kBAAkB,GAClB,QAAQ,GACR,sBAAsB,GACtB,sBAAsB,GACtB,IAAI,KACL,OAAO,CAAA;IACZ,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,CAAA;IAC1B,YAAY,EAAE,CACZ,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,QAAQ,CAAC;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,UAAU,EAAE,OAAO,CAAA;KAAE,CAAC,KAC1D,UAAU,CAAA;IACf,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAA;IACvD,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IACxD,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,aAAa,EAAE,IAAI,CAAA;IACnB,eAAe,EAAE,MAAM,CAAA;IACvB,cAAc,EAAE,MAAM,CAAA;IACtB,iBAAiB,EAAE,MAAM,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IACpD,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,YAAY,GAAG,SAAS,CAAA;IAC/D,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,MAAM,CAAC,EAAE,YAAY,CAAA;CACtB,CAAC,CAAA;AAEF,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,CAAA;AAIjD,sHAAsH;AACtH,eAAO,MAAM,IAAI,GAAI,OAAO,EAAE,IAAI,SAAS,MAAM,EAC/C,QAAQ,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,KAChC,IAkaF,CAAA"}
@@ -1,10 +1,12 @@
1
- import { Array, Effect, Match as M, Option, Schema as S, String as Str, pipe, } from 'effect';
1
+ import { Array, Effect, Match as M, Option, Predicate, Schema as S, String as Str, pipe, } from 'effect';
2
2
  import { OptionExt } from '../../effectExtensions';
3
3
  import { html } from '../../html';
4
4
  import { m } from '../../message';
5
5
  import { evo } from '../../struct';
6
6
  import * as Task from '../../task';
7
- import { findFirstEnabledIndex, keyToIndex, wrapIndex } from '../keyboard';
7
+ import { groupContiguous } from '../group';
8
+ import { findFirstEnabledIndex, isPrintableKey, keyToIndex } from '../keyboard';
9
+ import { resolveTypeaheadMatch } from '../typeahead';
8
10
  import { anchorHooks } from './anchor';
9
11
  // MODEL
10
12
  /** Schema for the activation trigger — whether the user interacted via mouse or keyboard. */
@@ -92,6 +94,7 @@ export const ReleasedPointerOnItems = m('ReleasedPointerOnItems', {
92
94
  export const Message = S.Union(Opened, Closed, ClosedByTab, ActivatedItem, DeactivatedItem, SelectedItem, MovedPointerOverItem, RequestedItemClick, Searched, ClearedSearch, NoOp, AdvancedTransitionFrame, EndedTransition, DetectedButtonMovement, PressedPointerOnButton, ReleasedPointerOnItems);
93
95
  // INIT
94
96
  const SEARCH_DEBOUNCE_MILLISECONDS = 350;
97
+ const LEFT_MOUSE_BUTTON = 0;
95
98
  const POINTER_HOLD_THRESHOLD_MILLISECONDS = 200;
96
99
  const POINTER_MOVEMENT_THRESHOLD_PIXELS = 5;
97
100
  /** Creates an initial menu model from a config. Defaults to closed with no active item. */
@@ -251,7 +254,7 @@ export const update = (model, message) => {
251
254
  const withPointerType = evo(model, {
252
255
  maybeLastButtonPointerType: () => Option.some(pointerType),
253
256
  });
254
- if (pointerType !== 'mouse' || button !== 0) {
257
+ if (pointerType !== 'mouse' || button !== LEFT_MOUSE_BUTTON) {
255
258
  return [withPointerType, []];
256
259
  }
257
260
  if (model.isOpen) {
@@ -303,30 +306,8 @@ export const update = (model, message) => {
303
306
  NoOp: () => [model, []],
304
307
  }));
305
308
  };
306
- export const groupContiguous = (items, toKey) => {
307
- const tagged = Array.map(items, (item, index) => ({
308
- key: toKey(item, index),
309
- item,
310
- }));
311
- return Array.chop(tagged, nonEmpty => {
312
- const key = Array.headNonEmpty(nonEmpty).key;
313
- const [matching, rest] = Array.span(nonEmpty, tagged => tagged.key === key);
314
- return [{ key, items: Array.map(matching, ({ item }) => item) }, rest];
315
- });
316
- };
309
+ export { groupContiguous, resolveTypeaheadMatch };
317
310
  const itemId = (id, index) => `${id}-item-${index}`;
318
- /** Finds the first enabled item whose search text starts with the query, searching forward from the active item and wrapping around. On a fresh search, starts after the active item; on a refinement, includes the active item. */
319
- export const resolveTypeaheadMatch = (items, query, maybeActiveItemIndex, isDisabled, itemToSearchText, isRefinement) => {
320
- const lowerQuery = Str.toLowerCase(query);
321
- const offset = isRefinement ? 0 : 1;
322
- const startIndex = Option.match(maybeActiveItemIndex, {
323
- onNone: () => 0,
324
- onSome: index => index + offset,
325
- });
326
- const isEnabledMatch = (index) => !isDisabled(index) &&
327
- pipe(items, Array.get(index), Option.exists(item => pipe(itemToSearchText(item, index), Str.toLowerCase, Str.startsWith(lowerQuery))));
328
- return pipe(items.length, Array.makeBy(step => wrapIndex(startIndex + step, items.length)), Array.findFirst(isEnabledMatch));
329
- };
330
311
  /** Renders a headless menu with typeahead search, keyboard navigation, and aria-activedescendant focus management. */
331
312
  export const view = (config) => {
332
313
  const { div, AriaActiveDescendant, AriaControls, AriaDisabled, AriaExpanded, AriaHasPopup, AriaLabelledBy, Class, DataAttribute, Id, OnBlur, OnClick, OnDestroy, OnInsert, OnKeyDownPreventDefault, OnKeyUpPreventDefault, OnPointerDown, OnPointerLeave, OnPointerMove, OnPointerUp, Role, Style, Tabindex, Type, keyed, } = html();
@@ -348,7 +329,7 @@ export const view = (config) => {
348
329
  DataAttribute('leave', ''),
349
330
  DataAttribute('transition', ''),
350
331
  ]), M.orElse(() => []));
351
- const isDisabled = (index) => !!isItemDisabled &&
332
+ const isDisabled = (index) => Predicate.isNotUndefined(isItemDisabled) &&
352
333
  pipe(items, Array.get(index), Option.exists(item => isItemDisabled(item, index)));
353
334
  const firstEnabledIndex = findFirstEnabledIndex(items.length, 0, isDisabled)(0, 1);
354
335
  const lastEnabledIndex = findFirstEnabledIndex(items.length, 0, isDisabled)(items.length - 1, -1);
@@ -388,7 +369,7 @@ export const view = (config) => {
388
369
  : Option.map(maybeActiveItemIndex, index => toMessage(RequestedItemClick({ index })))), M.whenOr('ArrowDown', 'ArrowUp', 'Home', 'End', 'PageUp', 'PageDown', () => Option.some(toMessage(ActivatedItem({
389
370
  index: resolveActiveIndex(key),
390
371
  activationTrigger: 'Keyboard',
391
- })))), M.when(key => key.length === 1, () => searchForKey(key)), M.orElse(() => Option.none()));
372
+ })))), M.when(isPrintableKey, () => searchForKey(key)), M.orElse(() => Option.none()));
392
373
  const handleItemsPointerUp = (screenX, screenY, pointerType, timeStamp) => OptionExt.when(pointerType === 'mouse', toMessage(ReleasedPointerOnItems({ screenX, screenY, timeStamp })));
393
374
  const buttonAttributes = [
394
375
  Id(`${id}-button`),
@@ -450,7 +431,6 @@ export const view = (config) => {
450
431
  return keyed('div')(itemId(id, index), [
451
432
  Id(itemId(id, index)),
452
433
  Role('menuitem'),
453
- Tabindex(-1),
454
434
  Class(itemConfig.className),
455
435
  ...(isActiveItem ? [DataAttribute('active', '')] : []),
456
436
  ...(isDisabledItem
@@ -0,0 +1,4 @@
1
+ import { Option } from 'effect';
2
+ /** Finds the first enabled item whose search text starts with the query, searching forward from the active item and wrapping around. On a fresh search, starts after the active item; on a refinement, includes the active item. */
3
+ export declare const resolveTypeaheadMatch: <Item>(items: ReadonlyArray<Item>, query: string, maybeActiveItemIndex: Option.Option<number>, isDisabled: (index: number) => boolean, itemToSearchText: (item: Item, index: number) => string, isRefinement: boolean) => Option.Option<number>;
4
+ //# sourceMappingURL=typeahead.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typeahead.d.ts","sourceRoot":"","sources":["../../src/ui/typeahead.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAuB,MAAM,QAAQ,CAAA;AAI3D,oOAAoO;AACpO,eAAO,MAAM,qBAAqB,GAAI,IAAI,EACxC,OAAO,aAAa,CAAC,IAAI,CAAC,EAC1B,OAAO,MAAM,EACb,sBAAsB,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAC3C,YAAY,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,EACtC,kBAAkB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,EACvD,cAAc,OAAO,KACpB,MAAM,CAAC,MAAM,CAAC,MAAM,CA4BtB,CAAA"}
@@ -0,0 +1,14 @@
1
+ import { Array, Option, String as Str, pipe } from 'effect';
2
+ import { wrapIndex } from './keyboard';
3
+ /** Finds the first enabled item whose search text starts with the query, searching forward from the active item and wrapping around. On a fresh search, starts after the active item; on a refinement, includes the active item. */
4
+ export const resolveTypeaheadMatch = (items, query, maybeActiveItemIndex, isDisabled, itemToSearchText, isRefinement) => {
5
+ const lowerQuery = Str.toLowerCase(query);
6
+ const offset = isRefinement ? 0 : 1;
7
+ const startIndex = Option.match(maybeActiveItemIndex, {
8
+ onNone: () => 0,
9
+ onSome: index => index + offset,
10
+ });
11
+ const isEnabledMatch = (index) => !isDisabled(index) &&
12
+ pipe(items, Array.get(index), Option.exists(item => pipe(itemToSearchText(item, index), Str.toLowerCase, Str.startsWith(lowerQuery))));
13
+ return pipe(items, Array.length, Array.makeBy(step => wrapIndex(startIndex + step, items.length)), Array.findFirst(isEnabledMatch));
14
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldkit",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Elm-inspired UI framework powered by Effect",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -63,6 +63,10 @@
63
63
  "types": "./dist/ui/disclosure/public.d.ts",
64
64
  "import": "./dist/ui/disclosure/public.js"
65
65
  },
66
+ "./ui/listbox": {
67
+ "types": "./dist/ui/listbox/public.d.ts",
68
+ "import": "./dist/ui/listbox/public.js"
69
+ },
66
70
  "./ui/menu": {
67
71
  "types": "./dist/ui/menu/public.d.ts",
68
72
  "import": "./dist/ui/menu/public.js"