@multiplatform.one/components 7.10.0 → 7.11.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.
Files changed (65) hide show
  1. package/README.md +98 -0
  2. package/dist/cjs/SheetModal/index.cjs +22 -3
  3. package/dist/cjs/SheetModal/index.native.js +50 -10
  4. package/dist/cjs/SheetModal/index.native.js.map +1 -1
  5. package/dist/cjs/Tabs/index.cjs +33 -12
  6. package/dist/cjs/Tabs/index.native.js +37 -12
  7. package/dist/cjs/Tabs/index.native.js.map +1 -1
  8. package/dist/cjs/layouts/tags.native.js.map +1 -1
  9. package/dist/cjs/views/TreeView.cjs +175 -43
  10. package/dist/cjs/views/TreeView.native.js +181 -46
  11. package/dist/cjs/views/TreeView.native.js.map +1 -1
  12. package/dist/esm/SheetModal/index.mjs +22 -3
  13. package/dist/esm/SheetModal/index.mjs.map +1 -1
  14. package/dist/esm/SheetModal/index.native.js +51 -11
  15. package/dist/esm/SheetModal/index.native.js.map +1 -1
  16. package/dist/esm/Tabs/index.mjs +33 -12
  17. package/dist/esm/Tabs/index.mjs.map +1 -1
  18. package/dist/esm/Tabs/index.native.js +37 -12
  19. package/dist/esm/Tabs/index.native.js.map +1 -1
  20. package/dist/esm/layouts/tags.mjs.map +1 -1
  21. package/dist/esm/layouts/tags.native.js.map +1 -1
  22. package/dist/esm/views/TreeView.mjs +175 -43
  23. package/dist/esm/views/TreeView.mjs.map +1 -1
  24. package/dist/esm/views/TreeView.native.js +181 -46
  25. package/dist/esm/views/TreeView.native.js.map +1 -1
  26. package/dist/jsx/SheetModal/index.mjs +22 -3
  27. package/dist/jsx/SheetModal/index.mjs.map +1 -1
  28. package/dist/jsx/SheetModal/index.native.js +50 -10
  29. package/dist/jsx/SheetModal/index.native.js.map +1 -1
  30. package/dist/jsx/Tabs/index.mjs +33 -12
  31. package/dist/jsx/Tabs/index.mjs.map +1 -1
  32. package/dist/jsx/Tabs/index.native.js +37 -12
  33. package/dist/jsx/Tabs/index.native.js.map +1 -1
  34. package/dist/jsx/layouts/tags.mjs.map +1 -1
  35. package/dist/jsx/layouts/tags.native.js.map +1 -1
  36. package/dist/jsx/views/TreeView.mjs +175 -43
  37. package/dist/jsx/views/TreeView.mjs.map +1 -1
  38. package/dist/jsx/views/TreeView.native.js +181 -46
  39. package/dist/jsx/views/TreeView.native.js.map +1 -1
  40. package/package.json +11 -11
  41. package/src/Accordion/index.tsx +1 -1
  42. package/src/SheetModal/SheetModal.fill.spec.tsx +147 -0
  43. package/src/SheetModal/SheetModal.ownedBody.spec.tsx +200 -0
  44. package/src/SheetModal/SheetModal.stories.spec.tsx +23 -2
  45. package/src/SheetModal/SheetModal.stories.tsx +275 -2
  46. package/src/SheetModal/index.native.tsx +50 -6
  47. package/src/SheetModal/index.tsx +40 -3
  48. package/src/Tabs/Tabs.lifecycle.spec.tsx +123 -0
  49. package/src/Tabs/Tabs.measurement.spec.tsx +186 -0
  50. package/src/Tabs/Tabs.stories.tsx +43 -0
  51. package/src/Tabs/index.tsx +42 -13
  52. package/src/layouts/tags.tsx +2 -1
  53. package/src/views/TreeView.reveal.spec.tsx +7 -0
  54. package/src/views/TreeView.reveal.stories.tsx +134 -0
  55. package/src/views/TreeView.scroll-index.spec.tsx +246 -0
  56. package/src/views/TreeView.scroll.spec.tsx +711 -50
  57. package/src/views/TreeView.tsx +230 -49
  58. package/types/SheetModal/index.d.ts +8 -1
  59. package/types/SheetModal/index.d.ts.map +1 -1
  60. package/types/SheetModal/index.native.d.ts +1 -1
  61. package/types/SheetModal/index.native.d.ts.map +1 -1
  62. package/types/Tabs/index.d.ts.map +1 -1
  63. package/types/layouts/tags.d.ts.map +1 -1
  64. package/types/views/TreeView.d.ts +10 -1
  65. package/types/views/TreeView.d.ts.map +1 -1
