@bccampus/ui-components 0.4.0 → 0.4.2

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.
Files changed (37) hide show
  1. package/dist/AbstractFocusProvider-CxvlcEki.js +29 -0
  2. package/dist/AbstractSelectionProvider-BtaROstC.js +30 -0
  3. package/dist/CompositeDataItem-DuHOHCWy.js +158 -0
  4. package/dist/ListboxFocusProvider.d.ts +149 -0
  5. package/dist/ListboxFocusProvider.js +53 -0
  6. package/dist/MultipleSelectionProvider.d.ts +141 -0
  7. package/dist/MultipleSelectionProvider.js +19 -0
  8. package/dist/SingleSelectionProvider.d.ts +141 -0
  9. package/dist/SingleSelectionProvider.js +23 -0
  10. package/dist/composite-component-DSUbd1XS.js +122 -0
  11. package/dist/composite.d.ts +108 -51
  12. package/dist/composite.js +57 -447
  13. package/dist/icon-generator-tuhuqdpL.js +76 -0
  14. package/dist/icon-generator.d.ts +11 -4
  15. package/dist/icon-generator.js +4 -74
  16. package/dist/listbox.d.ts +171 -0
  17. package/dist/listbox.js +76 -0
  18. package/dist/masked-image-generator.js +7 -7
  19. package/dist/ui-components.js +5 -5
  20. package/package.json +5 -1
  21. package/src/components/ui/composite/CompositeData.ts +22 -114
  22. package/src/components/ui/composite/FocusProvider/AbstractFocusProvider.ts +83 -0
  23. package/src/components/ui/composite/FocusProvider/ListboxFocusProvider.ts +74 -0
  24. package/src/components/ui/composite/SelectionProvider/AbstractSelectionProvider.ts +45 -0
  25. package/src/components/ui/composite/SelectionProvider/MultipleSelectionProvider.ts +28 -0
  26. package/src/components/ui/composite/SelectionProvider/SingleSelectionProvider.ts +37 -0
  27. package/src/components/ui/composite/composite-component-item.tsx +12 -10
  28. package/src/components/ui/composite/composite-component.tsx +39 -68
  29. package/src/components/ui/composite/index.ts +4 -1
  30. package/src/components/ui/composite/listbox.tsx +61 -0
  31. package/src/components/ui/composite/types.ts +51 -30
  32. package/src/components/ui/icon-generator/index.ts +2 -0
  33. package/src/hooks/use-keyboard-event.ts +31 -42
  34. package/vite.config.ts +6 -2
  35. package/src/components/ui/composite/composite-data-context.tsx +0 -31
  36. /package/dist/{igenerate-tiles.d.ts → generate-tiles.d.ts} +0 -0
  37. /package/dist/{igenerate-tiles.js → generate-tiles.js} +0 -0
@@ -1,76 +1,6 @@
1
- import { jsx as u } from "react/jsx-runtime";
2
- import { useMemo as r } from "react";
3
- import { T as o, g as l } from "./generate-tiles-DuagGD1d.js";
4
- import { c as m } from "./utils-CRiPKpXj.js";
5
- const k = [
6
- [o.PieRand, o.PieRand],
7
- [o.PieRand, o.PieRand]
8
- ];
9
- function b({
10
- pattern: e = k,
11
- tileSize: h = 64,
12
- tileClassName: s,
13
- tileBgClassName: c,
14
- renderChildren: d,
15
- fit: i = "none",
16
- ...g
17
- }) {
18
- const t = r(
19
- () => ({
20
- width: e.reduce((n, v) => Math.max(n, v.length), 0) * h,
21
- height: e.length * h
22
- }),
23
- [e, h]
24
- ), a = r(() => {
25
- const n = l(e, h, s);
26
- return c && n.unshift(
27
- /* @__PURE__ */ u(
28
- "rect",
29
- {
30
- x: 0,
31
- y: 0,
32
- width: t.width,
33
- height: t.height,
34
- className: m("fill-black stroke-2 stroke-black", c)
35
- },
36
- "svg-mask-invert"
37
- )
38
- ), n;
39
- }, [e, t, s, c, h]), w = r(() => {
40
- switch (i) {
41
- case "fill":
42
- return t.width <= t.height ? "100%" : void 0;
43
- case "width":
44
- return "100%";
45
- case "none":
46
- return t.width;
47
- default:
48
- return;
49
- }
50
- }, [i, t]), f = r(() => {
51
- switch (i) {
52
- case "fill":
53
- return t.height < t.width ? "100%" : void 0;
54
- case "height":
55
- return "100%";
56
- case "none":
57
- return t.height;
58
- default:
59
- return;
60
- }
61
- }, [i, t]);
62
- return /* @__PURE__ */ u(
63
- "svg",
64
- {
65
- width: w,
66
- height: f,
67
- viewBox: `0 0 ${t.width} ${t.height}`,
68
- overflow: "visible",
69
- ...g,
70
- children: d ? d(a, t.width, t.height) : a
71
- }
72
- );
73
- }
1
+ import { I as r } from "./icon-generator-tuhuqdpL.js";
2
+ import { T as p } from "./generate-tiles-DuagGD1d.js";
74
3
  export {
75
- b as IconGenerator
4
+ r as IconGenerator,
5
+ p as TileShape
76
6
  };
