@ankhorage/zora 25.0.0 → 25.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/CHANGELOG.md +12 -0
- package/README.md +2 -2
- package/dist/features/explorer/adapters/inbound/Explorer.d.ts.map +1 -1
- package/dist/features/explorer/adapters/inbound/Explorer.js +30 -13
- package/dist/features/explorer/adapters/inbound/Explorer.js.map +1 -1
- package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.d.ts +9 -0
- package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.d.ts.map +1 -0
- package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.js +5 -0
- package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.js.map +1 -0
- package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.native.d.ts +9 -0
- package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.native.d.ts.map +1 -0
- package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.native.js +5 -0
- package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.native.js.map +1 -0
- package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.web.d.ts +9 -0
- package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.web.d.ts.map +1 -0
- package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.web.js +10 -0
- package/dist/features/explorer/adapters/inbound/ExplorerKeyboardProxy.web.js.map +1 -0
- package/dist/features/explorer/adapters/inbound/useExplorerKeyboardFocus.d.ts +14 -0
- package/dist/features/explorer/adapters/inbound/useExplorerKeyboardFocus.d.ts.map +1 -0
- package/dist/features/explorer/adapters/inbound/useExplorerKeyboardFocus.js +54 -0
- package/dist/features/explorer/adapters/inbound/useExplorerKeyboardFocus.js.map +1 -0
- package/dist/features/explorer/application/getExplorerNextFocusId.d.ts +6 -0
- package/dist/features/explorer/application/getExplorerNextFocusId.d.ts.map +1 -0
- package/dist/features/explorer/application/getExplorerNextFocusId.js +53 -0
- package/dist/features/explorer/application/getExplorerNextFocusId.js.map +1 -0
- package/dist/features/explorer/public.d.ts +1 -0
- package/dist/features/explorer/public.d.ts.map +1 -1
- package/dist/features/explorer/public.js +1 -0
- package/dist/features/explorer/public.js.map +1 -1
- package/dist/features/grid-view/adapters/inbound/GridView.d.ts +1 -1
- package/dist/features/grid-view/adapters/inbound/GridView.d.ts.map +1 -1
- package/dist/features/grid-view/adapters/inbound/GridView.js +36 -4
- package/dist/features/grid-view/adapters/inbound/GridView.js.map +1 -1
- package/dist/features/grid-view/adapters/inbound/TileGrid.d.ts +1 -1
- package/dist/features/grid-view/adapters/inbound/TileGrid.d.ts.map +1 -1
- package/dist/features/grid-view/adapters/inbound/TileGrid.js +7 -3
- package/dist/features/grid-view/adapters/inbound/TileGrid.js.map +1 -1
- package/dist/types/grid-view.d.ts +10 -0
- package/dist/types/grid-view.d.ts.map +1 -1
- package/dist/types/grid-view.js.map +1 -1
- package/package.json +3 -3
- package/src/features/explorer/adapters/inbound/Explorer.tsx +79 -43
- package/src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.native.tsx +11 -0
- package/src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.tsx +11 -0
- package/src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.web.tsx +20 -0
- package/src/features/explorer/adapters/inbound/useExplorerKeyboardFocus.test.tsx +232 -0
- package/src/features/explorer/adapters/inbound/useExplorerKeyboardFocus.ts +77 -0
- package/src/features/explorer/application/getExplorerNextFocusId.test.ts +37 -0
- package/src/features/explorer/application/getExplorerNextFocusId.ts +61 -0
- package/src/features/explorer/public.ts +1 -0
- package/src/features/grid-view/adapters/inbound/GridView.tsx +49 -1
- package/src/features/grid-view/adapters/inbound/TileGrid.tsx +11 -0
- package/src/types/grid-view.ts +10 -0
- package/web-dist/chunks/{Accordion-8m8p9wpw.js → Accordion-54k0ncs3.js} +83 -0
- package/web-dist/chunks/Accordion-fmn31fgf.js +321 -0
- package/web-dist/components/explorer/Explorer.js +2 -2
- package/web-dist/components/file-explorer/FileExplorer.js +2 -2
- package/web-dist/components/grid-view/GridView.js +1 -1
- package/web-dist/components/media-explorer/MediaExplorer.js +2 -2
- package/web-dist/components/tile-grid/TileGrid.js +1 -1
- package/web-dist/components/uploader/Uploader.js +2 -2
- package/web-dist/manifest.json +10 -10
- package/web-dist/chunks/Accordion-2gvs3frs.js +0 -197
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { getExplorerNextFocusId } from './getExplorerNextFocusId';
|
|
4
|
+
|
|
5
|
+
describe('Explorer virtualized keyboard focus', () => {
|
|
6
|
+
const ids = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
|
|
7
|
+
|
|
8
|
+
test('moves by tile and by responsive row while staying within the catalogue', () => {
|
|
9
|
+
expect(getExplorerNextFocusId(ids, 'b', 'ArrowRight', 3)).toBe('c');
|
|
10
|
+
expect(getExplorerNextFocusId(ids, 'b', 'ArrowDown', 3)).toBe('e');
|
|
11
|
+
expect(getExplorerNextFocusId(ids, 'b', 'ArrowUp', 3)).toBe('a');
|
|
12
|
+
expect(getExplorerNextFocusId(ids, 'g', 'ArrowDown', 3)).toBe('g');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('uses full visual positions and skips disabled items without collapsing rows', () => {
|
|
16
|
+
const disabled = new Set(['d', 'a']);
|
|
17
|
+
expect(getExplorerNextFocusId(ids, 'b', 'ArrowDown', 3, disabled)).toBe('e');
|
|
18
|
+
expect(getExplorerNextFocusId(ids, 'b', 'Home', 3, disabled)).toBe('b');
|
|
19
|
+
expect(getExplorerNextFocusId(ids, 'f', 'ArrowLeft', 3, new Set(['e']))).toBe('d');
|
|
20
|
+
expect(getExplorerNextFocusId(ids, 'g', 'End', 3, new Set(['g']))).toBe('f');
|
|
21
|
+
expect(getExplorerNextFocusId(ids, 'b', 'Home', 3, new Set(ids))).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('navigates across ten thousand virtual positions without materializing DOM cells', () => {
|
|
25
|
+
const large = Array.from({ length: 10000 }, (_, index) => `tile-${index}`);
|
|
26
|
+
expect(getExplorerNextFocusId(large, 'tile-0', 'End', 4)).toBe('tile-9999');
|
|
27
|
+
expect(getExplorerNextFocusId(large, 'tile-9999', 'Home', 4)).toBe('tile-0');
|
|
28
|
+
expect(getExplorerNextFocusId(large, 'tile-2', 'ArrowDown', 4)).toBe('tile-6');
|
|
29
|
+
expect(getExplorerNextFocusId(large, 'tile-2', 'ArrowDown', 2)).toBe('tile-4');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('supports first and last collection navigation without creating placeholder cells', () => {
|
|
33
|
+
expect(getExplorerNextFocusId(ids, 'd', 'Home', 3)).toBe('a');
|
|
34
|
+
expect(getExplorerNextFocusId(ids, 'd', 'End', 3)).toBe('g');
|
|
35
|
+
expect(getExplorerNextFocusId(ids, 'd', 'Enter', 3)).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/***
|
|
2
|
+
* Resolve focus along actual tile positions while skipping unavailable items
|
|
3
|
+
* without collapsing the visual row geometry.
|
|
4
|
+
*/
|
|
5
|
+
export function getExplorerNextFocusId(
|
|
6
|
+
ids: readonly string[],
|
|
7
|
+
currentId: string | null,
|
|
8
|
+
key: string,
|
|
9
|
+
columns: number,
|
|
10
|
+
disabledIds: ReadonlySet<string> = new Set(),
|
|
11
|
+
): string | null {
|
|
12
|
+
const delta = resolveFocusDelta(key, columns);
|
|
13
|
+
if (delta === null || ids.length === 0) return null;
|
|
14
|
+
|
|
15
|
+
const firstIndex = ids.findIndex((id) => !disabledIds.has(id));
|
|
16
|
+
if (firstIndex < 0) return null;
|
|
17
|
+
|
|
18
|
+
const currentIndex = Math.max(firstIndex, ids.indexOf(currentId ?? ''));
|
|
19
|
+
const candidateIndex =
|
|
20
|
+
delta === Number.NEGATIVE_INFINITY
|
|
21
|
+
? 0
|
|
22
|
+
: delta === Number.POSITIVE_INFINITY
|
|
23
|
+
? ids.length - 1
|
|
24
|
+
: Math.max(0, Math.min(ids.length - 1, currentIndex + delta));
|
|
25
|
+
const direction =
|
|
26
|
+
delta === Number.NEGATIVE_INFINITY
|
|
27
|
+
? 1
|
|
28
|
+
: delta === Number.POSITIVE_INFINITY
|
|
29
|
+
? -1
|
|
30
|
+
: Math.sign(delta);
|
|
31
|
+
|
|
32
|
+
for (let index = candidateIndex; index >= 0 && index < ids.length; index += direction) {
|
|
33
|
+
const candidate = ids.at(index);
|
|
34
|
+
if (candidate !== undefined && !disabledIds.has(candidate)) return candidate;
|
|
35
|
+
}
|
|
36
|
+
const current = ids.at(currentIndex);
|
|
37
|
+
return current !== undefined && !disabledIds.has(current)
|
|
38
|
+
? current
|
|
39
|
+
: (ids.at(firstIndex) ?? null);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/*** Translate navigation keys into collection-relative movements. */
|
|
43
|
+
function resolveFocusDelta(key: string, columns: number): number | null {
|
|
44
|
+
const rowSize = Math.max(1, Math.floor(columns));
|
|
45
|
+
switch (key) {
|
|
46
|
+
case 'ArrowLeft':
|
|
47
|
+
return -1;
|
|
48
|
+
case 'ArrowRight':
|
|
49
|
+
return 1;
|
|
50
|
+
case 'ArrowUp':
|
|
51
|
+
return -rowSize;
|
|
52
|
+
case 'ArrowDown':
|
|
53
|
+
return rowSize;
|
|
54
|
+
case 'Home':
|
|
55
|
+
return Number.NEGATIVE_INFINITY;
|
|
56
|
+
case 'End':
|
|
57
|
+
return Number.POSITIVE_INFINITY;
|
|
58
|
+
default:
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -9,4 +9,5 @@ export type {
|
|
|
9
9
|
export { Explorer } from './adapters/inbound/Explorer';
|
|
10
10
|
export { FileExplorer } from './adapters/inbound/FileExplorer';
|
|
11
11
|
export { MediaExplorer } from './adapters/inbound/MediaExplorer';
|
|
12
|
+
export { getExplorerNextFocusId } from './application/getExplorerNextFocusId';
|
|
12
13
|
export { resolveExplorerSelection } from './application/resolveExplorerSelection';
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getVisibleGridItems,
|
|
3
|
+
type GridViewport,
|
|
4
|
+
revealWorldRect,
|
|
5
|
+
worldToViewport,
|
|
6
|
+
} from '@ankhorage/grid-view';
|
|
2
7
|
import React from 'react';
|
|
3
8
|
import { ScrollView as NativeScrollView, View as NativeView } from 'react-native';
|
|
4
9
|
|
|
@@ -18,13 +23,18 @@ export function GridView({
|
|
|
18
23
|
height,
|
|
19
24
|
zoom = 1,
|
|
20
25
|
overscanPixels = 160,
|
|
26
|
+
focusedItemId,
|
|
27
|
+
revealPaddingPixels = 8,
|
|
21
28
|
interactionPolicy,
|
|
22
29
|
onViewportChange,
|
|
30
|
+
onVisibleItemIdsChange,
|
|
23
31
|
renderItem,
|
|
24
32
|
testID,
|
|
25
33
|
}: GridViewProps) {
|
|
26
34
|
const [scrollX, setScrollX] = React.useState(0);
|
|
27
35
|
const [scrollY, setScrollY] = React.useState(0);
|
|
36
|
+
const horizontalScrollRef = React.useRef<NativeScrollView>(null);
|
|
37
|
+
const verticalScrollRef = React.useRef<NativeScrollView>(null);
|
|
28
38
|
const scale = Math.max(0.01, zoom);
|
|
29
39
|
const viewport = React.useMemo<GridViewport>(
|
|
30
40
|
() => ({
|
|
@@ -41,13 +51,50 @@ export function GridView({
|
|
|
41
51
|
() => getVisibleGridItems(items, viewport, overscanPixels),
|
|
42
52
|
[items, overscanPixels, viewport],
|
|
43
53
|
);
|
|
54
|
+
const viewportVisibleIds = React.useMemo(
|
|
55
|
+
() => getVisibleGridItems(visibleItems, viewport).map((item) => item.id),
|
|
56
|
+
[viewport, visibleItems],
|
|
57
|
+
);
|
|
58
|
+
const previousVisibleIdsRef = React.useRef<readonly string[]>([]);
|
|
59
|
+
|
|
60
|
+
React.useEffect(() => {
|
|
61
|
+
if (!onVisibleItemIdsChange) return;
|
|
62
|
+
const previous = previousVisibleIdsRef.current;
|
|
63
|
+
if (
|
|
64
|
+
previous.length === viewportVisibleIds.length &&
|
|
65
|
+
previous.every((id, index) => id === viewportVisibleIds.at(index))
|
|
66
|
+
) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
previousVisibleIdsRef.current = viewportVisibleIds;
|
|
70
|
+
onVisibleItemIdsChange(viewportVisibleIds);
|
|
71
|
+
}, [onVisibleItemIdsChange, viewportVisibleIds]);
|
|
44
72
|
|
|
45
73
|
React.useEffect(() => {
|
|
46
74
|
onViewportChange?.(viewport);
|
|
47
75
|
}, [onViewportChange, viewport]);
|
|
48
76
|
|
|
77
|
+
const viewportRef = React.useRef(viewport);
|
|
78
|
+
React.useEffect(() => {
|
|
79
|
+
viewportRef.current = viewport;
|
|
80
|
+
}, [viewport]);
|
|
81
|
+
|
|
82
|
+
React.useEffect(() => {
|
|
83
|
+
const currentViewport = viewportRef.current;
|
|
84
|
+
const focusedItem = items.find((item) => item.id === focusedItemId);
|
|
85
|
+
if (!focusedItem) return;
|
|
86
|
+
const revealed = revealWorldRect(currentViewport, focusedItem, revealPaddingPixels);
|
|
87
|
+
if (revealed.offsetX !== currentViewport.offsetX) {
|
|
88
|
+
horizontalScrollRef.current?.scrollTo({ x: revealed.offsetX * scale, animated: true });
|
|
89
|
+
}
|
|
90
|
+
if (revealed.offsetY !== currentViewport.offsetY) {
|
|
91
|
+
verticalScrollRef.current?.scrollTo({ y: revealed.offsetY * scale, animated: true });
|
|
92
|
+
}
|
|
93
|
+
}, [focusedItemId, height, items, revealPaddingPixels, scale, width]);
|
|
94
|
+
|
|
49
95
|
return (
|
|
50
96
|
<NativeScrollView
|
|
97
|
+
ref={horizontalScrollRef}
|
|
51
98
|
horizontal
|
|
52
99
|
scrollEnabled={interactionPolicy !== 'passive'}
|
|
53
100
|
scrollEventThrottle={32}
|
|
@@ -57,6 +104,7 @@ export function GridView({
|
|
|
57
104
|
onScroll={(event) => setScrollX(event.nativeEvent.contentOffset.x)}
|
|
58
105
|
>
|
|
59
106
|
<NativeScrollView
|
|
107
|
+
ref={verticalScrollRef}
|
|
60
108
|
scrollEnabled={interactionPolicy !== 'passive'}
|
|
61
109
|
scrollEventThrottle={32}
|
|
62
110
|
showsVerticalScrollIndicator
|
|
@@ -14,6 +14,10 @@ export function TileGrid({
|
|
|
14
14
|
gap = 12,
|
|
15
15
|
zoom = 1,
|
|
16
16
|
overscanPixels,
|
|
17
|
+
focusedItemId,
|
|
18
|
+
revealPaddingPixels,
|
|
19
|
+
onColumnsChange,
|
|
20
|
+
onVisibleItemIdsChange,
|
|
17
21
|
renderItem,
|
|
18
22
|
interactionPolicy,
|
|
19
23
|
testID,
|
|
@@ -26,6 +30,10 @@ export function TileGrid({
|
|
|
26
30
|
);
|
|
27
31
|
const itemLookup = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
|
|
28
32
|
|
|
33
|
+
React.useEffect(() => {
|
|
34
|
+
if (resolvedWidth > 0) onColumnsChange?.(layout.columns);
|
|
35
|
+
}, [layout.columns, onColumnsChange, resolvedWidth]);
|
|
36
|
+
|
|
29
37
|
return (
|
|
30
38
|
<NativeView
|
|
31
39
|
onLayout={(event) => setMeasuredWidth(event.nativeEvent.layout.width)}
|
|
@@ -37,13 +45,16 @@ export function TileGrid({
|
|
|
37
45
|
contentHeight={layout.height}
|
|
38
46
|
contentWidth={Math.max(resolvedWidth / zoom, layout.width)}
|
|
39
47
|
height={height}
|
|
48
|
+
focusedItemId={focusedItemId}
|
|
40
49
|
interactionPolicy={interactionPolicy}
|
|
41
50
|
items={layout.items}
|
|
51
|
+
onVisibleItemIdsChange={onVisibleItemIdsChange}
|
|
42
52
|
overscanPixels={overscanPixels}
|
|
43
53
|
renderItem={(item) => {
|
|
44
54
|
const source = itemLookup.get(item.id);
|
|
45
55
|
return source ? renderItem(source) : null;
|
|
46
56
|
}}
|
|
57
|
+
revealPaddingPixels={revealPaddingPixels}
|
|
47
58
|
width={resolvedWidth}
|
|
48
59
|
zoom={zoom}
|
|
49
60
|
/>
|
package/src/types/grid-view.ts
CHANGED
|
@@ -12,7 +12,12 @@ export interface GridViewProps extends ZoraBaseProps {
|
|
|
12
12
|
height: number;
|
|
13
13
|
zoom?: number;
|
|
14
14
|
overscanPixels?: number;
|
|
15
|
+
/** Stable item identity that must be scrolled into view without duplicating viewport geometry. */
|
|
16
|
+
focusedItemId?: string;
|
|
17
|
+
/** Pixel inset retained around a revealed focused item. */
|
|
18
|
+
revealPaddingPixels?: number;
|
|
15
19
|
onViewportChange?: (viewport: GridViewport) => void;
|
|
20
|
+
onVisibleItemIdsChange?: (ids: readonly string[]) => void;
|
|
16
21
|
renderItem: (item: GridRectItem) => React.ReactNode;
|
|
17
22
|
}
|
|
18
23
|
|
|
@@ -30,5 +35,10 @@ export interface TileGridProps extends ZoraBaseProps {
|
|
|
30
35
|
gap?: number;
|
|
31
36
|
zoom?: number;
|
|
32
37
|
overscanPixels?: number;
|
|
38
|
+
focusedItemId?: string;
|
|
39
|
+
revealPaddingPixels?: number;
|
|
40
|
+
/** Reports the same measured column count used to position tiles. */
|
|
41
|
+
onColumnsChange?: (columns: number) => void;
|
|
42
|
+
onVisibleItemIdsChange?: (ids: readonly string[]) => void;
|
|
33
43
|
renderItem: (item: TileGridItem) => React.ReactNode;
|
|
34
44
|
}
|
|
@@ -36,6 +36,42 @@ function getVisibleGridItems(items, viewport, overscanPixels = 0) {
|
|
|
36
36
|
const bounds = getVisibleWorldBounds(viewport, overscanPixels);
|
|
37
37
|
return items.filter((item) => item.x <= bounds.x + bounds.width && item.x + item.width >= bounds.x && item.y <= bounds.y + bounds.height && item.y + item.height >= bounds.y);
|
|
38
38
|
}
|
|
39
|
+
// node_modules/@ankhorage/grid-view/dist/features/viewport/revealWorldRect.js
|
|
40
|
+
function revealWorldRect(viewport, rect, paddingPixels = 0) {
|
|
41
|
+
validateRevealInput(viewport, rect, paddingPixels);
|
|
42
|
+
return {
|
|
43
|
+
...viewport,
|
|
44
|
+
offsetX: revealAxis(viewport.offsetX, viewport.width, viewport.pixelsPerUnitX, rect.x, rect.width, paddingPixels),
|
|
45
|
+
offsetY: revealAxis(viewport.offsetY, viewport.height, viewport.pixelsPerUnitY, rect.y, rect.height, paddingPixels)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function revealAxis(offset, viewportPixels, pixelsPerUnit, rectStart, rectSize, paddingPixels) {
|
|
49
|
+
const visibleStart = offset + paddingPixels / pixelsPerUnit;
|
|
50
|
+
const visibleEnd = offset + (viewportPixels - paddingPixels) / pixelsPerUnit;
|
|
51
|
+
const rectEnd = rectStart + rectSize;
|
|
52
|
+
if (rectSize > visibleEnd - visibleStart || rectStart < visibleStart) {
|
|
53
|
+
return rectStart - paddingPixels / pixelsPerUnit;
|
|
54
|
+
}
|
|
55
|
+
if (rectEnd > visibleEnd) {
|
|
56
|
+
return rectEnd - (viewportPixels - paddingPixels) / pixelsPerUnit;
|
|
57
|
+
}
|
|
58
|
+
return offset;
|
|
59
|
+
}
|
|
60
|
+
function validateRevealInput(viewport, rect, paddingPixels) {
|
|
61
|
+
if (![
|
|
62
|
+
viewport.width,
|
|
63
|
+
viewport.height,
|
|
64
|
+
viewport.offsetX,
|
|
65
|
+
viewport.offsetY,
|
|
66
|
+
viewport.pixelsPerUnitX,
|
|
67
|
+
viewport.pixelsPerUnitY
|
|
68
|
+
].every(Number.isFinite) || viewport.width <= 0 || viewport.height <= 0 || viewport.pixelsPerUnitX <= 0 || viewport.pixelsPerUnitY <= 0 || !Number.isFinite(paddingPixels) || paddingPixels < 0 || paddingPixels * 2 >= viewport.width || paddingPixels * 2 >= viewport.height) {
|
|
69
|
+
throw new RangeError("Viewport geometry must be finite and padding must leave visible space.");
|
|
70
|
+
}
|
|
71
|
+
if (![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite) || rect.width < 0 || rect.height < 0) {
|
|
72
|
+
throw new RangeError("World rectangles must have finite nonnegative dimensions.");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
39
75
|
// node_modules/@ankhorage/grid-view/dist/features/viewport/worldToViewport.js
|
|
40
76
|
function worldToViewport(point, viewport) {
|
|
41
77
|
return {
|
|
@@ -55,13 +91,18 @@ function GridView2({
|
|
|
55
91
|
height,
|
|
56
92
|
zoom = 1,
|
|
57
93
|
overscanPixels = 160,
|
|
94
|
+
focusedItemId,
|
|
95
|
+
revealPaddingPixels = 8,
|
|
58
96
|
interactionPolicy,
|
|
59
97
|
onViewportChange,
|
|
98
|
+
onVisibleItemIdsChange,
|
|
60
99
|
renderItem,
|
|
61
100
|
testID
|
|
62
101
|
}) {
|
|
63
102
|
const [scrollX, setScrollX] = React.useState(0);
|
|
64
103
|
const [scrollY, setScrollY] = React.useState(0);
|
|
104
|
+
const horizontalScrollRef = React.useRef(null);
|
|
105
|
+
const verticalScrollRef = React.useRef(null);
|
|
65
106
|
const scale = Math.max(0.01, zoom);
|
|
66
107
|
const viewport = React.useMemo(() => ({
|
|
67
108
|
width,
|
|
@@ -72,10 +113,40 @@ function GridView2({
|
|
|
72
113
|
pixelsPerUnitY: scale
|
|
73
114
|
}), [height, scale, scrollX, scrollY, width]);
|
|
74
115
|
const visibleItems = React.useMemo(() => getVisibleGridItems(items, viewport, overscanPixels), [items, overscanPixels, viewport]);
|
|
116
|
+
const viewportVisibleIds = React.useMemo(() => getVisibleGridItems(visibleItems, viewport).map((item) => item.id), [viewport, visibleItems]);
|
|
117
|
+
const previousVisibleIdsRef = React.useRef([]);
|
|
118
|
+
React.useEffect(() => {
|
|
119
|
+
if (!onVisibleItemIdsChange)
|
|
120
|
+
return;
|
|
121
|
+
const previous = previousVisibleIdsRef.current;
|
|
122
|
+
if (previous.length === viewportVisibleIds.length && previous.every((id, index) => id === viewportVisibleIds.at(index))) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
previousVisibleIdsRef.current = viewportVisibleIds;
|
|
126
|
+
onVisibleItemIdsChange(viewportVisibleIds);
|
|
127
|
+
}, [onVisibleItemIdsChange, viewportVisibleIds]);
|
|
75
128
|
React.useEffect(() => {
|
|
76
129
|
onViewportChange?.(viewport);
|
|
77
130
|
}, [onViewportChange, viewport]);
|
|
131
|
+
const viewportRef = React.useRef(viewport);
|
|
132
|
+
React.useEffect(() => {
|
|
133
|
+
viewportRef.current = viewport;
|
|
134
|
+
}, [viewport]);
|
|
135
|
+
React.useEffect(() => {
|
|
136
|
+
const currentViewport = viewportRef.current;
|
|
137
|
+
const focusedItem = items.find((item) => item.id === focusedItemId);
|
|
138
|
+
if (!focusedItem)
|
|
139
|
+
return;
|
|
140
|
+
const revealed = revealWorldRect(currentViewport, focusedItem, revealPaddingPixels);
|
|
141
|
+
if (revealed.offsetX !== currentViewport.offsetX) {
|
|
142
|
+
horizontalScrollRef.current?.scrollTo({ x: revealed.offsetX * scale, animated: true });
|
|
143
|
+
}
|
|
144
|
+
if (revealed.offsetY !== currentViewport.offsetY) {
|
|
145
|
+
verticalScrollRef.current?.scrollTo({ y: revealed.offsetY * scale, animated: true });
|
|
146
|
+
}
|
|
147
|
+
}, [focusedItemId, height, items, revealPaddingPixels, scale, width]);
|
|
78
148
|
return /* @__PURE__ */ jsx(import_react_native.ScrollView, {
|
|
149
|
+
ref: horizontalScrollRef,
|
|
79
150
|
horizontal: true,
|
|
80
151
|
scrollEnabled: interactionPolicy !== "passive",
|
|
81
152
|
scrollEventThrottle: 32,
|
|
@@ -84,6 +155,7 @@ function GridView2({
|
|
|
84
155
|
testID,
|
|
85
156
|
onScroll: (event) => setScrollX(event.nativeEvent.contentOffset.x),
|
|
86
157
|
children: /* @__PURE__ */ jsx(import_react_native.ScrollView, {
|
|
158
|
+
ref: verticalScrollRef,
|
|
87
159
|
scrollEnabled: interactionPolicy !== "passive",
|
|
88
160
|
scrollEventThrottle: 32,
|
|
89
161
|
showsVerticalScrollIndicator: true,
|
|
@@ -148,6 +220,10 @@ function TileGrid2({
|
|
|
148
220
|
gap = 12,
|
|
149
221
|
zoom = 1,
|
|
150
222
|
overscanPixels,
|
|
223
|
+
focusedItemId,
|
|
224
|
+
revealPaddingPixels,
|
|
225
|
+
onColumnsChange,
|
|
226
|
+
onVisibleItemIdsChange,
|
|
151
227
|
renderItem,
|
|
152
228
|
interactionPolicy,
|
|
153
229
|
testID
|
|
@@ -156,6 +232,10 @@ function TileGrid2({
|
|
|
156
232
|
const resolvedWidth = width ?? measuredWidth;
|
|
157
233
|
const layout = React2.useMemo(() => layoutTileGrid(items, resolvedWidth, tileSize, gap, zoom), [items, resolvedWidth, tileSize, gap, zoom]);
|
|
158
234
|
const itemLookup = React2.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
|
|
235
|
+
React2.useEffect(() => {
|
|
236
|
+
if (resolvedWidth > 0)
|
|
237
|
+
onColumnsChange?.(layout.columns);
|
|
238
|
+
}, [layout.columns, onColumnsChange, resolvedWidth]);
|
|
159
239
|
return /* @__PURE__ */ jsx2(import_react_native2.View, {
|
|
160
240
|
onLayout: (event) => setMeasuredWidth(event.nativeEvent.layout.width),
|
|
161
241
|
style: { width, height, overflow: "hidden" },
|
|
@@ -164,13 +244,16 @@ function TileGrid2({
|
|
|
164
244
|
contentHeight: layout.height,
|
|
165
245
|
contentWidth: Math.max(resolvedWidth / zoom, layout.width),
|
|
166
246
|
height,
|
|
247
|
+
focusedItemId,
|
|
167
248
|
interactionPolicy,
|
|
168
249
|
items: layout.items,
|
|
250
|
+
onVisibleItemIdsChange,
|
|
169
251
|
overscanPixels,
|
|
170
252
|
renderItem: (item) => {
|
|
171
253
|
const source = itemLookup.get(item.id);
|
|
172
254
|
return source ? renderItem(source) : null;
|
|
173
255
|
},
|
|
256
|
+
revealPaddingPixels,
|
|
174
257
|
width: resolvedWidth,
|
|
175
258
|
zoom
|
|
176
259
|
}) : null
|