@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,33 @@
|
|
|
1
|
+
export declare abstract class Machine<State, Event extends {
|
|
2
|
+
type: number | string;
|
|
3
|
+
}> {
|
|
4
|
+
#private;
|
|
5
|
+
disposables: {
|
|
6
|
+
requestAnimationFrame(callback: (time: number) => void): () => void;
|
|
7
|
+
nextFrame(callback: (time: number) => void): () => void;
|
|
8
|
+
setTimeout(callback: (_: void) => void, delay?: number | undefined): () => void;
|
|
9
|
+
microTask(cb: () => void): () => void;
|
|
10
|
+
group(cb: (d: {
|
|
11
|
+
requestAnimationFrame(callback: (time: number) => void): () => void;
|
|
12
|
+
nextFrame(callback: (time: number) => void): () => void;
|
|
13
|
+
setTimeout(callback: (_: void) => void, delay?: number | undefined): () => void;
|
|
14
|
+
microTask(cb: () => void): () => void;
|
|
15
|
+
group(cb: (d: /*elided*/ any) => void): () => void;
|
|
16
|
+
add(cb: () => void): () => void;
|
|
17
|
+
dispose(): void;
|
|
18
|
+
}) => void): () => void;
|
|
19
|
+
add(cb: () => void): () => void;
|
|
20
|
+
dispose(): void;
|
|
21
|
+
};
|
|
22
|
+
constructor(initialState: State);
|
|
23
|
+
dispose(): void;
|
|
24
|
+
get state(): Readonly<State>;
|
|
25
|
+
abstract reduce(state: Readonly<State>, event: Event): Readonly<State>;
|
|
26
|
+
subscribe<Slice>(selector: (state: Readonly<State>) => Slice, callback: (state: Slice) => void): () => void;
|
|
27
|
+
on<T extends Event['type']>(type: T, callback: (state: State, event: Extract<Event, {
|
|
28
|
+
type: T;
|
|
29
|
+
}>) => void): () => void;
|
|
30
|
+
send(event: Event): void;
|
|
31
|
+
}
|
|
32
|
+
export declare function shallowEqual(a: any, b: any): boolean;
|
|
33
|
+
export declare function batch<F extends (...args: any[]) => void, P extends any[] = Parameters<F>>(setup: () => [callback: F, handle: () => void]): (...args: P) => void;
|
package/dist/machine.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { DefaultMap } from './utils/default-map';
|
|
2
|
+
import { disposables } from './utils/disposables';
|
|
3
|
+
export class Machine {
|
|
4
|
+
#state = {};
|
|
5
|
+
#eventSubscribers = new DefaultMap(() => new Set());
|
|
6
|
+
#subscribers = new Set();
|
|
7
|
+
disposables = disposables();
|
|
8
|
+
constructor(initialState) {
|
|
9
|
+
this.#state = initialState;
|
|
10
|
+
}
|
|
11
|
+
dispose() {
|
|
12
|
+
this.disposables.dispose();
|
|
13
|
+
}
|
|
14
|
+
get state() {
|
|
15
|
+
return this.#state;
|
|
16
|
+
}
|
|
17
|
+
subscribe(selector, callback) {
|
|
18
|
+
let subscriber = {
|
|
19
|
+
selector,
|
|
20
|
+
callback,
|
|
21
|
+
current: selector(this.#state),
|
|
22
|
+
};
|
|
23
|
+
this.#subscribers.add(subscriber);
|
|
24
|
+
return this.disposables.add(() => {
|
|
25
|
+
this.#subscribers.delete(subscriber);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
on(type, callback) {
|
|
29
|
+
this.#eventSubscribers.get(type).add(callback);
|
|
30
|
+
return this.disposables.add(() => {
|
|
31
|
+
this.#eventSubscribers.get(type).delete(callback);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
send(event) {
|
|
35
|
+
let newState = this.reduce(this.#state, event);
|
|
36
|
+
if (newState === this.#state)
|
|
37
|
+
return; // No change
|
|
38
|
+
this.#state = newState;
|
|
39
|
+
for (let subscriber of this.#subscribers) {
|
|
40
|
+
let slice = subscriber.selector(this.#state);
|
|
41
|
+
if (shallowEqual(subscriber.current, slice))
|
|
42
|
+
continue;
|
|
43
|
+
subscriber.current = slice;
|
|
44
|
+
subscriber.callback(slice);
|
|
45
|
+
}
|
|
46
|
+
for (let callback of this.#eventSubscribers.get(event.type)) {
|
|
47
|
+
callback(this.#state, event);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function shallowEqual(a, b) {
|
|
52
|
+
// Exact same reference
|
|
53
|
+
if (Object.is(a, b))
|
|
54
|
+
return true;
|
|
55
|
+
// Must be some type of object
|
|
56
|
+
if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null)
|
|
57
|
+
return false;
|
|
58
|
+
// Arrays
|
|
59
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
60
|
+
if (a.length !== b.length)
|
|
61
|
+
return false;
|
|
62
|
+
return compareEntries(a[Symbol.iterator](), b[Symbol.iterator]());
|
|
63
|
+
}
|
|
64
|
+
// Map and Set. Map entries are [key, value] pairs, so they are compared by their parts.
|
|
65
|
+
if ((a instanceof Map && b instanceof Map) || (a instanceof Set && b instanceof Set)) {
|
|
66
|
+
if (a.size !== b.size)
|
|
67
|
+
return false;
|
|
68
|
+
return compareEntries(a.entries(), b.entries(), pairsAreEqual);
|
|
69
|
+
}
|
|
70
|
+
// Plain objects
|
|
71
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
72
|
+
return compareEntries(Object.entries(a)[Symbol.iterator](), Object.entries(b)[Symbol.iterator](), pairsAreEqual);
|
|
73
|
+
}
|
|
74
|
+
// TODO: Not sure how to compare other types of objects
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
function pairsAreEqual(a, b) {
|
|
78
|
+
return Object.is(a[0], b[0]) && Object.is(a[1], b[1]);
|
|
79
|
+
}
|
|
80
|
+
function compareEntries(a, b, isEqual = Object.is) {
|
|
81
|
+
let aResult = a.next();
|
|
82
|
+
let bResult = b.next();
|
|
83
|
+
while (!aResult.done && !bResult.done) {
|
|
84
|
+
if (!isEqual(aResult.value, bResult.value))
|
|
85
|
+
return false;
|
|
86
|
+
aResult = a.next();
|
|
87
|
+
bResult = b.next();
|
|
88
|
+
}
|
|
89
|
+
// Both iterators must be exhausted together to be equal
|
|
90
|
+
return aResult.done === bResult.done;
|
|
91
|
+
}
|
|
92
|
+
function isPlainObject(value) {
|
|
93
|
+
if (Object.prototype.toString.call(value) !== '[object Object]') {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
let prototype = Object.getPrototypeOf(value);
|
|
97
|
+
return prototype === null || Object.getPrototypeOf(prototype) === null;
|
|
98
|
+
}
|
|
99
|
+
export function batch(setup) {
|
|
100
|
+
let [callback, handle] = setup();
|
|
101
|
+
let d = disposables();
|
|
102
|
+
return (...args) => {
|
|
103
|
+
callback(...args);
|
|
104
|
+
d.dispose();
|
|
105
|
+
d.microTask(handle);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Machine } from '../machine';
|
|
2
|
+
import { DefaultMap } from '../utils/default-map';
|
|
3
|
+
type Scope = string | null;
|
|
4
|
+
type Id = string;
|
|
5
|
+
interface State {
|
|
6
|
+
stack: Id[];
|
|
7
|
+
}
|
|
8
|
+
export declare enum ActionTypes {
|
|
9
|
+
Push = 0,
|
|
10
|
+
Pop = 1
|
|
11
|
+
}
|
|
12
|
+
export type Actions = {
|
|
13
|
+
type: ActionTypes.Push;
|
|
14
|
+
id: Id;
|
|
15
|
+
} | {
|
|
16
|
+
type: ActionTypes.Pop;
|
|
17
|
+
id: Id;
|
|
18
|
+
};
|
|
19
|
+
declare class StackMachine extends Machine<State, Actions> {
|
|
20
|
+
static new(): StackMachine;
|
|
21
|
+
reduce(state: Readonly<State>, action: Actions): State;
|
|
22
|
+
actions: {
|
|
23
|
+
push: (id: Id) => void;
|
|
24
|
+
pop: (id: Id) => void;
|
|
25
|
+
};
|
|
26
|
+
selectors: {
|
|
27
|
+
isTop: (state: State, id: Id) => boolean;
|
|
28
|
+
inStack: (state: State, id: Id) => boolean;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export declare const stackMachines: DefaultMap<Scope, StackMachine>;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Machine } from '../machine';
|
|
2
|
+
import { DefaultMap } from '../utils/default-map';
|
|
3
|
+
import { match } from '../utils/match';
|
|
4
|
+
export var ActionTypes;
|
|
5
|
+
(function (ActionTypes) {
|
|
6
|
+
ActionTypes[ActionTypes["Push"] = 0] = "Push";
|
|
7
|
+
ActionTypes[ActionTypes["Pop"] = 1] = "Pop";
|
|
8
|
+
})(ActionTypes || (ActionTypes = {}));
|
|
9
|
+
let reducers = {
|
|
10
|
+
[ActionTypes.Push](state, action) {
|
|
11
|
+
let id = action.id;
|
|
12
|
+
let stack = state.stack;
|
|
13
|
+
let idx = state.stack.indexOf(id);
|
|
14
|
+
// Already in the stack, move it to the top
|
|
15
|
+
if (idx !== -1) {
|
|
16
|
+
let copy = state.stack.slice();
|
|
17
|
+
copy.splice(idx, 1);
|
|
18
|
+
copy.push(id);
|
|
19
|
+
stack = copy;
|
|
20
|
+
return { ...state, stack };
|
|
21
|
+
}
|
|
22
|
+
// Not in the stack, add it to the top
|
|
23
|
+
return { ...state, stack: [...state.stack, id] };
|
|
24
|
+
},
|
|
25
|
+
[ActionTypes.Pop](state, action) {
|
|
26
|
+
let id = action.id;
|
|
27
|
+
let idx = state.stack.indexOf(id);
|
|
28
|
+
if (idx === -1)
|
|
29
|
+
return state; // Not in the stack
|
|
30
|
+
let copy = state.stack.slice();
|
|
31
|
+
copy.splice(idx, 1);
|
|
32
|
+
return { ...state, stack: copy };
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
class StackMachine extends Machine {
|
|
36
|
+
static new() {
|
|
37
|
+
return new StackMachine({ stack: [] });
|
|
38
|
+
}
|
|
39
|
+
reduce(state, action) {
|
|
40
|
+
return match(action.type, reducers, state, action);
|
|
41
|
+
}
|
|
42
|
+
actions = {
|
|
43
|
+
push: (id) => this.send({ type: ActionTypes.Push, id }),
|
|
44
|
+
pop: (id) => this.send({ type: ActionTypes.Pop, id }),
|
|
45
|
+
};
|
|
46
|
+
selectors = {
|
|
47
|
+
isTop: (state, id) => state.stack[state.stack.length - 1] === id,
|
|
48
|
+
inStack: (state, id) => state.stack.includes(id),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export const stackMachines = new DefaultMap(() => StackMachine.new());
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector';
|
|
2
|
+
import { useEvent } from './hooks/use-event';
|
|
3
|
+
import { shallowEqual } from './machine';
|
|
4
|
+
export function useSlice(machine, selector, compare = shallowEqual) {
|
|
5
|
+
return useSyncExternalStoreWithSelector(useEvent((onStoreChange) => machine.subscribe(identity, onStoreChange)), useEvent(() => machine.state), useEvent(() => machine.state), useEvent(selector), compare);
|
|
6
|
+
}
|
|
7
|
+
function identity(value) {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare enum Focus {
|
|
2
|
+
/** Focus the first non-disabled item. */
|
|
3
|
+
First = 0,
|
|
4
|
+
/** Focus the previous non-disabled item. */
|
|
5
|
+
Previous = 1,
|
|
6
|
+
/** Focus the next non-disabled item. */
|
|
7
|
+
Next = 2,
|
|
8
|
+
/** Focus the last non-disabled item. */
|
|
9
|
+
Last = 3,
|
|
10
|
+
/** Focus a specific item based on the `id` of the item. */
|
|
11
|
+
Specific = 4,
|
|
12
|
+
/** Focus no items at all. */
|
|
13
|
+
Nothing = 5
|
|
14
|
+
}
|
|
15
|
+
export declare function calculateActiveIndex<TItem>(action: {
|
|
16
|
+
focus: Focus.Specific;
|
|
17
|
+
id: string;
|
|
18
|
+
} | {
|
|
19
|
+
focus: Exclude<Focus, Focus.Specific>;
|
|
20
|
+
}, resolvers: {
|
|
21
|
+
resolveItems(): TItem[];
|
|
22
|
+
resolveActiveIndex(): number | null;
|
|
23
|
+
resolveId(item: TItem, index: number, items: TItem[]): string;
|
|
24
|
+
resolveDisabled(item: TItem, index: number, items: TItem[]): boolean;
|
|
25
|
+
}): number | null;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
function assertNever(x) {
|
|
2
|
+
throw new Error('Unexpected object: ' + x);
|
|
3
|
+
}
|
|
4
|
+
export var Focus;
|
|
5
|
+
(function (Focus) {
|
|
6
|
+
/** Focus the first non-disabled item. */
|
|
7
|
+
Focus[Focus["First"] = 0] = "First";
|
|
8
|
+
/** Focus the previous non-disabled item. */
|
|
9
|
+
Focus[Focus["Previous"] = 1] = "Previous";
|
|
10
|
+
/** Focus the next non-disabled item. */
|
|
11
|
+
Focus[Focus["Next"] = 2] = "Next";
|
|
12
|
+
/** Focus the last non-disabled item. */
|
|
13
|
+
Focus[Focus["Last"] = 3] = "Last";
|
|
14
|
+
/** Focus a specific item based on the `id` of the item. */
|
|
15
|
+
Focus[Focus["Specific"] = 4] = "Specific";
|
|
16
|
+
/** Focus no items at all. */
|
|
17
|
+
Focus[Focus["Nothing"] = 5] = "Nothing";
|
|
18
|
+
})(Focus || (Focus = {}));
|
|
19
|
+
export function calculateActiveIndex(action, resolvers) {
|
|
20
|
+
let items = resolvers.resolveItems();
|
|
21
|
+
if (items.length <= 0)
|
|
22
|
+
return null;
|
|
23
|
+
let currentActiveIndex = resolvers.resolveActiveIndex();
|
|
24
|
+
let activeIndex = currentActiveIndex ?? -1;
|
|
25
|
+
switch (action.focus) {
|
|
26
|
+
case Focus.First: {
|
|
27
|
+
for (let i = 0; i < items.length; ++i) {
|
|
28
|
+
if (!resolvers.resolveDisabled(items[i], i, items)) {
|
|
29
|
+
return i;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return currentActiveIndex;
|
|
33
|
+
}
|
|
34
|
+
case Focus.Previous: {
|
|
35
|
+
// If nothing is active, focus the last relevant item
|
|
36
|
+
if (activeIndex === -1)
|
|
37
|
+
activeIndex = items.length;
|
|
38
|
+
for (let i = activeIndex - 1; i >= 0; --i) {
|
|
39
|
+
if (!resolvers.resolveDisabled(items[i], i, items)) {
|
|
40
|
+
return i;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return currentActiveIndex;
|
|
44
|
+
}
|
|
45
|
+
case Focus.Next: {
|
|
46
|
+
for (let i = activeIndex + 1; i < items.length; ++i) {
|
|
47
|
+
if (!resolvers.resolveDisabled(items[i], i, items)) {
|
|
48
|
+
return i;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return currentActiveIndex;
|
|
52
|
+
}
|
|
53
|
+
case Focus.Last: {
|
|
54
|
+
for (let i = items.length - 1; i >= 0; --i) {
|
|
55
|
+
if (!resolvers.resolveDisabled(items[i], i, items)) {
|
|
56
|
+
return i;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return currentActiveIndex;
|
|
60
|
+
}
|
|
61
|
+
case Focus.Specific: {
|
|
62
|
+
for (let i = 0; i < items.length; ++i) {
|
|
63
|
+
if (resolvers.resolveId(items[i], i, items) === action.id) {
|
|
64
|
+
return i;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return currentActiveIndex;
|
|
68
|
+
}
|
|
69
|
+
case Focus.Nothing:
|
|
70
|
+
return null;
|
|
71
|
+
default:
|
|
72
|
+
assertNever(action);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class DefaultMap extends Map {
|
|
2
|
+
factory;
|
|
3
|
+
constructor(factory) {
|
|
4
|
+
super();
|
|
5
|
+
this.factory = factory;
|
|
6
|
+
}
|
|
7
|
+
get(key) {
|
|
8
|
+
let value = super.get(key);
|
|
9
|
+
if (value === undefined) {
|
|
10
|
+
value = this.factory(key);
|
|
11
|
+
this.set(key, value);
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type Disposables = ReturnType<typeof disposables>;
|
|
2
|
+
/**
|
|
3
|
+
* Disposables are a way to manage functions like `setTimeout` and
|
|
4
|
+
* `requestAnimationFrame` that need to be cleaned up when they are no longer
|
|
5
|
+
* needed.
|
|
6
|
+
*
|
|
7
|
+
* When you register a disposable function, it is added to a collection of
|
|
8
|
+
* disposables. Each disposable in the collection provides a `dispose` clean up
|
|
9
|
+
* function that can be called when it's no longer needed. There is also a
|
|
10
|
+
* `dispose` function on the collection itself that can be used to clean up all
|
|
11
|
+
* pending disposables in that collection.
|
|
12
|
+
*
|
|
13
|
+
* Event listeners and DOM styles are not managed here: React Native has no DOM.
|
|
14
|
+
*/
|
|
15
|
+
export declare function disposables(): {
|
|
16
|
+
requestAnimationFrame(callback: (time: number) => void): () => void;
|
|
17
|
+
nextFrame(callback: (time: number) => void): () => void;
|
|
18
|
+
setTimeout(callback: (_: void) => void, delay?: number | undefined): () => void;
|
|
19
|
+
microTask(cb: () => void): () => void;
|
|
20
|
+
group(cb: (d: {
|
|
21
|
+
requestAnimationFrame(callback: (time: number) => void): () => void;
|
|
22
|
+
nextFrame(callback: (time: number) => void): () => void;
|
|
23
|
+
setTimeout(callback: (_: void) => void, delay?: number | undefined): () => void;
|
|
24
|
+
microTask(cb: () => void): () => void;
|
|
25
|
+
group(cb: (d: /*elided*/ any) => void): () => void;
|
|
26
|
+
add(cb: () => void): () => void;
|
|
27
|
+
dispose(): void;
|
|
28
|
+
}) => void): () => void;
|
|
29
|
+
add(cb: () => void): () => void;
|
|
30
|
+
dispose(): void;
|
|
31
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { microTask } from './micro-task';
|
|
2
|
+
/**
|
|
3
|
+
* Disposables are a way to manage functions like `setTimeout` and
|
|
4
|
+
* `requestAnimationFrame` that need to be cleaned up when they are no longer
|
|
5
|
+
* needed.
|
|
6
|
+
*
|
|
7
|
+
* When you register a disposable function, it is added to a collection of
|
|
8
|
+
* disposables. Each disposable in the collection provides a `dispose` clean up
|
|
9
|
+
* function that can be called when it's no longer needed. There is also a
|
|
10
|
+
* `dispose` function on the collection itself that can be used to clean up all
|
|
11
|
+
* pending disposables in that collection.
|
|
12
|
+
*
|
|
13
|
+
* Event listeners and DOM styles are not managed here: React Native has no DOM.
|
|
14
|
+
*/
|
|
15
|
+
export function disposables() {
|
|
16
|
+
let _disposables = [];
|
|
17
|
+
let api = {
|
|
18
|
+
requestAnimationFrame(...args) {
|
|
19
|
+
let raf = requestAnimationFrame(...args);
|
|
20
|
+
return api.add(() => cancelAnimationFrame(raf));
|
|
21
|
+
},
|
|
22
|
+
nextFrame(...args) {
|
|
23
|
+
return api.requestAnimationFrame(() => {
|
|
24
|
+
return api.requestAnimationFrame(...args);
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
setTimeout(...args) {
|
|
28
|
+
let timer = setTimeout(...args);
|
|
29
|
+
return api.add(() => clearTimeout(timer));
|
|
30
|
+
},
|
|
31
|
+
microTask(...args) {
|
|
32
|
+
let task = { current: true };
|
|
33
|
+
microTask(() => {
|
|
34
|
+
if (task.current) {
|
|
35
|
+
args[0]();
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return api.add(() => {
|
|
39
|
+
task.current = false;
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
group(cb) {
|
|
43
|
+
let d = disposables();
|
|
44
|
+
cb(d);
|
|
45
|
+
return this.add(() => d.dispose());
|
|
46
|
+
},
|
|
47
|
+
add(cb) {
|
|
48
|
+
// Ensure we don't add the same callback twice
|
|
49
|
+
if (!_disposables.includes(cb)) {
|
|
50
|
+
_disposables.push(cb);
|
|
51
|
+
}
|
|
52
|
+
return () => {
|
|
53
|
+
let idx = _disposables.indexOf(cb);
|
|
54
|
+
if (idx >= 0) {
|
|
55
|
+
for (let dispose of _disposables.splice(idx, 1)) {
|
|
56
|
+
dispose();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
dispose() {
|
|
62
|
+
for (let dispose of _disposables.splice(0)) {
|
|
63
|
+
dispose();
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
return api;
|
|
68
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// The active option is tracked by its id, not by its index. When the list is rebuilt, for example
|
|
2
|
+
// by a registration, an unregistration or a new position, the active index moves with its item. It
|
|
3
|
+
// is cleared if the item is gone.
|
|
4
|
+
export function findActiveIndex(previous, activeIndex, next) {
|
|
5
|
+
if (activeIndex === null)
|
|
6
|
+
return null;
|
|
7
|
+
let activeItem = previous[activeIndex];
|
|
8
|
+
if (activeItem === undefined)
|
|
9
|
+
return null;
|
|
10
|
+
let idx = next.findIndex((item) => item.id === activeItem.id);
|
|
11
|
+
return idx === -1 ? null : idx;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function match<TValue extends string | number = string, TReturnValue = unknown>(value: TValue, lookup: Record<TValue, TReturnValue | ((...args: any[]) => TReturnValue)>, ...args: any[]): TReturnValue;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function match(value, lookup, ...args) {
|
|
2
|
+
if (value in lookup) {
|
|
3
|
+
let returnValue = lookup[value];
|
|
4
|
+
return typeof returnValue === 'function' ? returnValue(...args) : returnValue;
|
|
5
|
+
}
|
|
6
|
+
let error = new Error(`Tried to handle "${value}" but there is no handler defined. Only defined handlers are: ${Object.keys(lookup)
|
|
7
|
+
.map((key) => `"${key}"`)
|
|
8
|
+
.join(', ')}.`);
|
|
9
|
+
if (Error.captureStackTrace)
|
|
10
|
+
Error.captureStackTrace(error, match);
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function microTask(cb: () => void): void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type Orderable = {
|
|
2
|
+
top?: number;
|
|
3
|
+
dataRef: {
|
|
4
|
+
current: {
|
|
5
|
+
order?: number;
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export declare function sortByOrder<T extends Orderable>(items: T[]): T[];
|
|
10
|
+
export declare function placeAtTops<T extends Orderable & {
|
|
11
|
+
id: string;
|
|
12
|
+
}>(items: T[], tops: {
|
|
13
|
+
id: string;
|
|
14
|
+
top: number;
|
|
15
|
+
}[]): T[] | null;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Compares two optional numbers. A missing value goes after the ones that are set.
|
|
2
|
+
function compareOptional(a, z) {
|
|
3
|
+
if (a === z)
|
|
4
|
+
return 0;
|
|
5
|
+
if (a === undefined)
|
|
6
|
+
return 1;
|
|
7
|
+
if (z === undefined)
|
|
8
|
+
return -1;
|
|
9
|
+
return a - z;
|
|
10
|
+
}
|
|
11
|
+
// Options sort by their explicit `order` first. Without one, they sort by their position on screen,
|
|
12
|
+
// which is the order a web page gives them in the DOM. `Array.prototype.sort` is stable.
|
|
13
|
+
export function sortByOrder(items) {
|
|
14
|
+
return items
|
|
15
|
+
.slice()
|
|
16
|
+
.sort((a, z) => compareOptional(a.dataRef.current.order, z.dataRef.current.order) ||
|
|
17
|
+
compareOptional(a.top, z.top));
|
|
18
|
+
}
|
|
19
|
+
// Sets the screen positions of several items at once, then sorts once. Sorting is the expensive
|
|
20
|
+
// part on long lists, so a batch of positions costs a single sort. Returns `null` when no position
|
|
21
|
+
// changed, so the caller can skip the update.
|
|
22
|
+
export function placeAtTops(items, tops) {
|
|
23
|
+
let topById = new Map(tops.map(({ id, top }) => [id, top]));
|
|
24
|
+
let changed = false;
|
|
25
|
+
let placed = items.map((item) => {
|
|
26
|
+
let top = topById.get(item.id);
|
|
27
|
+
if (top === undefined || top === item.top)
|
|
28
|
+
return item;
|
|
29
|
+
changed = true;
|
|
30
|
+
return { ...item, top };
|
|
31
|
+
});
|
|
32
|
+
return changed ? sortByOrder(placed) : null;
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function findSearchMatchIndex<T>(items: T[], activeIndex: number | null, wasAlreadySearching: boolean, matches: (item: T) => boolean): number;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Finds the next item that matches the search, starting after the active item and wrapping around.
|
|
2
|
+
// Returns its index, or `-1` when nothing matches.
|
|
3
|
+
export function findSearchMatchIndex(items, activeIndex, wasAlreadySearching, matches) {
|
|
4
|
+
// While a search is in progress, the active item can still match, so the search starts on it.
|
|
5
|
+
let offset = wasAlreadySearching ? 0 : 1;
|
|
6
|
+
let reordered = activeIndex !== null
|
|
7
|
+
? items.slice(activeIndex + offset).concat(items.slice(0, activeIndex + offset))
|
|
8
|
+
: items;
|
|
9
|
+
let match = reordered.find(matches);
|
|
10
|
+
return match ? items.indexOf(match) : -1;
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bareboard/react-native",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A set of completely unstyled, fully accessible UI components for React Native.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/jbrxl/bareboard.git",
|
|
9
|
+
"directory": "packages/react-native"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"main": "dist/index.js",
|
|
15
|
+
"types": "dist/index.d.ts",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"use-sync-external-store": "^1.5.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@playwright/test": "^1.64.1",
|
|
24
|
+
"@react-native/babel-preset": "0.88.0",
|
|
25
|
+
"@testing-library/react-native": "^13.3.3",
|
|
26
|
+
"@types/react": "~19.3.0",
|
|
27
|
+
"@types/use-sync-external-store": "^1.5.0",
|
|
28
|
+
"@vitejs/plugin-react": "^4.6.0",
|
|
29
|
+
"react": "19.3.0",
|
|
30
|
+
"react-dom": "19.3.0",
|
|
31
|
+
"react-native": "0.88.0",
|
|
32
|
+
"react-native-web": "~0.21.4",
|
|
33
|
+
"react-test-renderer": "19.3.0",
|
|
34
|
+
"vite": "^5.4.20"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"react": "^18 || ^19",
|
|
38
|
+
"react-native": "*"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -p tsconfig.build.json",
|
|
42
|
+
"lint": "vp lint",
|
|
43
|
+
"test:web": "playwright test -c e2e-web/playwright.config.ts",
|
|
44
|
+
"typecheck": "tsc --noEmit"
|
|
45
|
+
}
|
|
6
46
|
}
|