@@ -0,0 +1,171 @@
1
+ import { JSX } from 'react/jsx-runtime';
2
+ import { KeyboardEventHandler } from 'react';
3
+ import { MouseEventHandler } from 'react';
4
+ import { PreinitializedMapStore } from 'nanostores';
5
+ import { PreinitializedWritableAtom } from 'nanostores';
6
+ import { ReactNode } from 'react';
7
+
8
+ declare abstract class AbstractFocusProvider<T extends object> implements FocusProvider {
9
+ protected data: CompositeData<T>;
10
+ protected dataOptions: CompositeDataOptions<T>;
11
+ firstFocusableItem: CompositeDataItem<T> | null;
12
+ lastFocusableItem: CompositeDataItem<T> | null;
13
+ focusedItem: PreinitializedWritableAtom<CompositeDataItem<T> | null>;
14
+ init(data: CompositeData<T>, dataOptions: CompositeDataOptions<T>): void;
15
+ isFocusable(itemAtom: CompositeDataItem<T>): boolean;
16
+ focus(itemKey: CompositeItemKey): void;
17
+ focus(item: CompositeDataItem<T>): void;
18
+ abstract setFocusPointers(): readonly [first: CompositeDataItem<T> | null, last: CompositeDataItem<T> | null];
19
+ abstract focusUp(): void;
20
+ abstract focusDown(): void;
21
+ abstract focusLeft(): void;
22
+ abstract focusRight(): void;
23
+ abstract focusPageUp(): void;
24
+ abstract focesPageDown(): void;
25
+ abstract focusToFirst(): void;
26
+ abstract focusToLast(): void;
27
+ abstract focusToFirstInRow(): void;
28
+ abstract focusToLastInRow(): void;
29
+ abstract focusToTypeAheadMatch(): void;
30
+ }
31
+
32
+ declare abstract class AbstractSelectionProvider<T extends object> {
33
+ protected data: CompositeData<T>;
34
+ protected dataOptions: CompositeDataOptions<T>;
35
+ selectedKeys: PreinitializedWritableAtom<Set<CompositeItemKey>>;
36
+ init(data: CompositeData<T>, dataOptions: CompositeDataOptions<T>): void;
37
+ abstract select(itemKey?: CompositeItemKey, recursive?: boolean): void;
38
+ abstract select(item: CompositeDataItem<T>, recursive?: boolean): void;
39
+ abstract select(item?: CompositeDataItem<T> | CompositeItemKey, recursive?: boolean): void;
40
+ abstract deselect(itemKey?: CompositeItemKey, recursive?: boolean): void;
41
+ abstract deselect(item: CompositeDataItem<T>, recursive?: boolean): void;
42
+ abstract deselect(item?: CompositeDataItem<T> | CompositeItemKey, recursive?: boolean): void;
43
+ toggleSelect(itemKey?: CompositeItemKey, recursive?: boolean): void;
44
+ toggleSelect(item: CompositeDataItem<T>, recursive?: boolean): void;
45
+ }
46
+
47
+ declare interface BaseCompositeProps<T extends object> extends React.ComponentPropsWithoutRef<"div"> {
48
+ data: CompositeData<T>;
49
+ className?: string;
50
+ ref?: React.Ref<HTMLDivElement>;
51
+ handleRef?: React.Ref<CompositeHandle>;
52
+ renderItem: CompositeItemRenderFn<T>;
53
+ itemClassName?: string;
54
+ }
55
+
56
+ declare class CompositeData<T extends object> implements Iterable<CompositeDataItem<T>> {
57
+ #private;
58
+ lookup: Map<CompositeItemKey, CompositeDataItem<T>>;
59
+ focusProvider: AbstractFocusProvider<T>;
60
+ selectionProvider?: AbstractSelectionProvider<T>;
61
+ disabledKeys: Set<CompositeItemKey>;
62
+ root: CompositeDataItem<T>;
63
+ constructor(items: T[], providerOptions: CompositeProviderOptions<T>, options?: CompositeOptions);
64
+ [Symbol.iterator](): Iterator<CompositeDataItem<T>>;
65
+ toJSON(): T[];
66
+ init(items: T[]): void;
67
+ item(key: CompositeItemKey): CompositeDataItem<T> | undefined;
68
+ descendants(itemKey: CompositeItemKey): CompositeDataItem<T>[] | undefined;
69
+ ancestors(itemKey: CompositeItemKey): CompositeDataItem<T>[] | undefined;
70
+ updateItem(key: CompositeItemKey, data: Partial<T> | ((item: T) => Partial<T>)): void;
71
+ }
72
+
73
+ declare class CompositeDataItem<T extends object> implements Iterable<CompositeDataItem<T>> {
74
+ #private;
75
+ parent: CompositeDataItem<T> | null;
76
+ children?: CompositeDataItem<T>[];
77
+ level: number;
78
+ data: PreinitializedMapStore<T>;
79
+ state: PreinitializedMapStore<CompositeDataItemState>;
80
+ pointers: {
81
+ left?: CompositeItemKey;
82
+ right?: CompositeItemKey;
83
+ up?: CompositeItemKey;
84
+ down?: CompositeItemKey;
85
+ };
86
+ childrenProp: string;
87
+ constructor(item: T, options: CompositeDataItemOptions<T>, parent: CompositeDataItem<T> | null);
88
+ get key(): CompositeItemKey;
89
+ [Symbol.iterator](): Iterator<CompositeDataItem<T>>;
90
+ toJSON(): T;
91
+ descendants(): CompositeDataItem<T>[];
92
+ ancestors(): CompositeDataItem<T>[];
93
+ toggleSelect(recursive?: boolean): void;
94
+ select(recursive?: boolean): CompositeItemKey[];
95
+ deselect(recursive?: boolean): CompositeItemKey[];
96
+ disable(recursive?: boolean): CompositeItemKey[];
97
+ enable(recursive?: boolean): CompositeItemKey[];
98
+ }
99
+
100
+ declare type CompositeDataItemOptions<T> = CompositeDataPropGetters<T> & {
101
+ initialState?: CompositeDataItemState;
102
+ itemChildrenProp: string;
103
+ };
104
+
105
+ declare interface CompositeDataItemState {
106
+ focused: boolean;
107
+ selected: boolean;
108
+ disabled: boolean;
109
+ }
110
+
111
+ declare type CompositeDataOptions<T> = Required<CompositeOptions> & CompositeDataPropGetters<T>;
112
+
113
+ declare interface CompositeDataPropGetters<T> {
114
+ getItemKey: (item: T) => CompositeItemKey;
115
+ getItemChildren: (item: T) => T[] | undefined;
116
+ }
117
+
118
+ declare interface CompositeEventHandlers {
119
+ mouseEventHandler?: MouseEventHandler<HTMLElement>;
120
+ keyboardEventHandler?: KeyboardEventHandler<HTMLElement>;
121
+ }
122
+
123
+ declare interface CompositeHandle {
124
+ focusProvider: FocusProvider;
125
+ selectionProvider?: SelectionProvider;
126
+ }
127
+
128
+ declare type CompositeItemKey = string | number;
129
+
130
+ declare type CompositeItemRenderFn<T extends object> = (item: {
131
+ data: T;
132
+ level: number;
133
+ key: CompositeItemKey;
134
+ }, state: CompositeDataItemState, eventHandlers: CompositeEventHandlers) => ReactNode;
135
+
136
+ declare interface CompositeOptions {
137
+ disabledKeys?: CompositeItemKey[];
138
+ selectedKeys?: CompositeItemKey[];
139
+ itemKeyProp?: string;
140
+ itemChildrenProp?: string;
141
+ }
142
+
143
+ declare interface CompositeProviderOptions<T extends object> {
144
+ focusProvider: AbstractFocusProvider<T>;
145
+ selectionProvider?: AbstractSelectionProvider<T>;
146
+ }
147
+
148
+ declare interface FocusProvider {
149
+ focus(itemKey: CompositeItemKey): void;
150
+ focusUp(): void;
151
+ focusDown(): void;
152
+ focusLeft(): void;
153
+ focusRight(): void;
154
+ focusPageUp(): void;
155
+ focesPageDown(): void;
156
+ focusToFirst(): void;
157
+ focusToLast(): void;
158
+ focusToFirstInRow(): void;
159
+ focusToLastInRow(): void;
160
+ focusToTypeAheadMatch(): void;
161
+ }
162
+
163
+ export declare function Listbox<T extends object>({ data, ...props }: BaseCompositeProps<T>): JSX.Element;
164
+
165
+ declare interface SelectionProvider {
166
+ select(itemKey?: CompositeItemKey, recursive?: boolean): void;
167
+ deselect(itemKey?: CompositeItemKey, recursive?: boolean): void;
168
+ toggleSelect(itemKey?: CompositeItemKey, recursive?: boolean): void;
169
+ }
170
+
171
+ export { }
@@ -0,0 +1,76 @@
1
+ import { jsx as y } from "react/jsx-runtime";
2
+ import { C as d } from "./composite-component-DSUbd1XS.js";
3
+ import { useMemo as m, useRef as a, useCallback as u } from "react";
4
+ const h = /* @__PURE__ */ new Set(["ctrl", "shift", "alt", "meta"]), f = {
5
+ " ": "space"
6
+ };
7
+ function K(e) {
8
+ const r = [];
9
+ for (const [t, n] of Object.entries(e)) {
10
+ const o = t.toLowerCase().trim().split(/\s*\+\s*/g);
11
+ o.length === 1 && h.has(o[0]) ? console.error(`[useKeyboardEvent] '${t}': A key sequence cannot be only a modifier key.`) : o.includes("") ? console.error(`[useKeyboardEvent] '${t}': Invalid key sequence defined in the sequence.`) : r.push({
12
+ sequence: new RegExp("^" + o.join("\\+") + "$"),
13
+ handler: n
14
+ });
15
+ }
16
+ return r;
17
+ }
18
+ const l = {
19
+ eventKeyProp: "key"
20
+ };
21
+ function v(e, r = l) {
22
+ const t = { ...r, ...l }, n = K(e);
23
+ return (o) => {
24
+ const c = o[t.eventKeyProp], s = [];
25
+ o.ctrlKey && s.push("ctrl"), o.shiftKey && s.push("shift"), o.altKey && s.push("alt"), o.metaKey && s.push("meta"), f[c] ? s.push(f[c]) : s.push(c.toLowerCase());
26
+ const i = n.find((p) => p.sequence.test(s.join("+")));
27
+ i && (o.preventDefault(), o.stopPropagation(), i.handler(o));
28
+ };
29
+ }
30
+ function P(e, r) {
31
+ return m(() => v(e, r), [e, r]);
32
+ }
33
+ function k({ data: e, ...r }) {
34
+ const t = a(null), n = u(() => {
35
+ const s = e.focusProvider.focusedItem.get()?.key;
36
+ if (s && t.current) {
37
+ const i = t.current.querySelector(`[data-key="${s}"]`);
38
+ i && i.focus();
39
+ }
40
+ }, [e]), o = P({
41
+ ArrowUp: () => {
42
+ e.focusProvider.focusUp(), n();
43
+ },
44
+ ArrowDown: () => {
45
+ e.focusProvider.focusDown(), n();
46
+ },
47
+ Home: () => {
48
+ e.focusProvider.focusToFirst(), n();
49
+ },
50
+ End: () => {
51
+ e.focusProvider.focusToLast(), n();
52
+ },
53
+ Space: () => {
54
+ e.selectionProvider?.toggleSelect(), n();
55
+ }
56
+ }), c = u(
57
+ (s) => {
58
+ e.focusProvider.focus(s.key), e.selectionProvider?.toggleSelect(s), n();
59
+ },
60
+ [e.focusProvider, e.selectionProvider, n]
61
+ );
62
+ return /* @__PURE__ */ y(
63
+ d,
64
+ {
65
+ ref: t,
66
+ variant: "listbox",
67
+ data: e,
68
+ onKeyDown: o,
69
+ itemMouseEventHandler: c,
70
+ ...r
71
+ }
72
+ );
73
+ }
74
+ export {
75
+ k as Listbox
76
+ };
@@ -1,12 +1,12 @@
1
1
  import { jsx as o, jsxs as c, Fragment as d } from "react/jsx-runtime";
