@bareboard/react-native 0.0.0-stage → 0.1.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/LICENSE +22 -0
- package/README.md +24 -2
- package/dist/components/button/button.d.ts +12 -0
- package/dist/components/button/button.js +10 -0
- package/dist/components/checkbox/checkbox.d.ts +5 -0
- package/dist/components/checkbox/checkbox.js +7 -0
- package/dist/components/close-button/close-button.d.ts +5 -0
- package/dist/components/close-button/close-button.js +14 -0
- package/dist/components/combobox/combobox-machine-glue.d.ts +6 -0
- package/dist/components/combobox/combobox-machine-glue.js +16 -0
- package/dist/components/combobox/combobox-machine.d.ts +143 -0
- package/dist/components/combobox/combobox-machine.js +377 -0
- package/dist/components/combobox/combobox.d.ts +66 -0
- package/dist/components/combobox/combobox.js +199 -0
- package/dist/components/data-interactive/data-interactive.d.ts +4 -0
- package/dist/components/data-interactive/data-interactive.js +8 -0
- package/dist/components/description/description.d.ts +4 -0
- package/dist/components/description/description.js +6 -0
- package/dist/components/dialog/dialog.d.ts +22 -0
- package/dist/components/dialog/dialog.js +27 -0
- package/dist/components/disclosure/disclosure.d.ts +35 -0
- package/dist/components/disclosure/disclosure.js +35 -0
- package/dist/components/field/field.d.ts +6 -0
- package/dist/components/field/field.js +9 -0
- package/dist/components/fieldset/fieldset.d.ts +3 -0
- package/dist/components/fieldset/fieldset.js +3 -0
- package/dist/components/focus-trap/focus-trap.d.ts +5 -0
- package/dist/components/focus-trap/focus-trap.js +5 -0
- package/dist/components/input/input.d.ts +6 -0
- package/dist/components/input/input.js +9 -0
- package/dist/components/label/label.d.ts +6 -0
- package/dist/components/label/label.js +9 -0
- package/dist/components/legend/legend.d.ts +6 -0
- package/dist/components/legend/legend.js +9 -0
- package/dist/components/listbox/listbox-machine-glue.d.ts +6 -0
- package/dist/components/listbox/listbox-machine-glue.js +16 -0
- package/dist/components/listbox/listbox-machine.d.ts +140 -0
- package/dist/components/listbox/listbox-machine.js +347 -0
- package/dist/components/listbox/listbox.d.ts +53 -0
- package/dist/components/listbox/listbox.js +91 -0
- package/dist/components/listbox-label/listbox-label.d.ts +2 -0
- package/dist/components/listbox-label/listbox-label.js +2 -0
- package/dist/components/listbox-selected-option/listbox-selected-option.d.ts +7 -0
- package/dist/components/listbox-selected-option/listbox-selected-option.js +16 -0
- package/dist/components/menu/menu-machine-glue.d.ts +6 -0
- package/dist/components/menu/menu-machine-glue.js +16 -0
- package/dist/components/menu/menu-machine.d.ts +109 -0
- package/dist/components/menu/menu-machine.js +238 -0
- package/dist/components/menu/menu.d.ts +34 -0
- package/dist/components/menu/menu.js +60 -0
- package/dist/components/menu-heading/menu-heading.d.ts +4 -0
- package/dist/components/menu-heading/menu-heading.js +6 -0
- package/dist/components/menu-section/menu-section.d.ts +4 -0
- package/dist/components/menu-section/menu-section.js +6 -0
- package/dist/components/menu-separator/menu-separator.d.ts +4 -0
- package/dist/components/menu-separator/menu-separator.js +6 -0
- package/dist/components/popover/popover-machine-glue.d.ts +6 -0
- package/dist/components/popover/popover-machine-glue.js +16 -0
- package/dist/components/popover/popover-machine.d.ts +30 -0
- package/dist/components/popover/popover-machine.js +49 -0
- package/dist/components/popover/popover.d.ts +25 -0
- package/dist/components/popover/popover.js +37 -0
- package/dist/components/popover-backdrop/popover-backdrop.d.ts +6 -0
- package/dist/components/popover-backdrop/popover-backdrop.js +14 -0
- package/dist/components/popover-group/popover-group.d.ts +4 -0
- package/dist/components/popover-group/popover-group.js +6 -0
- package/dist/components/popover-overlay/popover-overlay.d.ts +4 -0
- package/dist/components/popover-overlay/popover-overlay.js +6 -0
- package/dist/components/portal/portal.d.ts +9 -0
- package/dist/components/portal/portal.js +8 -0
- package/dist/components/radio-group/radio-group.d.ts +29 -0
- package/dist/components/radio-group/radio-group.js +36 -0
- package/dist/components/select/select.d.ts +7 -0
- package/dist/components/select/select.js +10 -0
- package/dist/components/switch/switch.d.ts +20 -0
- package/dist/components/switch/switch.js +24 -0
- package/dist/components/tab-panels/tab-panels.d.ts +4 -0
- package/dist/components/tab-panels/tab-panels.js +6 -0
- package/dist/components/tabs/tabs.d.ts +29 -0
- package/dist/components/tabs/tabs.js +40 -0
- package/dist/components/textarea/textarea.d.ts +5 -0
- package/dist/components/textarea/textarea.js +7 -0
- package/dist/components/transition/transition.d.ts +21 -0
- package/dist/components/transition/transition.js +34 -0
- package/dist/hooks/use-controllable.d.ts +1 -0
- package/dist/hooks/use-controllable.js +34 -0
- package/dist/hooks/use-event.d.ts +1 -0
- package/dist/hooks/use-event.js +8 -0
- package/dist/hooks/use-iso-morphic-effect.d.ts +2 -0
- package/dist/hooks/use-iso-morphic-effect.js +3 -0
- package/dist/hooks/use-latest-value.d.ts +1 -0
- package/dist/hooks/use-latest-value.js +9 -0
- package/dist/hooks/use-on-unmount.d.ts +1 -0
- package/dist/hooks/use-on-unmount.js +18 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +33 -0
- package/dist/internal/close-provider.d.ts +6 -0
- package/dist/internal/close-provider.js +9 -0
- package/dist/internal/disabled.d.ts +5 -0
- package/dist/internal/disabled.js +9 -0
- package/dist/internal/toggle.d.ts +17 -0
- package/dist/internal/toggle.js +17 -0
- package/dist/machine.d.ts +33 -0
- package/dist/machine.js +107 -0
- package/dist/machines/stack-machine.d.ts +32 -0
- package/dist/machines/stack-machine.js +51 -0
- package/dist/react-glue.d.ts +2 -0
- package/dist/react-glue.js +9 -0
- package/dist/utils/calculate-active-index.d.ts +25 -0
- package/dist/utils/calculate-active-index.js +74 -0
- package/dist/utils/default-map.d.ts +5 -0
- package/dist/utils/default-map.js +15 -0
- package/dist/utils/disposables.d.ts +31 -0
- package/dist/utils/disposables.js +68 -0
- package/dist/utils/find-active-index.d.ts +3 -0
- package/dist/utils/find-active-index.js +12 -0
- package/dist/utils/match.d.ts +1 -0
- package/dist/utils/match.js +12 -0
- package/dist/utils/micro-task.d.ts +1 -0
- package/dist/utils/micro-task.js +13 -0
- package/dist/utils/render.d.ts +3 -0
- package/dist/utils/render.js +4 -0
- package/dist/utils/sort-by-order.d.ts +16 -0
- package/dist/utils/sort-by-order.js +33 -0
- package/dist/utils/typeahead.d.ts +1 -0
- package/dist/utils/typeahead.js +11 -0
- package/package.json +43 -3
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Machine } from '../../machine';
|
|
2
|
+
import { Focus } from '../../utils/calculate-active-index';
|
|
3
|
+
interface MutableRefObject<T> {
|
|
4
|
+
current: T;
|
|
5
|
+
}
|
|
6
|
+
export declare enum ListboxStates {
|
|
7
|
+
Open = 0,
|
|
8
|
+
Closed = 1
|
|
9
|
+
}
|
|
10
|
+
export declare enum ValueMode {
|
|
11
|
+
Single = 0,
|
|
12
|
+
Multi = 1
|
|
13
|
+
}
|
|
14
|
+
export declare enum ActivationTrigger {
|
|
15
|
+
Pointer = 0,
|
|
16
|
+
Other = 1
|
|
17
|
+
}
|
|
18
|
+
type ListboxOptionDataRef<T> = MutableRefObject<{
|
|
19
|
+
textValue?: string;
|
|
20
|
+
disabled: boolean;
|
|
21
|
+
value: T;
|
|
22
|
+
order?: number;
|
|
23
|
+
}>;
|
|
24
|
+
interface State<T> {
|
|
25
|
+
id: string;
|
|
26
|
+
dataRef: MutableRefObject<{
|
|
27
|
+
value: unknown;
|
|
28
|
+
disabled: boolean;
|
|
29
|
+
mode: ValueMode;
|
|
30
|
+
onChange: (value: T) => void;
|
|
31
|
+
compare(a: unknown, z: unknown): boolean;
|
|
32
|
+
isSelected(value: unknown): boolean;
|
|
33
|
+
}>;
|
|
34
|
+
listboxState: ListboxStates;
|
|
35
|
+
options: {
|
|
36
|
+
id: string;
|
|
37
|
+
top?: number;
|
|
38
|
+
dataRef: ListboxOptionDataRef<T>;
|
|
39
|
+
}[];
|
|
40
|
+
searchQuery: string;
|
|
41
|
+
activeOptionIndex: number | null;
|
|
42
|
+
activationTrigger: ActivationTrigger;
|
|
43
|
+
frozenValue: boolean;
|
|
44
|
+
pendingFocus: {
|
|
45
|
+
focus: Exclude<Focus, Focus.Specific>;
|
|
46
|
+
} | {
|
|
47
|
+
focus: Focus.Specific;
|
|
48
|
+
id: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export declare enum ActionTypes {
|
|
52
|
+
OpenListbox = 0,
|
|
53
|
+
CloseListbox = 1,
|
|
54
|
+
GoToOption = 2,
|
|
55
|
+
Search = 3,
|
|
56
|
+
ClearSearch = 4,
|
|
57
|
+
SelectOption = 5,
|
|
58
|
+
RegisterOptions = 6,
|
|
59
|
+
UnregisterOptions = 7,
|
|
60
|
+
SetOptionsTop = 8
|
|
61
|
+
}
|
|
62
|
+
type Actions<T> = {
|
|
63
|
+
type: ActionTypes.CloseListbox;
|
|
64
|
+
} | {
|
|
65
|
+
type: ActionTypes.OpenListbox;
|
|
66
|
+
focus: {
|
|
67
|
+
focus: Exclude<Focus, Focus.Specific>;
|
|
68
|
+
} | {
|
|
69
|
+
focus: Focus.Specific;
|
|
70
|
+
id: string;
|
|
71
|
+
};
|
|
72
|
+
} | {
|
|
73
|
+
type: ActionTypes.GoToOption;
|
|
74
|
+
focus: Focus.Specific;
|
|
75
|
+
id: string;
|
|
76
|
+
trigger?: ActivationTrigger;
|
|
77
|
+
} | {
|
|
78
|
+
type: ActionTypes.GoToOption;
|
|
79
|
+
focus: Exclude<Focus, Focus.Specific>;
|
|
80
|
+
trigger?: ActivationTrigger;
|
|
81
|
+
} | {
|
|
82
|
+
type: ActionTypes.Search;
|
|
83
|
+
value: string;
|
|
84
|
+
} | {
|
|
85
|
+
type: ActionTypes.ClearSearch;
|
|
86
|
+
} | {
|
|
87
|
+
type: ActionTypes.SelectOption;
|
|
88
|
+
value: T;
|
|
89
|
+
} | {
|
|
90
|
+
type: ActionTypes.RegisterOptions;
|
|
91
|
+
options: {
|
|
92
|
+
id: string;
|
|
93
|
+
dataRef: ListboxOptionDataRef<T>;
|
|
94
|
+
}[];
|
|
95
|
+
} | {
|
|
96
|
+
type: ActionTypes.UnregisterOptions;
|
|
97
|
+
options: string[];
|
|
98
|
+
} | {
|
|
99
|
+
type: ActionTypes.SetOptionsTop;
|
|
100
|
+
tops: {
|
|
101
|
+
id: string;
|
|
102
|
+
top: number;
|
|
103
|
+
}[];
|
|
104
|
+
};
|
|
105
|
+
export declare class ListboxMachine<T> extends Machine<State<T>, Actions<T>> {
|
|
106
|
+
static new({ id }: {
|
|
107
|
+
id: string;
|
|
108
|
+
}): ListboxMachine<unknown>;
|
|
109
|
+
constructor(initialState: State<T>);
|
|
110
|
+
actions: {
|
|
111
|
+
onChange: (newValue: T) => void;
|
|
112
|
+
setOptionTop: (id: string, top: number) => void;
|
|
113
|
+
registerOption: (id: string, dataRef: ListboxOptionDataRef<T>) => void;
|
|
114
|
+
unregisterOption: (id: string) => void;
|
|
115
|
+
goToOption: (focus: {
|
|
116
|
+
focus: Focus.Specific;
|
|
117
|
+
id: string;
|
|
118
|
+
} | {
|
|
119
|
+
focus: Exclude<Focus, Focus.Specific>;
|
|
120
|
+
}, trigger?: ActivationTrigger | undefined) => void;
|
|
121
|
+
closeListbox: () => void;
|
|
122
|
+
openListbox: (focus: {
|
|
123
|
+
focus: Exclude<Focus, Focus.Specific>;
|
|
124
|
+
} | {
|
|
125
|
+
focus: Focus.Specific;
|
|
126
|
+
id: string;
|
|
127
|
+
}) => void;
|
|
128
|
+
selectActiveOption: () => void;
|
|
129
|
+
selectOption: (value: T) => void;
|
|
130
|
+
search: (value: string) => void;
|
|
131
|
+
clearSearch: () => void;
|
|
132
|
+
};
|
|
133
|
+
selectors: {
|
|
134
|
+
activeDescendantId(state: State<T>): string | undefined;
|
|
135
|
+
isActive(state: State<T>, id: string): boolean;
|
|
136
|
+
hasFrozenValue(state: State<T>): boolean;
|
|
137
|
+
};
|
|
138
|
+
reduce(state: Readonly<State<T>>, action: Actions<T>): State<T>;
|
|
139
|
+
}
|
|
140
|
+
export {};
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { Machine, batch } from '../../machine';
|
|
2
|
+
import { ActionTypes as StackActionTypes, stackMachines } from '../../machines/stack-machine';
|
|
3
|
+
import { Focus, calculateActiveIndex } from '../../utils/calculate-active-index';
|
|
4
|
+
import { match } from '../../utils/match';
|
|
5
|
+
import { findActiveIndex } from '../../utils/find-active-index';
|
|
6
|
+
import { placeAtTops, sortByOrder } from '../../utils/sort-by-order';
|
|
7
|
+
import { findSearchMatchIndex } from '../../utils/typeahead';
|
|
8
|
+
export var ListboxStates;
|
|
9
|
+
(function (ListboxStates) {
|
|
10
|
+
ListboxStates[ListboxStates["Open"] = 0] = "Open";
|
|
11
|
+
ListboxStates[ListboxStates["Closed"] = 1] = "Closed";
|
|
12
|
+
})(ListboxStates || (ListboxStates = {}));
|
|
13
|
+
export var ValueMode;
|
|
14
|
+
(function (ValueMode) {
|
|
15
|
+
ValueMode[ValueMode["Single"] = 0] = "Single";
|
|
16
|
+
ValueMode[ValueMode["Multi"] = 1] = "Multi";
|
|
17
|
+
})(ValueMode || (ValueMode = {}));
|
|
18
|
+
export var ActivationTrigger;
|
|
19
|
+
(function (ActivationTrigger) {
|
|
20
|
+
ActivationTrigger[ActivationTrigger["Pointer"] = 0] = "Pointer";
|
|
21
|
+
ActivationTrigger[ActivationTrigger["Other"] = 1] = "Other";
|
|
22
|
+
})(ActivationTrigger || (ActivationTrigger = {}));
|
|
23
|
+
export var ActionTypes;
|
|
24
|
+
(function (ActionTypes) {
|
|
25
|
+
ActionTypes[ActionTypes["OpenListbox"] = 0] = "OpenListbox";
|
|
26
|
+
ActionTypes[ActionTypes["CloseListbox"] = 1] = "CloseListbox";
|
|
27
|
+
ActionTypes[ActionTypes["GoToOption"] = 2] = "GoToOption";
|
|
28
|
+
ActionTypes[ActionTypes["Search"] = 3] = "Search";
|
|
29
|
+
ActionTypes[ActionTypes["ClearSearch"] = 4] = "ClearSearch";
|
|
30
|
+
ActionTypes[ActionTypes["SelectOption"] = 5] = "SelectOption";
|
|
31
|
+
ActionTypes[ActionTypes["RegisterOptions"] = 6] = "RegisterOptions";
|
|
32
|
+
ActionTypes[ActionTypes["UnregisterOptions"] = 7] = "UnregisterOptions";
|
|
33
|
+
ActionTypes[ActionTypes["SetOptionsTop"] = 8] = "SetOptionsTop";
|
|
34
|
+
})(ActionTypes || (ActionTypes = {}));
|
|
35
|
+
let reducers = {
|
|
36
|
+
[ActionTypes.CloseListbox](state) {
|
|
37
|
+
if (state.dataRef.current.disabled)
|
|
38
|
+
return state;
|
|
39
|
+
if (state.listboxState === ListboxStates.Closed)
|
|
40
|
+
return state;
|
|
41
|
+
return {
|
|
42
|
+
...state,
|
|
43
|
+
activeOptionIndex: null,
|
|
44
|
+
pendingFocus: { focus: Focus.Nothing },
|
|
45
|
+
listboxState: ListboxStates.Closed,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
[ActionTypes.OpenListbox](state, action) {
|
|
49
|
+
if (state.dataRef.current.disabled)
|
|
50
|
+
return state;
|
|
51
|
+
if (state.listboxState === ListboxStates.Open)
|
|
52
|
+
return state;
|
|
53
|
+
// Check if we have a selected value that we can make active
|
|
54
|
+
let activeOptionIndex = state.activeOptionIndex;
|
|
55
|
+
let { isSelected } = state.dataRef.current;
|
|
56
|
+
let optionIdx = state.options.findIndex((option) => isSelected(option.dataRef.current.value));
|
|
57
|
+
if (optionIdx !== -1) {
|
|
58
|
+
activeOptionIndex = optionIdx;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
...state,
|
|
62
|
+
frozenValue: false,
|
|
63
|
+
pendingFocus: action.focus,
|
|
64
|
+
listboxState: ListboxStates.Open,
|
|
65
|
+
activeOptionIndex,
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
[ActionTypes.GoToOption](state, action) {
|
|
69
|
+
if (state.dataRef.current.disabled)
|
|
70
|
+
return state;
|
|
71
|
+
if (state.listboxState === ListboxStates.Closed)
|
|
72
|
+
return state;
|
|
73
|
+
let base = {
|
|
74
|
+
...state,
|
|
75
|
+
searchQuery: '',
|
|
76
|
+
activationTrigger: action.trigger ?? ActivationTrigger.Other,
|
|
77
|
+
};
|
|
78
|
+
// There is no need to look at the options if we know that we don't want to focus anything
|
|
79
|
+
if (action.focus === Focus.Nothing) {
|
|
80
|
+
return {
|
|
81
|
+
...base,
|
|
82
|
+
activeOptionIndex: null,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// There is no need to look at the options if we know exactly where to go
|
|
86
|
+
if (action.focus === Focus.Specific) {
|
|
87
|
+
return {
|
|
88
|
+
...base,
|
|
89
|
+
activeOptionIndex: state.options.findIndex((o) => o.id === action.id),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
let activeOptionIndex = calculateActiveIndex(action, {
|
|
93
|
+
resolveItems: () => state.options,
|
|
94
|
+
resolveActiveIndex: () => state.activeOptionIndex,
|
|
95
|
+
resolveId: (option) => option.id,
|
|
96
|
+
resolveDisabled: (option) => option.dataRef.current.disabled,
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
...base,
|
|
100
|
+
activeOptionIndex,
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
[ActionTypes.Search]: (state, action) => {
|
|
104
|
+
if (state.dataRef.current.disabled)
|
|
105
|
+
return state;
|
|
106
|
+
if (state.listboxState === ListboxStates.Closed)
|
|
107
|
+
return state;
|
|
108
|
+
let wasAlreadySearching = state.searchQuery !== '';
|
|
109
|
+
let searchQuery = state.searchQuery + action.value.toLowerCase();
|
|
110
|
+
let matchIdx = findSearchMatchIndex(state.options, state.activeOptionIndex, wasAlreadySearching, (option) => !option.dataRef.current.disabled &&
|
|
111
|
+
!!option.dataRef.current.textValue?.startsWith(searchQuery));
|
|
112
|
+
if (matchIdx === -1 || matchIdx === state.activeOptionIndex)
|
|
113
|
+
return { ...state, searchQuery };
|
|
114
|
+
return {
|
|
115
|
+
...state,
|
|
116
|
+
searchQuery,
|
|
117
|
+
activeOptionIndex: matchIdx,
|
|
118
|
+
activationTrigger: ActivationTrigger.Other,
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
[ActionTypes.ClearSearch](state) {
|
|
122
|
+
if (state.dataRef.current.disabled)
|
|
123
|
+
return state;
|
|
124
|
+
if (state.listboxState === ListboxStates.Closed)
|
|
125
|
+
return state;
|
|
126
|
+
if (state.searchQuery === '')
|
|
127
|
+
return state;
|
|
128
|
+
return { ...state, searchQuery: '' };
|
|
129
|
+
},
|
|
130
|
+
[ActionTypes.SelectOption](state) {
|
|
131
|
+
if (state.dataRef.current.mode === ValueMode.Single) {
|
|
132
|
+
// The moment you select a value in single value mode, we want to close
|
|
133
|
+
// the listbox and freeze the value to prevent UI flicker.
|
|
134
|
+
return { ...state, frozenValue: true };
|
|
135
|
+
}
|
|
136
|
+
// In multi-value mode there is no state change, but the event listener for
|
|
137
|
+
// `SelectOption` must still run, so a new object is returned.
|
|
138
|
+
return { ...state };
|
|
139
|
+
},
|
|
140
|
+
[ActionTypes.RegisterOptions]: (state, action) => {
|
|
141
|
+
let options = sortByOrder(state.options.concat(action.options));
|
|
142
|
+
// Sorting can move the active option, so look it up again.
|
|
143
|
+
let activeOptionIndex = findActiveIndex(state.options, state.activeOptionIndex, options);
|
|
144
|
+
if (state.pendingFocus.focus !== Focus.Nothing) {
|
|
145
|
+
activeOptionIndex = calculateActiveIndex(state.pendingFocus, {
|
|
146
|
+
resolveItems: () => options,
|
|
147
|
+
resolveActiveIndex: () => state.activeOptionIndex,
|
|
148
|
+
resolveId: (item) => item.id,
|
|
149
|
+
resolveDisabled: (item) => item.dataRef.current.disabled,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
// Check if we need to make the newly registered option active.
|
|
153
|
+
if (state.activeOptionIndex === null) {
|
|
154
|
+
let { isSelected } = state.dataRef.current;
|
|
155
|
+
if (isSelected) {
|
|
156
|
+
let idx = options.findIndex((option) => isSelected?.(option.dataRef.current.value));
|
|
157
|
+
if (idx !== -1)
|
|
158
|
+
activeOptionIndex = idx;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
...state,
|
|
163
|
+
options,
|
|
164
|
+
activeOptionIndex,
|
|
165
|
+
pendingFocus: { focus: Focus.Nothing },
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
[ActionTypes.SetOptionsTop]: (state, action) => {
|
|
169
|
+
let options = placeAtTops(state.options, action.tops);
|
|
170
|
+
if (!options)
|
|
171
|
+
return state;
|
|
172
|
+
return {
|
|
173
|
+
...state,
|
|
174
|
+
options,
|
|
175
|
+
activeOptionIndex: findActiveIndex(state.options, state.activeOptionIndex, options),
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
[ActionTypes.UnregisterOptions]: (state, action) => {
|
|
179
|
+
let options = state.options;
|
|
180
|
+
let idxs = [];
|
|
181
|
+
let ids = new Set(action.options);
|
|
182
|
+
for (let [idx, option] of options.entries()) {
|
|
183
|
+
if (ids.has(option.id)) {
|
|
184
|
+
idxs.push(idx);
|
|
185
|
+
ids.delete(option.id);
|
|
186
|
+
if (ids.size === 0)
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (idxs.length > 0) {
|
|
191
|
+
options = options.slice();
|
|
192
|
+
for (let idx of idxs.reverse()) {
|
|
193
|
+
options.splice(idx, 1);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
...state,
|
|
198
|
+
options,
|
|
199
|
+
activeOptionIndex: findActiveIndex(state.options, state.activeOptionIndex, options),
|
|
200
|
+
activationTrigger: ActivationTrigger.Other,
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
export class ListboxMachine extends Machine {
|
|
205
|
+
static new({ id }) {
|
|
206
|
+
return new ListboxMachine({
|
|
207
|
+
id,
|
|
208
|
+
// @ts-expect-error TODO: Re-structure such that we don't need to ignore this
|
|
209
|
+
dataRef: { current: {} },
|
|
210
|
+
listboxState: ListboxStates.Closed,
|
|
211
|
+
options: [],
|
|
212
|
+
searchQuery: '',
|
|
213
|
+
activeOptionIndex: null,
|
|
214
|
+
activationTrigger: ActivationTrigger.Other,
|
|
215
|
+
frozenValue: false,
|
|
216
|
+
pendingFocus: { focus: Focus.Nothing },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
constructor(initialState) {
|
|
220
|
+
super(initialState);
|
|
221
|
+
// When the listbox is open, and it's not on the top of the hierarchy, we
|
|
222
|
+
// should close it again.
|
|
223
|
+
{
|
|
224
|
+
let id = this.state.id;
|
|
225
|
+
let stackMachine = stackMachines.get(null);
|
|
226
|
+
this.disposables.add(stackMachine.on(StackActionTypes.Push, (state) => {
|
|
227
|
+
if (!stackMachine.selectors.isTop(state, id) &&
|
|
228
|
+
this.state.listboxState === ListboxStates.Open) {
|
|
229
|
+
this.actions.closeListbox();
|
|
230
|
+
}
|
|
231
|
+
}));
|
|
232
|
+
this.on(ActionTypes.OpenListbox, () => stackMachine.actions.push(id));
|
|
233
|
+
this.on(ActionTypes.CloseListbox, () => stackMachine.actions.pop(id));
|
|
234
|
+
}
|
|
235
|
+
this.on(ActionTypes.SelectOption, (_, action) => {
|
|
236
|
+
this.actions.onChange(action.value);
|
|
237
|
+
if (this.state.dataRef.current.mode === ValueMode.Single) {
|
|
238
|
+
this.actions.closeListbox();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
actions = {
|
|
243
|
+
onChange: (newValue) => {
|
|
244
|
+
let { onChange, compare, mode, value } = this.state.dataRef.current;
|
|
245
|
+
return match(mode, {
|
|
246
|
+
[ValueMode.Single]: () => {
|
|
247
|
+
return onChange?.(newValue);
|
|
248
|
+
},
|
|
249
|
+
[ValueMode.Multi]: () => {
|
|
250
|
+
let copy = value.slice();
|
|
251
|
+
let idx = copy.findIndex((item) => compare(item, newValue));
|
|
252
|
+
if (idx === -1) {
|
|
253
|
+
copy.push(newValue);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
copy.splice(idx, 1);
|
|
257
|
+
}
|
|
258
|
+
return onChange?.(copy);
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
},
|
|
262
|
+
// Batched version, so the options that mount together are sorted once
|
|
263
|
+
setOptionTop: batch(() => {
|
|
264
|
+
let tops = [];
|
|
265
|
+
return [
|
|
266
|
+
(id, top) => tops.push({ id, top }),
|
|
267
|
+
() => this.send({ type: ActionTypes.SetOptionsTop, tops: tops.splice(0) }),
|
|
268
|
+
];
|
|
269
|
+
}),
|
|
270
|
+
registerOption: batch(() => {
|
|
271
|
+
let options = [];
|
|
272
|
+
let seen = new Set();
|
|
273
|
+
return [
|
|
274
|
+
(id, dataRef) => {
|
|
275
|
+
if (seen.has(dataRef))
|
|
276
|
+
return;
|
|
277
|
+
seen.add(dataRef);
|
|
278
|
+
options.push({ id, dataRef });
|
|
279
|
+
},
|
|
280
|
+
() => {
|
|
281
|
+
seen.clear();
|
|
282
|
+
return this.send({ type: ActionTypes.RegisterOptions, options: options.splice(0) });
|
|
283
|
+
},
|
|
284
|
+
];
|
|
285
|
+
}),
|
|
286
|
+
unregisterOption: batch(() => {
|
|
287
|
+
let options = [];
|
|
288
|
+
return [
|
|
289
|
+
(id) => options.push(id),
|
|
290
|
+
() => {
|
|
291
|
+
this.send({ type: ActionTypes.UnregisterOptions, options: options.splice(0) });
|
|
292
|
+
},
|
|
293
|
+
];
|
|
294
|
+
}),
|
|
295
|
+
goToOption: batch(() => {
|
|
296
|
+
let last = null;
|
|
297
|
+
return [
|
|
298
|
+
(focus, trigger) => {
|
|
299
|
+
last = { type: ActionTypes.GoToOption, ...focus, trigger };
|
|
300
|
+
},
|
|
301
|
+
() => last && this.send(last),
|
|
302
|
+
];
|
|
303
|
+
}),
|
|
304
|
+
closeListbox: () => {
|
|
305
|
+
this.send({ type: ActionTypes.CloseListbox });
|
|
306
|
+
},
|
|
307
|
+
openListbox: (focus) => {
|
|
308
|
+
this.send({ type: ActionTypes.OpenListbox, focus });
|
|
309
|
+
},
|
|
310
|
+
selectActiveOption: () => {
|
|
311
|
+
if (this.state.activeOptionIndex !== null) {
|
|
312
|
+
let { dataRef } = this.state.options[this.state.activeOptionIndex];
|
|
313
|
+
this.actions.selectOption(dataRef.current.value);
|
|
314
|
+
}
|
|
315
|
+
else if (this.state.dataRef.current.mode === ValueMode.Single) {
|
|
316
|
+
this.actions.closeListbox();
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
selectOption: (value) => {
|
|
320
|
+
this.send({ type: ActionTypes.SelectOption, value });
|
|
321
|
+
},
|
|
322
|
+
search: (value) => {
|
|
323
|
+
this.send({ type: ActionTypes.Search, value });
|
|
324
|
+
},
|
|
325
|
+
clearSearch: () => {
|
|
326
|
+
this.send({ type: ActionTypes.ClearSearch });
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
selectors = {
|
|
330
|
+
activeDescendantId(state) {
|
|
331
|
+
let activeOptionIndex = state.activeOptionIndex;
|
|
332
|
+
let options = state.options;
|
|
333
|
+
return activeOptionIndex === null ? undefined : options[activeOptionIndex]?.id;
|
|
334
|
+
},
|
|
335
|
+
isActive(state, id) {
|
|
336
|
+
let activeOptionIndex = state.activeOptionIndex;
|
|
337
|
+
let options = state.options;
|
|
338
|
+
return activeOptionIndex !== null ? options[activeOptionIndex]?.id === id : false;
|
|
339
|
+
},
|
|
340
|
+
hasFrozenValue(state) {
|
|
341
|
+
return state.frozenValue;
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
reduce(state, action) {
|
|
345
|
+
return match(action.type, reducers, state, action);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ReactNode, type ForwardRefExoticComponent, type RefAttributes } from 'react';
|
|
2
|
+
import { type PressableProps, type ViewProps, type ViewInstance } from 'react-native';
|
|
3
|
+
import { type RenderChildren } from '../../utils/render';
|
|
4
|
+
type ListboxValueContextValue = {
|
|
5
|
+
disabled: boolean;
|
|
6
|
+
isSelected(value: unknown): boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare function useListboxValueContext(componentName: string): ListboxValueContextValue;
|
|
9
|
+
type ListboxBaseProps<TValue> = {
|
|
10
|
+
by?: (a: TValue, b: TValue) => boolean;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
};
|
|
14
|
+
type ListboxSingleProps<TValue> = {
|
|
15
|
+
multiple?: false;
|
|
16
|
+
value?: TValue;
|
|
17
|
+
defaultValue?: TValue;
|
|
18
|
+
onChange?: (value: TValue) => void;
|
|
19
|
+
};
|
|
20
|
+
type ListboxMultipleProps<TValue> = {
|
|
21
|
+
multiple: true;
|
|
22
|
+
value?: TValue[];
|
|
23
|
+
defaultValue?: TValue[];
|
|
24
|
+
onChange?: (value: TValue[]) => void;
|
|
25
|
+
};
|
|
26
|
+
export type ListboxProps<TValue> = (ListboxSingleProps<TValue> | ListboxMultipleProps<TValue>) & ListboxBaseProps<TValue>;
|
|
27
|
+
export declare function Listbox<TValue>({ value: controlledValue, defaultValue, onChange, multiple, by, disabled, children, }: ListboxProps<TValue>): import("react").JSX.Element;
|
|
28
|
+
export type ListboxButtonRenderPropArg = {
|
|
29
|
+
open: boolean;
|
|
30
|
+
disabled: boolean;
|
|
31
|
+
active: boolean;
|
|
32
|
+
};
|
|
33
|
+
export type ListboxButtonProps = Omit<PressableProps, 'children' | 'disabled'> & {
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
children?: RenderChildren<ListboxButtonRenderPropArg>;
|
|
36
|
+
};
|
|
37
|
+
export declare const ListboxButton: ForwardRefExoticComponent<ListboxButtonProps & RefAttributes<ViewInstance>>;
|
|
38
|
+
export type ListboxOptionsProps = ViewProps;
|
|
39
|
+
export declare const ListboxOptions: ForwardRefExoticComponent<ListboxOptionsProps & RefAttributes<ViewInstance>>;
|
|
40
|
+
export type ListboxOptionRenderPropArg = {
|
|
41
|
+
selected: boolean;
|
|
42
|
+
disabled: boolean;
|
|
43
|
+
active: boolean;
|
|
44
|
+
};
|
|
45
|
+
export type ListboxOptionProps<TValue> = Omit<PressableProps, 'children' | 'disabled'> & {
|
|
46
|
+
value: TValue;
|
|
47
|
+
disabled?: boolean;
|
|
48
|
+
textValue?: string;
|
|
49
|
+
order?: number;
|
|
50
|
+
children?: RenderChildren<ListboxOptionRenderPropArg>;
|
|
51
|
+
};
|
|
52
|
+
export declare function ListboxOption<TValue>({ value, disabled: optionDisabled, textValue, order, children, onPress, onLayout, ...theirProps }: ListboxOptionProps<TValue>): import("react").JSX.Element;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, forwardRef, useContext, useId, } from 'react';
|
|
3
|
+
import { Pressable, View, } from 'react-native';
|
|
4
|
+
import { useControllable } from '../../hooks/use-controllable';
|
|
5
|
+
import { useIsoMorphicEffect } from '../../hooks/use-iso-morphic-effect';
|
|
6
|
+
import { useLatestValue } from '../../hooks/use-latest-value';
|
|
7
|
+
import { useSlice } from '../../react-glue';
|
|
8
|
+
import { Focus } from '../../utils/calculate-active-index';
|
|
9
|
+
import { resolveChildren } from '../../utils/render';
|
|
10
|
+
import { ListboxStates, ValueMode } from './listbox-machine';
|
|
11
|
+
import { ListboxContext, useListboxMachine, useListboxMachineContext } from './listbox-machine-glue';
|
|
12
|
+
let ListboxValueContext = createContext(null);
|
|
13
|
+
export function useListboxValueContext(componentName) {
|
|
14
|
+
let context = useContext(ListboxValueContext);
|
|
15
|
+
if (context === null) {
|
|
16
|
+
throw new Error(`<${componentName} /> must be rendered inside a <Listbox /> component.`);
|
|
17
|
+
}
|
|
18
|
+
return context;
|
|
19
|
+
}
|
|
20
|
+
function defaultCompare(a, b) {
|
|
21
|
+
return a === b;
|
|
22
|
+
}
|
|
23
|
+
export function Listbox({ value: controlledValue, defaultValue, onChange, multiple = false, by, disabled = false, children, }) {
|
|
24
|
+
let id = useId();
|
|
25
|
+
let machine = useListboxMachine({ id });
|
|
26
|
+
// The controllable value is either a single value or an array, depending on `multiple`.
|
|
27
|
+
let [value, select] = useControllable(controlledValue, onChange, defaultValue ?? (multiple ? [] : undefined));
|
|
28
|
+
let compare = (a, b) => by ? by(a, b) : defaultCompare(a, b);
|
|
29
|
+
let isSelected = (candidate) => multiple
|
|
30
|
+
? value.some((item) => compare(item, candidate))
|
|
31
|
+
: value !== undefined && compare(value, candidate);
|
|
32
|
+
useIsoMorphicEffect(() => {
|
|
33
|
+
machine.state.dataRef.current = {
|
|
34
|
+
value,
|
|
35
|
+
disabled,
|
|
36
|
+
mode: multiple ? ValueMode.Multi : ValueMode.Single,
|
|
37
|
+
onChange: (next) => select(next),
|
|
38
|
+
compare,
|
|
39
|
+
isSelected,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
return (_jsx(ListboxContext.Provider, { value: machine, children: _jsx(ListboxValueContext.Provider, { value: { disabled, isSelected }, children: children }) }));
|
|
43
|
+
}
|
|
44
|
+
export const ListboxButton = forwardRef(function ListboxButtonFn(props, ref) {
|
|
45
|
+
let machine = useListboxMachineContext('ListboxButton');
|
|
46
|
+
let listbox = useListboxValueContext('ListboxButton');
|
|
47
|
+
let open = useSlice(machine, (state) => state.listboxState === ListboxStates.Open);
|
|
48
|
+
let { disabled: buttonDisabled = false, children, onPress, ...theirProps } = props;
|
|
49
|
+
let disabled = listbox.disabled || buttonDisabled;
|
|
50
|
+
let handlePress = (event) => {
|
|
51
|
+
onPress?.(event);
|
|
52
|
+
if (open) {
|
|
53
|
+
machine.actions.closeListbox();
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
machine.actions.openListbox({ focus: Focus.Nothing });
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
return (_jsx(Pressable, { ...theirProps, ref: ref, disabled: disabled, accessibilityRole: "button", accessibilityState: { ...theirProps.accessibilityState, expanded: open, disabled }, onPress: handlePress, children: ({ pressed }) => resolveChildren(children, { open, disabled, active: pressed }) }));
|
|
60
|
+
});
|
|
61
|
+
export const ListboxOptions = forwardRef(function ListboxOptionsFn(props, ref) {
|
|
62
|
+
let machine = useListboxMachineContext('ListboxOptions');
|
|
63
|
+
let open = useSlice(machine, (state) => state.listboxState === ListboxStates.Open);
|
|
64
|
+
if (!open)
|
|
65
|
+
return null;
|
|
66
|
+
return _jsx(View, { ...props, ref: ref });
|
|
67
|
+
});
|
|
68
|
+
export function ListboxOption({ value, disabled: optionDisabled = false, textValue, order, children, onPress, onLayout, ...theirProps }) {
|
|
69
|
+
let machine = useListboxMachineContext('ListboxOption');
|
|
70
|
+
let listbox = useListboxValueContext('ListboxOption');
|
|
71
|
+
let id = useId();
|
|
72
|
+
let disabled = listbox.disabled || optionDisabled;
|
|
73
|
+
let dataRef = useLatestValue({ disabled, value, textValue, order });
|
|
74
|
+
useIsoMorphicEffect(() => {
|
|
75
|
+
machine.actions.registerOption(id, dataRef);
|
|
76
|
+
return () => machine.actions.unregisterOption(id);
|
|
77
|
+
}, [id, machine]);
|
|
78
|
+
let active = useSlice(machine, (state) => machine.selectors.isActive(state, id));
|
|
79
|
+
let selected = listbox.isSelected(value);
|
|
80
|
+
let handlePress = (event) => {
|
|
81
|
+
onPress?.(event);
|
|
82
|
+
if (disabled)
|
|
83
|
+
return;
|
|
84
|
+
machine.actions.selectOption(value);
|
|
85
|
+
};
|
|
86
|
+
let handleLayout = (event) => {
|
|
87
|
+
onLayout?.(event);
|
|
88
|
+
machine.actions.setOptionTop(id, event.nativeEvent.layout.y);
|
|
89
|
+
};
|
|
90
|
+
return (_jsx(Pressable, { ...theirProps, disabled: disabled, accessibilityRole: "button", accessibilityState: { ...theirProps.accessibilityState, selected, disabled }, onPress: handlePress, onLayout: handleLayout, children: ({ pressed }) => resolveChildren(children, { selected, disabled, active: active || pressed }) }));
|
|
91
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type TextProps } from 'react-native';
|
|
3
|
+
export type ListboxSelectedOptionProps = Omit<TextProps, 'children'> & {
|
|
4
|
+
options: ReactNode;
|
|
5
|
+
placeholder?: ReactNode;
|
|
6
|
+
};
|
|
7
|
+
export declare function ListboxSelectedOption({ options, placeholder, ...theirProps }: ListboxSelectedOptionProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Children, isValidElement } from 'react';
|
|
3
|
+
import { Text } from 'react-native';
|
|
4
|
+
import { resolveChildren } from '../../utils/render';
|
|
5
|
+
import { useListboxValueContext } from '../listbox/listbox';
|
|
6
|
+
export function ListboxSelectedOption({ options, placeholder, ...theirProps }) {
|
|
7
|
+
let listbox = useListboxValueContext('ListboxSelectedOption');
|
|
8
|
+
let selectedOptions = Children.toArray(options).filter((child) => isValidElement(child) && listbox.isSelected(child.props.value));
|
|
9
|
+
return (_jsx(Text, { ...theirProps, children: selectedOptions.length > 0
|
|
10
|
+
? selectedOptions.map((option) => (_jsx(Text, { children: resolveChildren(option.props.children, {
|
|
11
|
+
selected: true,
|
|
12
|
+
disabled: option.props.disabled ?? false,
|
|
13
|
+
active: false,
|
|
14
|
+
}) }, option.key)))
|
|
15
|
+
: placeholder }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { MenuMachine } from './menu-machine';
|
|
2
|
+
export declare const MenuContext: import("react").Context<MenuMachine | null>;
|
|
3
|
+
export declare function useMenuMachineContext(component: string): MenuMachine;
|
|
4
|
+
export declare function useMenuMachine({ id }: {
|
|
5
|
+
id: string;
|
|
6
|
+
}): MenuMachine;
|