@@ -0,0 +1,246 @@
1
+ import type { FlashListProps, FlashListRef } from "@shopify/flash-list";
2
+ import { act, cleanup, fireEvent, screen, waitFor } from "@testing-library/react";
3
+ import { renderWithProviders } from "@multiplatform.one/test-utils";
4
+ import { afterEach, expect, it, vi } from "vitest";
5
+ import { TreeView } from "./TreeView";
6
+
7
+ const observed = vi.hoisted(() => ({ list: null as FlashListRef<unknown> | null }));
8
+ vi.mock("@shopify/flash-list", async (importOriginal) => {
9
+ const actual = await importOriginal<typeof import("@shopify/flash-list")>();
10
+ const React = await import("react");
11
+ return {
12
+ ...actual,
13
+ FlashList: React.forwardRef<FlashListRef<unknown>, FlashListProps<unknown>>((props, ref) => {
14
+ const captureRef = React.useCallback(
15
+ (list: FlashListRef<unknown> | null) => {
16
+ observed.list = list;
17
+ if (typeof ref === "function") ref(list);
18
+ else if (ref) ref.current = list;
19
+ },
20
+ [ref],
21
+ );
22
+ return React.createElement(actual.FlashList, { ...props, ref: captureRef });
23
+ }),
24
+ };
25
+ });
26
+ afterEach(() => {
27
+ cleanup();
28
+ vi.restoreAllMocks();
29
+ });
30
+
31
+ it("reports no preparatory index offset while the actual scroll transport is deferred", async () => {
32
+ vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(320);
33
+ vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockReturnValue(240);
34
+ vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockReturnValue({
35
+ x: 0,
36
+ y: 0,
37
+ top: 0,
38
+ left: 0,
39
+ right: 320,
40
+ bottom: 240,
41
+ width: 320,
42
+ height: 240,
43
+ toJSON: () => ({}),
44
+ });
45
+ const change = vi.fn();
46
+ renderWithProviders(
47
+ <TreeView
48
+ nodes={Array.from({ length: 100 }, (_, i) => ({ id: `row-${i}`, label: `Row ${i}` }))}
49
+ height={240}
50
+ onScrollOffsetChange={change}
51
+ />,
52
+ );
53
+ await waitFor(() => expect(observed.list?.getChildContainerDimensions().height).toBe(24000));
54
+ await waitFor(() => expect(change).toHaveBeenCalledWith(0));
55
+ expect(observed.list?.getWindowSize().height).toBe(240);
56
+ expect(observed.list?.getLayout(99)?.y).toBe(23760);
57
+ const native = observed.list!.getNativeScrollRef()!;
58
+ const transport = vi.spyOn(native, "scrollTo").mockImplementation(() => {});
59
+ const scroller = native.getScrollableNode();
60
+ const index = vi.spyOn(observed.list!, "scrollToIndex");
61
+ change.mockClear();
62
+ fireEvent.keyDown(screen.getByRole("tree"), { key: "End" });
63
+ expect(index).toHaveBeenCalledExactlyOnceWith({ index: 99, animated: true });
64
+ await act(async () => {
65
+ await new Promise((resolve) => setTimeout(resolve, 40));
66
+ });
67
+ expect(transport).toHaveBeenCalled();
68
+ expect(observed.list!.getAbsoluteLastScrollOffset()).toBeGreaterThan(0);
69
+ expect(scroller.scrollTop).toBe(0);
70
+ expect(change).not.toHaveBeenCalled();
71
+ await act(async () => {
72
+ await index.mock.results[0].value;
73
+ });
74
+ expect(change).not.toHaveBeenCalled();
75
+ fireEvent.scroll(scroller, { target: { scrollTop: 96.5 } });
76
+ await waitFor(() => expect(change).toHaveBeenCalledExactlyOnceWith(96.5));
77
+ change.mockClear();
78
+ fireEvent.keyDown(screen.getByRole("tree"), { key: "Home" });
79
+ expect(index).toHaveBeenLastCalledWith({ index: 0, animated: true });
80
+ await act(async () => {
81
+ await index.mock.results[1].value;
82
+ });
83
+ expect(change).not.toHaveBeenCalled();
84
+ fireEvent.scroll(scroller, { target: { scrollTop: 0 } });
85
+ await waitFor(() => expect(change).toHaveBeenCalledExactlyOnceWith(0));
86
+ });
87
+
88
+ it("positive control: actual scroll delivery reports the transported offset", async () => {
89
+ vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(320);
90
+ vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockReturnValue(240);
91
+ vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockReturnValue({
92
+ x: 0,
93
+ y: 0,
94
+ top: 0,
95
+ left: 0,
96
+ right: 320,
97
+ bottom: 240,
98
+ width: 320,
99
+ height: 240,
100
+ toJSON: () => ({}),
101
+ });
102
+ const change = vi.fn();
103
+ renderWithProviders(
104
+ <TreeView
105
+ nodes={Array.from({ length: 100 }, (_, i) => ({ id: `row-${i}`, label: `Row ${i}` }))}
106
+ height={240}
107
+ onScrollOffsetChange={change}
108
+ />,
109
+ );
110
+ await waitFor(() => expect(observed.list?.getChildContainerDimensions().height).toBe(24000));
111
+ await waitFor(() => expect(change).toHaveBeenCalledWith(0));
112
+ change.mockClear();
113
+ const native = observed.list!.getNativeScrollRef()!;
114
+ const scroller = native.getScrollableNode();
115
+ fireEvent.scroll(scroller, { target: { scrollTop: 96.5 } });
116
+ await waitFor(() => expect(change).toHaveBeenCalledWith(96.5));
117
+ });
118
+
119
+ function dimensions() {
120
+ vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(320);
121
+ vi.spyOn(HTMLElement.prototype, "clientHeight", "get").mockReturnValue(240);
122
+ vi.spyOn(HTMLElement.prototype, "getBoundingClientRect").mockImplementation(
123
+ function (this: HTMLElement) {
124
+ let y = 0;
125
+ for (let ancestor = this.parentElement; ancestor; ancestor = ancestor.parentElement) {
126
+ y -= ancestor.scrollTop;
127
+ }
128
+ return {
129
+ x: 0,
130
+ y,
131
+ top: y,
132
+ left: 0,
133
+ right: 320,
134
+ bottom: y + 240,
135
+ width: 320,
136
+ height: 240,
137
+ toJSON: () => ({}),
138
+ };
139
+ },
140
+ );
141
+ }
142
+
143
+ it.each([false, true])(
144
+ "acknowledges index-prepared reveal only after physical delivery (delivered: %s)",
145
+ async (delivered) => {
146
+ dimensions();
147
+ const nodes = Array.from({ length: 100 }, (_, i) => ({ id: `row-${i}`, label: `Row ${i}` }));
148
+ const change = vi.fn();
149
+ const complete = vi.fn();
150
+ const tree = renderWithProviders(
151
+ <TreeView
152
+ nodes={nodes}
153
+ height={240}
154
+ onScrollOffsetChange={change}
155
+ onRevealComplete={complete}
156
+ />,
157
+ );
158
+ await waitFor(() => expect(observed.list?.getChildContainerDimensions().height).toBe(24000));
159
+ await waitFor(() => expect(change).toHaveBeenCalledWith(0));
160
+ const native = observed.list!.getNativeScrollRef()!;
161
+ const transport = vi.spyOn(native, "scrollTo").mockImplementation(() => {});
162
+ const scroller = native.getScrollableNode();
163
+ const index = vi.spyOn(observed.list!, "scrollToIndex");
164
+ fireEvent.keyDown(screen.getByRole("tree"), { key: "End" });
165
+ await act(async () => {
166
+ await index.mock.results[0].value;
167
+ });
168
+ expect(transport).toHaveBeenCalled();
169
+ expect(scroller.scrollTop).toBe(0);
170
+ const offset = observed.list!.getAbsoluteLastScrollOffset();
171
+ const visible = observed.list!.computeVisibleIndices();
172
+ const id = `row-${visible.startIndex}`;
173
+ const layout = observed.list!.getLayout(visible.startIndex)!;
174
+ expect(offset).toBeGreaterThan(0);
175
+ expect(layout.isHeightMeasured).toBe(true);
176
+ expect(screen.queryByText(`Row ${visible.startIndex}`)).not.toBeNull();
177
+ if (delivered) {
178
+ fireEvent.scroll(scroller, { target: { scrollTop: offset } });
179
+ expect(scroller.scrollTop).toBe(offset);
180
+ await waitFor(() => expect(change).toHaveBeenCalledWith(offset));
181
+ }
182
+ transport.mockClear();
183
+ tree.rerender(
184
+ <TreeView
185
+ nodes={nodes}
186
+ height={240}
187
+ onScrollOffsetChange={change}
188
+ onRevealComplete={complete}
189
+ revealRequest={{ id, requestId: 71 }}
190
+ />,
191
+ );
192
+ await act(async () => {
193
+ await new Promise((resolve) => setTimeout(resolve, 60));
194
+ });
195
+ if (!delivered) {
196
+ expect(complete).not.toHaveBeenCalled();
197
+ expect(scroller.scrollTop).toBe(0);
198
+ expect(transport).toHaveBeenCalledWith({ x: 0, y: offset, animated: false });
199
+ } else {
200
+ expect(complete).toHaveBeenCalledExactlyOnceWith({ id, requestId: 71 });
201
+ }
202
+ fireEvent.scroll(scroller, { target: { scrollTop: offset } });
203
+ await waitFor(() => expect(complete).toHaveBeenCalledExactlyOnceWith({ id, requestId: 71 }));
204
+ },
205
+ );
206
+
207
+ it("reveals an observed keyboard destination without a retention callback", async () => {
208
+ dimensions();
209
+ const nodes = Array.from({ length: 100 }, (_, i) => ({ id: `row-${i}`, label: `Row ${i}` }));
210
+ const complete = vi.fn();
211
+ const tree = renderWithProviders(
212
+ <TreeView nodes={nodes} height={240} onRevealComplete={complete} />,
213
+ );
214
+ await waitFor(() => expect(observed.list?.getChildContainerDimensions().height).toBe(24000));
215
+ const list = observed.list!;
216
+ const native = list.getNativeScrollRef()!;
217
+ const transport = vi.spyOn(native, "scrollTo").mockImplementation(() => {});
218
+ const scroller = native.getScrollableNode();
219
+ const index = vi.spyOn(list, "scrollToIndex");
220
+ fireEvent.keyDown(screen.getByRole("tree"), { key: "End" });
221
+ await act(async () => {
222
+ await index.mock.results[0].value;
223
+ });
224
+ const target = list.computeVisibleIndices().endIndex;
225
+ const layout = list.getLayout(target)!;
226
+ const offset = layout.y + list.getFirstItemOffset();
227
+ expect(offset).toBeGreaterThan(list.getAbsoluteLastScrollOffset());
228
+ expect(layout.isHeightMeasured).toBe(true);
229
+ expect(screen.getByText(`Row ${target}`)).toBeTruthy();
230
+ fireEvent.scroll(scroller, { target: { scrollTop: offset } });
231
+ await waitFor(() => expect(list.getAbsoluteLastScrollOffset()).toBe(offset));
232
+ transport.mockClear();
233
+ const revealScroll = vi.spyOn(list, "scrollToOffset");
234
+ tree.rerender(
235
+ <TreeView
236
+ nodes={nodes}
237
+ height={240}
238
+ onRevealComplete={complete}
239
+ revealRequest={{ id: `row-${target}`, requestId: 72 }}
240
+ />,
241
+ );
242
+ await waitFor(() =>
243
+ expect(complete).toHaveBeenCalledExactlyOnceWith({ id: `row-${target}`, requestId: 72 }),
244
+ );
245
+ expect(revealScroll).not.toHaveBeenCalled();
246
+ });