2
2
  import { useId as p, useMemo as l } from "react";
3
- import { IconGenerator as M } from "./icon-generator.js";
3
+ import { I as M } from "./icon-generator-tuhuqdpL.js";
4
4
  const h = { cover: "slice", contain: "meet", fill: "none" }, k = { top: "xMidYMin", center: "xMidYMid", bottom: "xMidYMax" };
5
- function u({
6
- src: n,
5
+ function g({
6
+ src: s,
7
7
  imageFit: e = "cover",
8
8
  imagePosition: r = "top",
9
- maskType: s = "alpha",
9
+ maskType: n = "alpha",
10
10
  ...a
11
11
  }) {
12
12
  const t = p(), i = l(
@@ -18,12 +18,12 @@ function u({
18
18
  {
19
19
  ...a,
20
20
  renderChildren: (m) => /* @__PURE__ */ c(d, { children: [
21
- /* @__PURE__ */ o("mask", { id: `svg-mask${t}`, "mask-type": s, children: m }),
22
- /* @__PURE__ */ o("image", { href: n, width: "100%", height: "100%", mask: `url(#svg-mask${t})`, preserveAspectRatio: i })
21
+ /* @__PURE__ */ o("mask", { id: `svg-mask${t}`, "mask-type": n, children: m }),
22
+ /* @__PURE__ */ o("image", { href: s, width: "100%", height: "100%", mask: `url(#svg-mask${t})`, preserveAspectRatio: i })
23
23
  ] })
24
24
  }
25
25
  );
26
26
  }
27
27
  export {
28
- u as MaskedImageGenerator
28
+ g as MaskedImageGenerator
29
29
  };
@@ -1,9 +1,9 @@
1
- import { IconGenerator as o } from "./icon-generator.js";
1
+ import { I as o } from "./icon-generator-tuhuqdpL.js";
2
2
  import { MaskedImageGenerator as e } from "./masked-image-generator.js";
3
3
  import { Caption as i, captionVariants as p } from "./caption.js";
4
4
  import { Banner as m } from "./banner.js";
5
5
  import { Button as u, buttonVariants as C } from "./button.js";
6
- import { Card as x, CardArea as M, CardCaption as v, CardContent as I, CardImage as N, CardItemGroup as c, CardMedia as l, CardMeta as s, CardSubcaption as S, CardSubtitle as T, CardTitle as b, CardToolbar as V } from "./card.js";
6
+ import { Card as x, CardArea as M, CardCaption as v, CardContent as I, CardImage as N, CardItemGroup as s, CardMedia as c, CardMeta as l, CardSubcaption as S, CardSubtitle as T, CardTitle as b, CardToolbar as V } from "./card.js";
7
7
  import { HorizontalList as L } from "./horizontal-list.js";
8
8
  import { Input as k } from "./input.js";
9
9
  import { NavigationMenu as B, NavigationMenuContent as H, NavigationMenuIndicator as h, NavigationMenuItem as w, NavigationMenuLink as z, NavigationMenuList as A, NavigationMenuTrigger as O, NavigationMenuViewport as j, navigationMenuTriggerStyle as q } from "./navigation-menu.js";
@@ -22,9 +22,9 @@ export {
22
22
  v as CardCaption,
23
23
  I as CardContent,
24
24
  N as CardImage,
25
- c as CardItemGroup,
26
- l as CardMedia,
27
- s as CardMeta,
25
+ s as CardItemGroup,
26
+ c as CardMedia,
27
+ l as CardMeta,
28
28
  S as CardSubcaption,
29
29
  T as CardSubtitle,
30
30
  b as CardTitle,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bccampus/ui-components",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "packageManager": "yarn@4.10.3",
6
6
  "exports": {
@@ -12,6 +12,10 @@
12
12
  "types": "./dist/composite.d.ts",
13
13
  "import": "./dist/composite.js"
14
14
  },
15
+ "./listbox": {
16
+ "types": "./dist/listbox.d.ts",
17
+ "import": "./dist/listbox.js"
18
+ },
15
19
  "./icon-generator": {
16
20
  "types": "./dist/icon-generator.d.ts",
17
21
  "import": "./dist/icon-generator.js"
@@ -1,21 +1,22 @@
1
- import { difference, union } from "@/lib/set-operations";
2
- import { atom, type PreinitializedWritableAtom } from "nanostores";
3
- import type { CompositeDataOptions, CompositeItemKey, CompositeOptions } from "./types";
1
+ import type { CompositeDataOptions, CompositeItemKey, CompositeOptions, CompositeProviderOptions } from "./types";
4
2
  import { CompositeDataItem } from "./CompositeDataItem";
3
+ import type { AbstractFocusProvider } from "./FocusProvider/AbstractFocusProvider";
4
+ import type { AbstractSelectionProvider } from "./SelectionProvider/AbstractSelectionProvider";
5
+ import { union } from "@/lib/set-operations";
5
6
 
6
7
  export class CompositeData<T extends object> implements Iterable<CompositeDataItem<T>> {
7
8
  #options: CompositeDataOptions<T>;
8
9
 
9
10
  lookup: Map<CompositeItemKey, CompositeDataItem<T>> = new Map();
10
11
 
11
- firstFocusableItem!: CompositeDataItem<T> | null;
12
- focusedItem: PreinitializedWritableAtom<CompositeDataItem<T> | null> = atom(null);
13
- disabledKeys: PreinitializedWritableAtom<Set<CompositeItemKey>> = atom(new Set());
14
- selectedKeys: PreinitializedWritableAtom<Set<CompositeItemKey>> = atom(new Set());
12
+ focusProvider: AbstractFocusProvider<T>;
13
+ selectionProvider?: AbstractSelectionProvider<T>;
14
+
15
+ disabledKeys: Set<CompositeItemKey> = new Set();
15
16
 
16
17
  root!: CompositeDataItem<T>;
17
18
 
18
- constructor(items: T[], options?: CompositeOptions) {
19
+ constructor(items: T[], providerOptions: CompositeProviderOptions<T>, options?: CompositeOptions) {
19
20
  this.#options = {
20
21
  itemKeyProp: "key",
21
22
  itemChildrenProp: "children",
@@ -29,6 +30,9 @@ export class CompositeData<T extends object> implements Iterable<CompositeDataIt
29
30
  ...options
30
31
  };
31
32
 
33
+ this.focusProvider = providerOptions.focusProvider;
34
+ this.selectionProvider = providerOptions.selectionProvider;
35
+
32
36
  this.init(items);
33
37
  }
34
38
 
@@ -49,7 +53,6 @@ export class CompositeData<T extends object> implements Iterable<CompositeDataIt
49
53
 
50
54
 
51
55
  init(items: T[]) {
52
- // this.itemAtoms = this.#setItemMap(items);
53
56
  this.root = new CompositeDataItem(
54
57
  { [this.#options.itemKeyProp]: "ALL", [this.#options.itemChildrenProp]: items } as T,
55
58
  this.#options,
@@ -60,50 +63,12 @@ export class CompositeData<T extends object> implements Iterable<CompositeDataIt
60
63
  this.lookup = new Map([...this.root].map(item => [item.key, item]));
61
64
 
62
65
  // Set initially disabled items
63
- this.#options.disabledKeys.forEach(disabledKey => this.disable(disabledKey));
64
-
65
- // Set initially selected items : SelectionProvider
66
- this.#options.selectedKeys.forEach(selectedKey => this.select(selectedKey));
67
-
68
- // Set initially focused items : FocusProvider
69
- this.firstFocusableItem = this.getFirstFocusableItem() ?? null;
70
- if (this.firstFocusableItem) {
71
- this.firstFocusableItem.state.setKey("focused", true);
72
- this.focusedItem.set(this.firstFocusableItem);
73
- }
74
-
75
- // Set focus pointers: FocusProvider
76
- const lookupData = [...this.lookup];
77
- for (let index = 0; index < lookupData.length; index++) {
78
- const [key, item] = lookupData[index];
79
-
80
- if (!this.isFocusable(item)) continue;
81
-
82
- if (index < lookupData.length - 1) {
83
- let newIndex = index === lookupData.length - 1 ? 0 : index + 1;
84
- while (newIndex < lookupData.length && !this.isFocusable(lookupData[newIndex][1])) {
85
- newIndex = (newIndex + 1) % lookupData.length;
86
- }
87
-
88
- item.pointers.down = lookupData[newIndex][0];
89
- lookupData[newIndex][1].pointers.up = key;
90
- }
91
- }
92
- }
66
+ this.#options.disabledKeys.forEach(disabledKey => this.#disable(disabledKey));
93
67
 
68
+ if (this.selectionProvider)
69
+ this.selectionProvider.init(this, this.#options);
94
70
 
95
- isFocusable(itemAtom: CompositeDataItem<T>) {
96
- return !itemAtom.state.get().disabled;
97
- }
98
-
99
- getFirstFocusableItem() {
100
- let iterItem = this.lookup.values().next();
101
-
102
- while (iterItem.value && !this.isFocusable(iterItem.value)) {
103
- iterItem = this.lookup.values().next();
104
- }
105
-
106
- return iterItem.value;
71
+ this.focusProvider.init(this, this.#options);
107
72
  }
108
73
 
109
74
  item(key: CompositeItemKey) { return this.lookup.get(key) };
@@ -122,71 +87,14 @@ export class CompositeData<T extends object> implements Iterable<CompositeDataIt
122
87
  return item.ancestors();
123
88
  }
124
89
 
125
- focus(itemKey: CompositeItemKey): void;
126
- focus(item: CompositeDataItem<T>): void;
127
- focus(item: CompositeDataItem<T> | CompositeItemKey) {
128
- const _item = item instanceof CompositeDataItem ? item : this.lookup.get(item);
129
- if (!_item) return;
130
-
131
- if (this.focusedItem.get()) {
132
- if (_item.key === this.focusedItem.get()?.key) return;
133
-
134
- this.focusedItem.get()!.state.setKey("focused", false);
135
- }
136
-
137
- _item.state.setKey("focused", true);
138
- this.focusedItem.set(_item);
139
- }
140
-
141
- toggleSelect(itemKey: CompositeItemKey, recursive?: boolean): void;
142
- toggleSelect(item: CompositeDataItem<T>, recursive?: boolean): void;
143
- toggleSelect(item: CompositeDataItem<T> | CompositeItemKey, recursive: boolean = false) {
144
- const _item = item instanceof CompositeDataItem ? item : this.lookup.get(item);
145
- if (!_item) return;
146
-
147
- if (_item.state.get().selected) this.deselect(_item, recursive);
148
- else this.select(_item, recursive);
149
- }
150
-
151
- select(itemKey: CompositeItemKey, recursive?: boolean): void;
152
- select(item: CompositeDataItem<T>, recursive?: boolean): void;
153
- select(item: CompositeDataItem<T> | CompositeItemKey, recursive: boolean = false) {
154
- const _item = item instanceof CompositeDataItem ? item : this.lookup.get(item);
155
- if (!_item) return;
156
-
157
- const selectedKeys = _item.select(recursive);
158
- this.selectedKeys.set(union(this.selectedKeys.get(), selectedKeys));
159
- }
160
-
161
- deselect(itemKey: CompositeItemKey, recursive?: boolean): void;
162
- deselect(item: CompositeDataItem<T>, recursive?: boolean): void;
163
- deselect(item: CompositeDataItem<T> | CompositeItemKey, recursive: boolean = false) {
164
- const _item = item instanceof CompositeDataItem ? item : this.lookup.get(item);
165
- if (!_item) return;
166
-
167
- const deselectedKeys = _item.deselect(recursive);
168
- this.selectedKeys.set(difference(this.selectedKeys.get(), deselectedKeys));
169
- }
170
-
171
- disable(itemKey: CompositeItemKey, recursive?: boolean): void;
172
- disable(item: CompositeDataItem<T>, recursive?: boolean): void;
173
- disable(item: CompositeDataItem<T> | CompositeItemKey, recursive: boolean = false) {
90
+ #disable(itemKey: CompositeItemKey, recursive?: boolean): void;
91
+ #disable(item: CompositeDataItem<T>, recursive?: boolean): void;
92
+ #disable(item: CompositeDataItem<T> | CompositeItemKey, recursive: boolean = false) {
174
93
  const _item = item instanceof CompositeDataItem ? item : this.lookup.get(item);
175
94
  if (!_item) return;
176
95
 
177
96
  const disabledKeys = _item.disable(recursive);
178
- this.disabledKeys.set(union(this.selectedKeys.get(), disabledKeys));
179
- }
180
-
181
-
182
- enable(itemKey: CompositeItemKey, recursive?: boolean): void;
183
- enable(item: CompositeDataItem<T>, recursive?: boolean): void;
184
- enable(item: CompositeDataItem<T> | CompositeItemKey, recursive: boolean = false) {
185
- const _item = item instanceof CompositeDataItem ? item : this.lookup.get(item);
186
- if (!_item) return;
187
-
188
- const enabledKeys = _item.enable(recursive);
189
- this.disabledKeys.set(union(this.selectedKeys.get(), enabledKeys));
97
+ this.disabledKeys = union(this.disabledKeys, disabledKeys);
190
98
  }
191
99
 
192
100
 
@@ -208,8 +116,8 @@ export class CompositeData<T extends object> implements Iterable<CompositeDataIt
208
116
  }
209
117
 
210
118
  #updateIfSelectedItem(key: CompositeItemKey) {
211
- if (this.selectedKeys.get().has(key)) {
212
- this.selectedKeys.set(new Set([...this.selectedKeys.get()]));
119
+ if (this.selectionProvider && this.selectionProvider.selectedKeys.get().has(key)) {
120
+ this.selectionProvider.selectedKeys.set(new Set([...this.selectionProvider.selectedKeys.get()]));
213
121
  }
214
122
  }
215
123
  }
@@ -0,0 +1,83 @@
1
+ import { atom, type PreinitializedWritableAtom } from 'nanostores';
2
+ import type { CompositeData } from '../CompositeData';
3
+ import { CompositeDataItem } from '../CompositeDataItem';
4
+ import type { CompositeDataOptions, CompositeItemKey } from '../types';
5
+
6
+ export interface FocusProvider {
7
+ focus(itemKey: CompositeItemKey): void;
8
+
9
+ focusUp(): void;
10
+ focusDown(): void;
11
+ focusLeft(): void;
12
+ focusRight(): void;
13
+ focusPageUp(): void;
14
+ focesPageDown(): void;
15
+ focusToFirst(): void;
16
+ focusToLast(): void;
17
+ focusToFirstInRow(): void;
18
+ focusToLastInRow(): void;
19
+ focusToTypeAheadMatch(): void;
20
+ }
21
+
22
+ export abstract class AbstractFocusProvider<T extends object> implements FocusProvider {
23
+ protected data!: CompositeData<T>;
24
+ protected dataOptions!: CompositeDataOptions<T>;
25
+
26
+ firstFocusableItem!: CompositeDataItem<T> | null;
27
+ lastFocusableItem!: CompositeDataItem<T> | null;
28
+ focusedItem: PreinitializedWritableAtom<CompositeDataItem<T> | null> = atom(null);
29
+
30
+ init(data: CompositeData<T>, dataOptions: CompositeDataOptions<T>) {
31
+ this.data = data;
32
+ this.dataOptions = dataOptions;
33
+
34
+
35
+ // Set focus pointers
36
+ const [first, last] = this.setFocusPointers();
37
+
38
+ // Set initially focused items
39
+ this.firstFocusableItem = first;
40
+ if (this.firstFocusableItem) {
41
+ this.firstFocusableItem.state.setKey("focused", true);
42
+ this.focusedItem.set(this.firstFocusableItem);
43
+ }
44
+
45
+ this.lastFocusableItem = last;
46
+ }
47
+
48
+
49
+ isFocusable(itemAtom: CompositeDataItem<T>) {
50
+ return !itemAtom.state.get().disabled;
51
+ }
52
+
53
+ focus(itemKey: CompositeItemKey): void;
54
+ focus(item: CompositeDataItem<T>): void;
55
+ focus(item: CompositeDataItem<T> | CompositeItemKey) {
56
+ const _item = item instanceof CompositeDataItem ? item : this.data.lookup.get(item);
57
+ if (!_item) return;
58
+
59
+ if (this.focusedItem.get()) {
60
+ if (_item.key === this.focusedItem.get()?.key) return;
61
+
62
+ this.focusedItem.get()!.state.setKey("focused", false);
63
+ }
64
+
65
+ _item.state.setKey("focused", true);
66
+ this.focusedItem.set(_item);
67
+ }
68
+
69
+ abstract setFocusPointers(): readonly [first: CompositeDataItem<T> | null, last: CompositeDataItem<T> | null];
70
+
71
+ abstract focusUp(): void;
72
+ abstract focusDown(): void;
73
+ abstract focusLeft(): void;
74
+ abstract focusRight(): void;
75
+ abstract focusPageUp(): void;
76
+ abstract focesPageDown(): void;
77
+ abstract focusToFirst(): void;
78
+ abstract focusToLast(): void;
79
+ abstract focusToFirstInRow(): void;
80
+ abstract focusToLastInRow(): void;
81
+ abstract focusToTypeAheadMatch(): void;
82
+
83
+ }