@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 25.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 52f40c6: Add virtualized Explorer keyboard focus navigation with published GridView reveal geometry and a runnable native/web workspace example.
|
|
8
|
+
|
|
9
|
+
## 25.0.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- e7f2725: Update dependencies: `@ankhorage/grid-view`, `@ankhorage/runtime`.
|
|
14
|
+
|
|
3
15
|
## 25.0.0
|
|
4
16
|
|
|
5
17
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
# @ankhorage/zora
|
|
5
5
|
|
|
6
|
-
         
|
|
7
7
|
|
|
8
8
|
Opinionated React Native and React Native Web UI kit built on @ankhorage/surface.
|
|
9
9
|
|
|
@@ -52,7 +52,7 @@ export default function BasicApp() {
|
|
|
52
52
|
}
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
This package contains
|
|
55
|
+
This package contains 6 additional examples. See the generated documentation for the complete set.
|
|
56
56
|
|
|
57
57
|
## Generated documentation
|
|
58
58
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Explorer.d.ts","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/Explorer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,KAAK,EAAgB,aAAa,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"Explorer.d.ts","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/Explorer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,KAAK,EAAgB,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAW9E,2FAA2F;AAC3F,eAAO,MAAM,QAAQ,qDAAoC,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Pressable as NativePressable, View as NativeView } from 'react-native';
|
|
2
|
+
import { Platform, Pressable as NativePressable, View as NativeView } from 'react-native';
|
|
3
3
|
import { TileGrid } from '../../../grid-view/public';
|
|
4
4
|
import { Icon } from '../../../icon/public';
|
|
5
5
|
import { Image } from '../../../image/public';
|
|
@@ -7,6 +7,8 @@ import { withZoraThemeScope } from '../../../theme/adapters/inbound/withZoraThem
|
|
|
7
7
|
import { useZoraTheme } from '../../../theme/composition/useZoraTheme';
|
|
8
8
|
import { Text } from '../../../typography/public';
|
|
9
9
|
import { resolveExplorerSelection } from '../../application/resolveExplorerSelection';
|
|
10
|
+
import { ExplorerKeyboardProxy } from './ExplorerKeyboardProxy';
|
|
11
|
+
import { useExplorerKeyboardFocus } from './useExplorerKeyboardFocus';
|
|
10
12
|
/*** Single canonical Explorer implementation for media and file catalogue presentation. */
|
|
11
13
|
export const Explorer = withZoraThemeScope(ExplorerInner);
|
|
12
14
|
/*** Presents provider-neutral asset tiles with controlled or uncontrolled selection. */
|
|
@@ -14,9 +16,11 @@ function ExplorerInner({ items, selectedIds, defaultSelectedIds = [], selectionM
|
|
|
14
16
|
const { theme } = useZoraTheme();
|
|
15
17
|
const [internalSelectedIds, setInternalSelectedIds] = React.useState(defaultSelectedIds);
|
|
16
18
|
const anchorId = React.useRef(null);
|
|
19
|
+
const [columns, setColumns] = React.useState(1);
|
|
20
|
+
const [visibleIds, setVisibleIds] = React.useState([]);
|
|
17
21
|
const effectiveSelectedIds = selectedIds ?? internalSelectedIds;
|
|
18
22
|
const itemLookup = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
|
|
19
|
-
const ids = items.filter((item) => !item.disabled).map((item) => item.id);
|
|
23
|
+
const ids = React.useMemo(() => items.filter((item) => !item.disabled).map((item) => item.id), [items]);
|
|
20
24
|
const selected = new Set(effectiveSelectedIds);
|
|
21
25
|
const passive = interactionPolicy === 'passive' || disabled || readOnly;
|
|
22
26
|
const select = (item, intent) => {
|
|
@@ -29,13 +33,22 @@ function ExplorerInner({ items, selectedIds, defaultSelectedIds = [], selectionM
|
|
|
29
33
|
setInternalSelectedIds(next);
|
|
30
34
|
onSelectionChange?.({ selectedIds: next });
|
|
31
35
|
};
|
|
36
|
+
const keyboard = useExplorerKeyboardFocus(items, columns, passive, visibleIds, (targetId, originId, shiftKey) => {
|
|
37
|
+
if (!shiftKey)
|
|
38
|
+
return;
|
|
39
|
+
anchorId.current ??= originId;
|
|
40
|
+
const nextItem = itemLookup.get(targetId);
|
|
41
|
+
if (nextItem)
|
|
42
|
+
select(nextItem, 'range');
|
|
43
|
+
});
|
|
32
44
|
const renderTile = (tile) => {
|
|
33
45
|
const item = itemLookup.get(tile.id);
|
|
34
46
|
if (!item)
|
|
35
47
|
return null;
|
|
36
48
|
const isSelected = selected.has(item.id);
|
|
37
49
|
const canInteract = !passive && !item.disabled;
|
|
38
|
-
return (<
|
|
50
|
+
return (<ExplorerKeyboardProxy onKeyDown={(key, shiftKey) => keyboard.onKeyDown(item.id, key, shiftKey)}>
|
|
51
|
+
<NativePressable accessibilityLabel={item.name} accessibilityRole="button" accessibilityState={{ selected: isSelected, disabled: !canInteract }} disabled={!canInteract} focusable={canInteract && (Platform.OS !== 'web' || item.id === keyboard.tabStopId)} onFocus={() => keyboard.onFocus(item.id)} onLongPress={() => select(item, 'toggle')} onPress={(event) => {
|
|
39
52
|
const intent = resolveExplorerPressIntent(event.nativeEvent);
|
|
40
53
|
select(item, intent);
|
|
41
54
|
if (selectionMode === 'single' && intent === 'replace') {
|
|
@@ -50,14 +63,15 @@ function ExplorerInner({ items, selectedIds, defaultSelectedIds = [], selectionM
|
|
|
50
63
|
backgroundColor: theme.semantics.neutral.surface,
|
|
51
64
|
padding: 8,
|
|
52
65
|
gap: 4,
|
|
53
|
-
}} testID={`explorer-item-${item.id}`}>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
}} ref={(node) => keyboard.registerTile(item.id, node)} testID={`explorer-item-${item.id}`}>
|
|
67
|
+
<NativeView style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
|
68
|
+
{item.thumbnailUri ? (<Image accessibilityLabel={item.name} aspectRatio={1} source={item.thumbnailUri} width="100%"/>) : (<Icon name={item.kind === 'folder' ? 'folder-outline' : 'document-outline'} size={32}/>)}
|
|
69
|
+
</NativeView>
|
|
70
|
+
<Text numberOfLines={1} variant="caption">
|
|
71
|
+
{item.name}
|
|
72
|
+
</Text>
|
|
73
|
+
</NativePressable>
|
|
74
|
+
</ExplorerKeyboardProxy>);
|
|
61
75
|
};
|
|
62
76
|
if (errorText)
|
|
63
77
|
return <Text testID={testID}>{errorText}</Text>;
|
|
@@ -65,9 +79,12 @@ function ExplorerInner({ items, selectedIds, defaultSelectedIds = [], selectionM
|
|
|
65
79
|
return <Text testID={testID}>Loading…</Text>;
|
|
66
80
|
if (items.length === 0)
|
|
67
81
|
return <Text testID={testID}>{emptyText}</Text>;
|
|
68
|
-
return (<TileGrid height={height} interactionPolicy={interactionPolicy} items={items} renderItem={renderTile} testID={testID} tileSize={tileSize} width={width} zoom={zoom}/>);
|
|
82
|
+
return (<TileGrid focusedItemId={keyboard.focusedId ?? undefined} height={height} interactionPolicy={interactionPolicy} onColumnsChange={setColumns} onVisibleItemIdsChange={setVisibleIds} items={items} renderItem={renderTile} testID={testID} tileSize={tileSize} width={width} zoom={zoom}/>);
|
|
69
83
|
}
|
|
70
|
-
/***
|
|
84
|
+
/***
|
|
85
|
+
* Normalize platform press modifiers into selection intents
|
|
86
|
+
* without passing event objects into the public contracts.
|
|
87
|
+
*/
|
|
71
88
|
function resolveExplorerPressIntent(event) {
|
|
72
89
|
if (typeof event !== 'object' || event === null)
|
|
73
90
|
return 'replace';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Explorer.js","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/Explorer.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react';\nimport { Pressable as NativePressable, View as NativeView } from 'react-native';\n\nimport type { ExplorerItem, ExplorerProps } from '../../../../types/explorer';\nimport { TileGrid } from '../../../grid-view/public';\nimport { Icon } from '../../../icon/public';\nimport { Image } from '../../../image/public';\nimport { withZoraThemeScope } from '../../../theme/adapters/inbound/withZoraThemeScope';\nimport { useZoraTheme } from '../../../theme/composition/useZoraTheme';\nimport { Text } from '../../../typography/public';\nimport { resolveExplorerSelection } from '../../application/resolveExplorerSelection';\n\n/*** Single canonical Explorer implementation for media and file catalogue presentation. */\nexport const Explorer = withZoraThemeScope(ExplorerInner);\n\n/*** Presents provider-neutral asset tiles with controlled or uncontrolled selection. */\nfunction ExplorerInner({\n items,\n selectedIds,\n defaultSelectedIds = [],\n selectionMode = 'single',\n onSelectionChange,\n onActivate,\n width,\n height,\n tileSize = 120,\n zoom = 1,\n loading = false,\n errorText,\n emptyText = 'No items',\n disabled = false,\n readOnly = false,\n interactionPolicy,\n testID,\n}: ExplorerProps) {\n const { theme } = useZoraTheme();\n const [internalSelectedIds, setInternalSelectedIds] =\n React.useState<readonly string[]>(defaultSelectedIds);\n const anchorId = React.useRef<string | null>(null);\n const effectiveSelectedIds = selectedIds ?? internalSelectedIds;\n const itemLookup = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);\n const ids = items.filter((item) => !item.disabled).map((item) => item.id);\n const selected = new Set(effectiveSelectedIds);\n const passive = interactionPolicy === 'passive' || disabled || readOnly;\n\n const select = (item: ExplorerItem, intent: 'replace' | 'toggle' | 'range') => {\n if (passive || item.disabled) return;\n const next = resolveExplorerSelection(\n ids,\n effectiveSelectedIds,\n item.id,\n anchorId.current,\n intent,\n selectionMode,\n );\n if (intent !== 'range') anchorId.current = item.id;\n if (selectedIds === undefined) setInternalSelectedIds(next);\n onSelectionChange?.({ selectedIds: next });\n };\n\n const renderTile = (tile: { readonly id: string }) => {\n const item = itemLookup.get(tile.id);\n if (!item) return null;\n const isSelected = selected.has(item.id);\n const canInteract = !passive && !item.disabled;\n return (\n <NativePressable\n accessibilityLabel={item.name}\n accessibilityRole=\"button\"\n accessibilityState={{ selected: isSelected, disabled: !canInteract }}\n disabled={!canInteract}\n onLongPress={() => select(item, 'toggle')}\n onPress={(event) => {\n const intent = resolveExplorerPressIntent(event.nativeEvent);\n select(item, intent);\n if (selectionMode === 'single' && intent === 'replace') {\n onActivate?.({ id: item.id });\n }\n }}\n style={{\n flex: 1,\n overflow: 'hidden',\n borderRadius: 8,\n borderWidth: isSelected ? 2 : 1,\n borderColor: isSelected ? theme.colors.primary : theme.semantics.neutral.divider,\n backgroundColor: theme.semantics.neutral.surface,\n padding: 8,\n gap: 4,\n }}\n testID={`explorer-item-${item.id}`}\n >\n <NativeView style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>\n {item.thumbnailUri ? (\n <Image\n accessibilityLabel={item.name}\n aspectRatio={1}\n source={item.thumbnailUri}\n width=\"100%\"\n />\n ) : (\n <Icon name={item.kind === 'folder' ? 'folder-outline' : 'document-outline'} size={32} />\n )}\n </NativeView>\n <Text numberOfLines={1} variant=\"caption\">\n {item.name}\n </Text>\n </NativePressable>\n );\n };\n\n if (errorText) return <Text testID={testID}>{errorText}</Text>;\n if (loading) return <Text testID={testID}>Loading…</Text>;\n if (items.length === 0) return <Text testID={testID}>{emptyText}</Text>;\n\n return (\n <TileGrid\n height={height}\n interactionPolicy={interactionPolicy}\n items={items}\n renderItem={renderTile}\n testID={testID}\n tileSize={tileSize}\n width={width}\n zoom={zoom}\n />\n );\n}\n\n/*** Normalize platform press modifiers for multi-selection without passing event objects into contracts. */\nfunction resolveExplorerPressIntent(event: unknown): 'replace' | 'toggle' | 'range' {\n if (typeof event !== 'object' || event === null) return 'replace';\n if ('shiftKey' in event && event.shiftKey === true) return 'range';\n if ('ctrlKey' in event && event.ctrlKey === true) return 'toggle';\n if ('metaKey' in event && event.metaKey === true) return 'toggle';\n return 'replace';\n}\n"]}
|
|
1
|
+
{"version":3,"file":"Explorer.js","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/Explorer.tsx"],"names":[],"mappings":"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","sourcesContent":["import React from 'react';\nimport { Platform, Pressable as NativePressable, View as NativeView } from 'react-native';\n\nimport type { ExplorerItem, ExplorerProps } from '../../../../types/explorer';\nimport { TileGrid } from '../../../grid-view/public';\nimport { Icon } from '../../../icon/public';\nimport { Image } from '../../../image/public';\nimport { withZoraThemeScope } from '../../../theme/adapters/inbound/withZoraThemeScope';\nimport { useZoraTheme } from '../../../theme/composition/useZoraTheme';\nimport { Text } from '../../../typography/public';\nimport { resolveExplorerSelection } from '../../application/resolveExplorerSelection';\nimport { ExplorerKeyboardProxy } from './ExplorerKeyboardProxy';\nimport { useExplorerKeyboardFocus } from './useExplorerKeyboardFocus';\n\n/*** Single canonical Explorer implementation for media and file catalogue presentation. */\nexport const Explorer = withZoraThemeScope(ExplorerInner);\n\n/*** Presents provider-neutral asset tiles with controlled or uncontrolled selection. */\nfunction ExplorerInner({\n items,\n selectedIds,\n defaultSelectedIds = [],\n selectionMode = 'single',\n onSelectionChange,\n onActivate,\n width,\n height,\n tileSize = 120,\n zoom = 1,\n loading = false,\n errorText,\n emptyText = 'No items',\n disabled = false,\n readOnly = false,\n interactionPolicy,\n testID,\n}: ExplorerProps) {\n const { theme } = useZoraTheme();\n const [internalSelectedIds, setInternalSelectedIds] =\n React.useState<readonly string[]>(defaultSelectedIds);\n const anchorId = React.useRef<string | null>(null);\n const [columns, setColumns] = React.useState(1);\n const [visibleIds, setVisibleIds] = React.useState<readonly string[]>([]);\n const effectiveSelectedIds = selectedIds ?? internalSelectedIds;\n const itemLookup = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);\n const ids = React.useMemo(\n () => items.filter((item) => !item.disabled).map((item) => item.id),\n [items],\n );\n const selected = new Set(effectiveSelectedIds);\n const passive = interactionPolicy === 'passive' || disabled || readOnly;\n\n const select = (item: ExplorerItem, intent: 'replace' | 'toggle' | 'range') => {\n if (passive || item.disabled) return;\n const next = resolveExplorerSelection(\n ids,\n effectiveSelectedIds,\n item.id,\n anchorId.current,\n intent,\n selectionMode,\n );\n if (intent !== 'range') anchorId.current = item.id;\n if (selectedIds === undefined) setInternalSelectedIds(next);\n onSelectionChange?.({ selectedIds: next });\n };\n\n const keyboard = useExplorerKeyboardFocus(\n items,\n columns,\n passive,\n visibleIds,\n (targetId, originId, shiftKey) => {\n if (!shiftKey) return;\n anchorId.current ??= originId;\n const nextItem = itemLookup.get(targetId);\n if (nextItem) select(nextItem, 'range');\n },\n );\n\n const renderTile = (tile: { readonly id: string }) => {\n const item = itemLookup.get(tile.id);\n if (!item) return null;\n const isSelected = selected.has(item.id);\n const canInteract = !passive && !item.disabled;\n return (\n <ExplorerKeyboardProxy\n onKeyDown={(key, shiftKey) => keyboard.onKeyDown(item.id, key, shiftKey)}\n >\n <NativePressable\n accessibilityLabel={item.name}\n accessibilityRole=\"button\"\n accessibilityState={{ selected: isSelected, disabled: !canInteract }}\n disabled={!canInteract}\n focusable={canInteract && (Platform.OS !== 'web' || item.id === keyboard.tabStopId)}\n onFocus={() => keyboard.onFocus(item.id)}\n onLongPress={() => select(item, 'toggle')}\n onPress={(event) => {\n const intent = resolveExplorerPressIntent(event.nativeEvent);\n select(item, intent);\n if (selectionMode === 'single' && intent === 'replace') {\n onActivate?.({ id: item.id });\n }\n }}\n style={{\n flex: 1,\n overflow: 'hidden',\n borderRadius: 8,\n borderWidth: isSelected ? 2 : 1,\n borderColor: isSelected ? theme.colors.primary : theme.semantics.neutral.divider,\n backgroundColor: theme.semantics.neutral.surface,\n padding: 8,\n gap: 4,\n }}\n ref={(node) => keyboard.registerTile(item.id, node)}\n testID={`explorer-item-${item.id}`}\n >\n <NativeView style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>\n {item.thumbnailUri ? (\n <Image\n accessibilityLabel={item.name}\n aspectRatio={1}\n source={item.thumbnailUri}\n width=\"100%\"\n />\n ) : (\n <Icon\n name={item.kind === 'folder' ? 'folder-outline' : 'document-outline'}\n size={32}\n />\n )}\n </NativeView>\n <Text numberOfLines={1} variant=\"caption\">\n {item.name}\n </Text>\n </NativePressable>\n </ExplorerKeyboardProxy>\n );\n };\n\n if (errorText) return <Text testID={testID}>{errorText}</Text>;\n if (loading) return <Text testID={testID}>Loading…</Text>;\n if (items.length === 0) return <Text testID={testID}>{emptyText}</Text>;\n\n return (\n <TileGrid\n focusedItemId={keyboard.focusedId ?? undefined}\n height={height}\n interactionPolicy={interactionPolicy}\n onColumnsChange={setColumns}\n onVisibleItemIdsChange={setVisibleIds}\n items={items}\n renderItem={renderTile}\n testID={testID}\n tileSize={tileSize}\n width={width}\n zoom={zoom}\n />\n );\n}\n\n/***\n * Normalize platform press modifiers into selection intents\n * without passing event objects into the public contracts.\n */\nfunction resolveExplorerPressIntent(event: unknown): 'replace' | 'toggle' | 'range' {\n if (typeof event !== 'object' || event === null) return 'replace';\n if ('shiftKey' in event && event.shiftKey === true) return 'range';\n if ('ctrlKey' in event && event.ctrlKey === true) return 'toggle';\n if ('metaKey' in event && event.metaKey === true) return 'toggle';\n return 'replace';\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
/*** Provides the native fallback type surface; RNW resolves the web adapter at runtime. */
|
|
3
|
+
export declare function ExplorerKeyboardProxy({ children }: ExplorerKeyboardProxyProps): React.JSX.Element;
|
|
4
|
+
interface ExplorerKeyboardProxyProps {
|
|
5
|
+
readonly children: React.ReactNode;
|
|
6
|
+
readonly onKeyDown: (key: string, shiftKey: boolean) => boolean;
|
|
7
|
+
}
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=ExplorerKeyboardProxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExplorerKeyboardProxy.d.ts","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,2FAA2F;AAC3F,wBAAgB,qBAAqB,CAAC,EAAE,QAAQ,EAAE,EAAE,0BAA0B,qBAE7E;AAED,UAAU,0BAA0B;IAClC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IACnC,QAAQ,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC;CACjE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExplorerKeyboardProxy.js","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.tsx"],"names":[],"mappings":"AAEA,2FAA2F;AAC3F,MAAM,UAAU,qBAAqB,CAAC,EAAE,QAAQ,EAA8B;IAC5E,OAAO,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC;AACzB,CAAC","sourcesContent":["import type React from 'react';\n\n/*** Provides the native fallback type surface; RNW resolves the web adapter at runtime. */\nexport function ExplorerKeyboardProxy({ children }: ExplorerKeyboardProxyProps) {\n return <>{children}</>;\n}\n\ninterface ExplorerKeyboardProxyProps {\n readonly children: React.ReactNode;\n readonly onKeyDown: (key: string, shiftKey: boolean) => boolean;\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
/*** Keeps Explorer navigation portable when a native host has no DOM keyboard event surface. */
|
|
3
|
+
export declare function ExplorerKeyboardProxy({ children }: ExplorerKeyboardProxyProps): React.JSX.Element;
|
|
4
|
+
interface ExplorerKeyboardProxyProps {
|
|
5
|
+
readonly children: React.ReactNode;
|
|
6
|
+
readonly onKeyDown: (key: string, shiftKey: boolean) => boolean;
|
|
7
|
+
}
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=ExplorerKeyboardProxy.native.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExplorerKeyboardProxy.native.d.ts","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.native.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,gGAAgG;AAChG,wBAAgB,qBAAqB,CAAC,EAAE,QAAQ,EAAE,EAAE,0BAA0B,qBAE7E;AAED,UAAU,0BAA0B;IAClC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IACnC,QAAQ,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC;CACjE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExplorerKeyboardProxy.native.js","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.native.tsx"],"names":[],"mappings":"AAEA,gGAAgG;AAChG,MAAM,UAAU,qBAAqB,CAAC,EAAE,QAAQ,EAA8B;IAC5E,OAAO,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC;AACzB,CAAC","sourcesContent":["import type React from 'react';\n\n/*** Keeps Explorer navigation portable when a native host has no DOM keyboard event surface. */\nexport function ExplorerKeyboardProxy({ children }: ExplorerKeyboardProxyProps) {\n return <>{children}</>;\n}\n\ninterface ExplorerKeyboardProxyProps {\n readonly children: React.ReactNode;\n readonly onKeyDown: (key: string, shiftKey: boolean) => boolean;\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
/*** Bridges RNW's rendered DOM focus events into Explorer's platform-neutral navigation policy. */
|
|
3
|
+
export declare function ExplorerKeyboardProxy({ children, onKeyDown }: ExplorerKeyboardProxyProps): React.JSX.Element;
|
|
4
|
+
interface ExplorerKeyboardProxyProps {
|
|
5
|
+
readonly children: React.ReactNode;
|
|
6
|
+
readonly onKeyDown: (key: string, shiftKey: boolean) => boolean;
|
|
7
|
+
}
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=ExplorerKeyboardProxy.web.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExplorerKeyboardProxy.web.d.ts","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.web.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,mGAAmG;AACnG,wBAAgB,qBAAqB,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,0BAA0B,qBAWxF;AAED,UAAU,0BAA0B;IAClC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IACnC,QAAQ,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC;CACjE"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*** Bridges RNW's rendered DOM focus events into Explorer's platform-neutral navigation policy. */
|
|
2
|
+
export function ExplorerKeyboardProxy({ children, onKeyDown }) {
|
|
3
|
+
return (<div onKeyDown={(event) => {
|
|
4
|
+
if (onKeyDown(event.key, event.shiftKey))
|
|
5
|
+
event.preventDefault();
|
|
6
|
+
}} style={{ display: 'flex', flex: 1 }}>
|
|
7
|
+
{children}
|
|
8
|
+
</div>);
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=ExplorerKeyboardProxy.web.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExplorerKeyboardProxy.web.js","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/ExplorerKeyboardProxy.web.tsx"],"names":[],"mappings":"AAEA,mGAAmG;AACnG,MAAM,UAAU,qBAAqB,CAAC,EAAE,QAAQ,EAAE,SAAS,EAA8B;IACvF,OAAO,CACL,CAAC,GAAG,CACF,SAAS,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE;YACnB,IAAI,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,QAAQ,CAAC;gBAAE,KAAK,CAAC,cAAc,EAAE,CAAC;QACnE,CAAC,CAAC,CACF,KAAK,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAEpC;MAAA,CAAC,QAAQ,CACX;IAAA,EAAE,GAAG,CAAC,CACP,CAAC;AACJ,CAAC","sourcesContent":["import type React from 'react';\n\n/*** Bridges RNW's rendered DOM focus events into Explorer's platform-neutral navigation policy. */\nexport function ExplorerKeyboardProxy({ children, onKeyDown }: ExplorerKeyboardProxyProps) {\n return (\n <div\n onKeyDown={(event) => {\n if (onKeyDown(event.key, event.shiftKey)) event.preventDefault();\n }}\n style={{ display: 'flex', flex: 1 }}\n >\n {children}\n </div>\n );\n}\n\ninterface ExplorerKeyboardProxyProps {\n readonly children: React.ReactNode;\n readonly onKeyDown: (key: string, shiftKey: boolean) => boolean;\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ExplorerItem } from '../../../../types/explorer';
|
|
2
|
+
/*** Tracks the one keyboard tab stop and restores focus only after a virtualized tile mounts. */
|
|
3
|
+
export declare function useExplorerKeyboardFocus(items: readonly ExplorerItem[], columns: number, disabled: boolean, visibleIds: readonly string[], onNavigate: (targetId: string, originId: string, shiftKey: boolean) => void): {
|
|
4
|
+
focusedId: string | null;
|
|
5
|
+
tabStopId: string | undefined;
|
|
6
|
+
onFocus: (id: string) => void;
|
|
7
|
+
onKeyDown: (id: string, key: string, shiftKey: boolean) => boolean;
|
|
8
|
+
registerTile: (id: string, node: FocusableTile | null) => void;
|
|
9
|
+
};
|
|
10
|
+
interface FocusableTile {
|
|
11
|
+
focus: () => void;
|
|
12
|
+
}
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=useExplorerKeyboardFocus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useExplorerKeyboardFocus.d.ts","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/useExplorerKeyboardFocus.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAG/D,iGAAiG;AACjG,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,SAAS,YAAY,EAAE,EAC9B,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,OAAO,EACjB,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI;;;kBAkBpC,MAAM;oBAmBtC,MAAM,OAAO,MAAM,YAAY,OAAO,KAAG,OAAO;uBAbX,MAAM,QAAQ,aAAa,GAAG,IAAI;EAqC/E;AAED,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { getExplorerNextFocusId } from '../../application/getExplorerNextFocusId';
|
|
3
|
+
/*** Tracks the one keyboard tab stop and restores focus only after a virtualized tile mounts. */
|
|
4
|
+
export function useExplorerKeyboardFocus(items, columns, disabled, visibleIds, onNavigate) {
|
|
5
|
+
const [focusedId, setFocusedId] = React.useState(null);
|
|
6
|
+
const focusedRef = React.useRef(null);
|
|
7
|
+
const pendingFocusRef = React.useRef(null);
|
|
8
|
+
const tileRefs = React.useRef(new Map());
|
|
9
|
+
const ids = React.useMemo(() => items.map(({ id }) => id), [items]);
|
|
10
|
+
const idSet = React.useMemo(() => new Set(ids), [ids]);
|
|
11
|
+
const disabledIds = React.useMemo(() => new Set(items.filter((item) => item.disabled).map(({ id }) => id)), [items]);
|
|
12
|
+
const visibleEligibleIds = visibleIds.filter((id) => idSet.has(id) && !disabledIds.has(id));
|
|
13
|
+
const tabStopId = focusedId !== null && visibleEligibleIds.includes(focusedId)
|
|
14
|
+
? focusedId
|
|
15
|
+
: (visibleEligibleIds[0] ?? ids.find((id) => !disabledIds.has(id)));
|
|
16
|
+
const onFocus = React.useCallback((id) => {
|
|
17
|
+
pendingFocusRef.current = null;
|
|
18
|
+
focusedRef.current = id;
|
|
19
|
+
setFocusedId(id);
|
|
20
|
+
}, []);
|
|
21
|
+
const registerTile = React.useCallback((id, node) => {
|
|
22
|
+
if (node === null) {
|
|
23
|
+
tileRefs.current.delete(id);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
tileRefs.current.set(id, node);
|
|
27
|
+
if (pendingFocusRef.current === id) {
|
|
28
|
+
pendingFocusRef.current = null;
|
|
29
|
+
node.focus();
|
|
30
|
+
}
|
|
31
|
+
}, []);
|
|
32
|
+
const onKeyDown = React.useCallback((id, key, shiftKey) => {
|
|
33
|
+
if (disabled)
|
|
34
|
+
return false;
|
|
35
|
+
if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(key))
|
|
36
|
+
return false;
|
|
37
|
+
const originId = focusedRef.current ?? id;
|
|
38
|
+
const nextId = getExplorerNextFocusId(ids, originId, key, columns, disabledIds);
|
|
39
|
+
if (nextId === null || nextId === originId)
|
|
40
|
+
return true;
|
|
41
|
+
focusedRef.current = nextId;
|
|
42
|
+
pendingFocusRef.current = nextId;
|
|
43
|
+
setFocusedId(nextId);
|
|
44
|
+
onNavigate(nextId, originId, shiftKey);
|
|
45
|
+
const mounted = tileRefs.current.get(nextId);
|
|
46
|
+
if (mounted) {
|
|
47
|
+
pendingFocusRef.current = null;
|
|
48
|
+
mounted.focus();
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}, [columns, disabled, disabledIds, ids, onNavigate]);
|
|
52
|
+
return { focusedId, tabStopId, onFocus, onKeyDown, registerTile };
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=useExplorerKeyboardFocus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useExplorerKeyboardFocus.js","sourceRoot":"","sources":["../../../../../src/features/explorer/adapters/inbound/useExplorerKeyboardFocus.ts"],"names":[],"mappings":"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","sourcesContent":["import React from 'react';\n\nimport type { ExplorerItem } from '../../../../types/explorer';\nimport { getExplorerNextFocusId } from '../../application/getExplorerNextFocusId';\n\n/*** Tracks the one keyboard tab stop and restores focus only after a virtualized tile mounts. */\nexport function useExplorerKeyboardFocus(\n items: readonly ExplorerItem[],\n columns: number,\n disabled: boolean,\n visibleIds: readonly string[],\n onNavigate: (targetId: string, originId: string, shiftKey: boolean) => void,\n) {\n const [focusedId, setFocusedId] = React.useState<string | null>(null);\n const focusedRef = React.useRef<string | null>(null);\n const pendingFocusRef = React.useRef<string | null>(null);\n const tileRefs = React.useRef(new Map<string, FocusableTile>());\n const ids = React.useMemo(() => items.map(({ id }) => id), [items]);\n const idSet = React.useMemo(() => new Set(ids), [ids]);\n const disabledIds = React.useMemo(\n () => new Set(items.filter((item) => item.disabled).map(({ id }) => id)),\n [items],\n );\n const visibleEligibleIds = visibleIds.filter((id) => idSet.has(id) && !disabledIds.has(id));\n const tabStopId =\n focusedId !== null && visibleEligibleIds.includes(focusedId)\n ? focusedId\n : (visibleEligibleIds[0] ?? ids.find((id) => !disabledIds.has(id)));\n\n const onFocus = React.useCallback((id: string) => {\n pendingFocusRef.current = null;\n focusedRef.current = id;\n setFocusedId(id);\n }, []);\n\n const registerTile = React.useCallback((id: string, node: FocusableTile | null) => {\n if (node === null) {\n tileRefs.current.delete(id);\n return;\n }\n tileRefs.current.set(id, node);\n if (pendingFocusRef.current === id) {\n pendingFocusRef.current = null;\n node.focus();\n }\n }, []);\n\n const onKeyDown = React.useCallback(\n (id: string, key: string, shiftKey: boolean): boolean => {\n if (disabled) return false;\n if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(key))\n return false;\n const originId = focusedRef.current ?? id;\n const nextId = getExplorerNextFocusId(ids, originId, key, columns, disabledIds);\n if (nextId === null || nextId === originId) return true;\n\n focusedRef.current = nextId;\n pendingFocusRef.current = nextId;\n setFocusedId(nextId);\n onNavigate(nextId, originId, shiftKey);\n\n const mounted = tileRefs.current.get(nextId);\n if (mounted) {\n pendingFocusRef.current = null;\n mounted.focus();\n }\n return true;\n },\n [columns, disabled, disabledIds, ids, onNavigate],\n );\n\n return { focusedId, tabStopId, onFocus, onKeyDown, registerTile };\n}\n\ninterface FocusableTile {\n focus: () => void;\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/***
|
|
2
|
+
* Resolve focus along actual tile positions while skipping unavailable items
|
|
3
|
+
* without collapsing the visual row geometry.
|
|
4
|
+
*/
|
|
5
|
+
export declare function getExplorerNextFocusId(ids: readonly string[], currentId: string | null, key: string, columns: number, disabledIds?: ReadonlySet<string>): string | null;
|
|
6
|
+
//# sourceMappingURL=getExplorerNextFocusId.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getExplorerNextFocusId.d.ts","sourceRoot":"","sources":["../../../../src/features/explorer/application/getExplorerNextFocusId.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,SAAS,MAAM,EAAE,EACtB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,WAAW,GAAE,WAAW,CAAC,MAAM,CAAa,GAC3C,MAAM,GAAG,IAAI,CA6Bf"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/***
|
|
2
|
+
* Resolve focus along actual tile positions while skipping unavailable items
|
|
3
|
+
* without collapsing the visual row geometry.
|
|
4
|
+
*/
|
|
5
|
+
export function getExplorerNextFocusId(ids, currentId, key, columns, disabledIds = new Set()) {
|
|
6
|
+
const delta = resolveFocusDelta(key, columns);
|
|
7
|
+
if (delta === null || ids.length === 0)
|
|
8
|
+
return null;
|
|
9
|
+
const firstIndex = ids.findIndex((id) => !disabledIds.has(id));
|
|
10
|
+
if (firstIndex < 0)
|
|
11
|
+
return null;
|
|
12
|
+
const currentIndex = Math.max(firstIndex, ids.indexOf(currentId ?? ''));
|
|
13
|
+
const candidateIndex = delta === Number.NEGATIVE_INFINITY
|
|
14
|
+
? 0
|
|
15
|
+
: delta === Number.POSITIVE_INFINITY
|
|
16
|
+
? ids.length - 1
|
|
17
|
+
: Math.max(0, Math.min(ids.length - 1, currentIndex + delta));
|
|
18
|
+
const direction = delta === Number.NEGATIVE_INFINITY
|
|
19
|
+
? 1
|
|
20
|
+
: delta === Number.POSITIVE_INFINITY
|
|
21
|
+
? -1
|
|
22
|
+
: Math.sign(delta);
|
|
23
|
+
for (let index = candidateIndex; index >= 0 && index < ids.length; index += direction) {
|
|
24
|
+
const candidate = ids.at(index);
|
|
25
|
+
if (candidate !== undefined && !disabledIds.has(candidate))
|
|
26
|
+
return candidate;
|
|
27
|
+
}
|
|
28
|
+
const current = ids.at(currentIndex);
|
|
29
|
+
return current !== undefined && !disabledIds.has(current)
|
|
30
|
+
? current
|
|
31
|
+
: (ids.at(firstIndex) ?? null);
|
|
32
|
+
}
|
|
33
|
+
/*** Translate navigation keys into collection-relative movements. */
|
|
34
|
+
function resolveFocusDelta(key, columns) {
|
|
35
|
+
const rowSize = Math.max(1, Math.floor(columns));
|
|
36
|
+
switch (key) {
|
|
37
|
+
case 'ArrowLeft':
|
|
38
|
+
return -1;
|
|
39
|
+
case 'ArrowRight':
|
|
40
|
+
return 1;
|
|
41
|
+
case 'ArrowUp':
|
|
42
|
+
return -rowSize;
|
|
43
|
+
case 'ArrowDown':
|
|
44
|
+
return rowSize;
|
|
45
|
+
case 'Home':
|
|
46
|
+
return Number.NEGATIVE_INFINITY;
|
|
47
|
+
case 'End':
|
|
48
|
+
return Number.POSITIVE_INFINITY;
|
|
49
|
+
default:
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=getExplorerNextFocusId.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getExplorerNextFocusId.js","sourceRoot":"","sources":["../../../../src/features/explorer/application/getExplorerNextFocusId.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CACpC,GAAsB,EACtB,SAAwB,EACxB,GAAW,EACX,OAAe,EACf,cAAmC,IAAI,GAAG,EAAE;IAE5C,MAAM,KAAK,GAAG,iBAAiB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC9C,IAAI,KAAK,KAAK,IAAI,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEpD,MAAM,UAAU,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/D,IAAI,UAAU,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAEhC,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,CAAC;IACxE,MAAM,cAAc,GAClB,KAAK,KAAK,MAAM,CAAC,iBAAiB;QAChC,CAAC,CAAC,CAAC;QACH,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,iBAAiB;YAClC,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC;YAChB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,YAAY,GAAG,KAAK,CAAC,CAAC,CAAC;IACpE,MAAM,SAAS,GACb,KAAK,KAAK,MAAM,CAAC,iBAAiB;QAChC,CAAC,CAAC,CAAC;QACH,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,iBAAiB;YAClC,CAAC,CAAC,CAAC,CAAC;YACJ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAEzB,KAAK,IAAI,KAAK,GAAG,cAAc,EAAE,KAAK,IAAI,CAAC,IAAI,KAAK,GAAG,GAAG,CAAC,MAAM,EAAE,KAAK,IAAI,SAAS,EAAE,CAAC;QACtF,MAAM,SAAS,GAAG,GAAG,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;QAChC,IAAI,SAAS,KAAK,SAAS,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAC;IAC/E,CAAC;IACD,MAAM,OAAO,GAAG,GAAG,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC;IACrC,OAAO,OAAO,KAAK,SAAS,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC;QACvD,CAAC,CAAC,OAAO;QACT,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,CAAC;AACnC,CAAC;AAED,qEAAqE;AACrE,SAAS,iBAAiB,CAAC,GAAW,EAAE,OAAe;IACrD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;IACjD,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,WAAW;YACd,OAAO,CAAC,CAAC,CAAC;QACZ,KAAK,YAAY;YACf,OAAO,CAAC,CAAC;QACX,KAAK,SAAS;YACZ,OAAO,CAAC,OAAO,CAAC;QAClB,KAAK,WAAW;YACd,OAAO,OAAO,CAAC;QACjB,KAAK,MAAM;YACT,OAAO,MAAM,CAAC,iBAAiB,CAAC;QAClC,KAAK,KAAK;YACR,OAAO,MAAM,CAAC,iBAAiB,CAAC;QAClC;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;AACH,CAAC","sourcesContent":["/***\n * Resolve focus along actual tile positions while skipping unavailable items\n * without collapsing the visual row geometry.\n */\nexport function getExplorerNextFocusId(\n ids: readonly string[],\n currentId: string | null,\n key: string,\n columns: number,\n disabledIds: ReadonlySet<string> = new Set(),\n): string | null {\n const delta = resolveFocusDelta(key, columns);\n if (delta === null || ids.length === 0) return null;\n\n const firstIndex = ids.findIndex((id) => !disabledIds.has(id));\n if (firstIndex < 0) return null;\n\n const currentIndex = Math.max(firstIndex, ids.indexOf(currentId ?? ''));\n const candidateIndex =\n delta === Number.NEGATIVE_INFINITY\n ? 0\n : delta === Number.POSITIVE_INFINITY\n ? ids.length - 1\n : Math.max(0, Math.min(ids.length - 1, currentIndex + delta));\n const direction =\n delta === Number.NEGATIVE_INFINITY\n ? 1\n : delta === Number.POSITIVE_INFINITY\n ? -1\n : Math.sign(delta);\n\n for (let index = candidateIndex; index >= 0 && index < ids.length; index += direction) {\n const candidate = ids.at(index);\n if (candidate !== undefined && !disabledIds.has(candidate)) return candidate;\n }\n const current = ids.at(currentIndex);\n return current !== undefined && !disabledIds.has(current)\n ? current\n : (ids.at(firstIndex) ?? null);\n}\n\n/*** Translate navigation keys into collection-relative movements. */\nfunction resolveFocusDelta(key: string, columns: number): number | null {\n const rowSize = Math.max(1, Math.floor(columns));\n switch (key) {\n case 'ArrowLeft':\n return -1;\n case 'ArrowRight':\n return 1;\n case 'ArrowUp':\n return -rowSize;\n case 'ArrowDown':\n return rowSize;\n case 'Home':\n return Number.NEGATIVE_INFINITY;\n case 'End':\n return Number.POSITIVE_INFINITY;\n default:\n return null;\n }\n}\n"]}
|
|
@@ -2,5 +2,6 @@ export type { ExplorerActivateEvent, ExplorerItem, ExplorerItemKind, ExplorerPro
|
|
|
2
2
|
export { Explorer } from './adapters/inbound/Explorer';
|
|
3
3
|
export { FileExplorer } from './adapters/inbound/FileExplorer';
|
|
4
4
|
export { MediaExplorer } from './adapters/inbound/MediaExplorer';
|
|
5
|
+
export { getExplorerNextFocusId } from './application/getExplorerNextFocusId';
|
|
5
6
|
export { resolveExplorerSelection } from './application/resolveExplorerSelection';
|
|
6
7
|
//# sourceMappingURL=public.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/features/explorer/public.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,qBAAqB,EACrB,YAAY,EACZ,gBAAgB,EAChB,aAAa,EACb,4BAA4B,EAC5B,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wCAAwC,CAAC"}
|
|
1
|
+
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/features/explorer/public.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,qBAAqB,EACrB,YAAY,EACZ,gBAAgB,EAChB,aAAa,EACb,4BAA4B,EAC5B,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAC9E,OAAO,EAAE,wBAAwB,EAAE,MAAM,wCAAwC,CAAC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { Explorer } from './adapters/inbound/Explorer';
|
|
2
2
|
export { FileExplorer } from './adapters/inbound/FileExplorer';
|
|
3
3
|
export { MediaExplorer } from './adapters/inbound/MediaExplorer';
|
|
4
|
+
export { getExplorerNextFocusId } from './application/getExplorerNextFocusId';
|
|
4
5
|
export { resolveExplorerSelection } from './application/resolveExplorerSelection';
|
|
5
6
|
//# sourceMappingURL=public.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public.js","sourceRoot":"","sources":["../../../src/features/explorer/public.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wCAAwC,CAAC","sourcesContent":["export type {\n ExplorerActivateEvent,\n ExplorerItem,\n ExplorerItemKind,\n ExplorerProps,\n ExplorerSelectionChangeEvent,\n ExplorerSelectionMode,\n} from '../../types/explorer';\nexport { Explorer } from './adapters/inbound/Explorer';\nexport { FileExplorer } from './adapters/inbound/FileExplorer';\nexport { MediaExplorer } from './adapters/inbound/MediaExplorer';\nexport { resolveExplorerSelection } from './application/resolveExplorerSelection';\n"]}
|
|
1
|
+
{"version":3,"file":"public.js","sourceRoot":"","sources":["../../../src/features/explorer/public.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAC9E,OAAO,EAAE,wBAAwB,EAAE,MAAM,wCAAwC,CAAC","sourcesContent":["export type {\n ExplorerActivateEvent,\n ExplorerItem,\n ExplorerItemKind,\n ExplorerProps,\n ExplorerSelectionChangeEvent,\n ExplorerSelectionMode,\n} from '../../types/explorer';\nexport { Explorer } from './adapters/inbound/Explorer';\nexport { FileExplorer } from './adapters/inbound/FileExplorer';\nexport { MediaExplorer } from './adapters/inbound/MediaExplorer';\nexport { getExplorerNextFocusId } from './application/getExplorerNextFocusId';\nexport { resolveExplorerSelection } from './application/resolveExplorerSelection';\n"]}
|
|
@@ -6,5 +6,5 @@ import type { GridViewProps } from '../../../../types/grid-view';
|
|
|
6
6
|
* This first renderer uses nested native scroll regions. Only visible items are mounted;
|
|
7
7
|
* logical cells and invisible elements are never rendered.
|
|
8
8
|
*/
|
|
9
|
-
export declare function GridView({ items, contentWidth, contentHeight, width, height, zoom, overscanPixels, interactionPolicy, onViewportChange, renderItem, testID, }: GridViewProps): React.JSX.Element;
|
|
9
|
+
export declare function GridView({ items, contentWidth, contentHeight, width, height, zoom, overscanPixels, focusedItemId, revealPaddingPixels, interactionPolicy, onViewportChange, onVisibleItemIdsChange, renderItem, testID, }: GridViewProps): React.JSX.Element;
|
|
10
10
|
//# sourceMappingURL=GridView.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GridView.d.ts","sourceRoot":"","sources":["../../../../../src/features/grid-view/adapters/inbound/GridView.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"GridView.d.ts","sourceRoot":"","sources":["../../../../../src/features/grid-view/adapters/inbound/GridView.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEjE;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,EACvB,KAAK,EACL,YAAY,EACZ,aAAa,EACb,KAAK,EACL,MAAM,EACN,IAAQ,EACR,cAAoB,EACpB,aAAa,EACb,mBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,EAChB,sBAAsB,EACtB,UAAU,EACV,MAAM,GACP,EAAE,aAAa,qBA8Gf"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getVisibleGridItems, worldToViewport } from '@ankhorage/grid-view';
|
|
1
|
+
import { getVisibleGridItems, revealWorldRect, worldToViewport, } from '@ankhorage/grid-view';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { ScrollView as NativeScrollView, View as NativeView } from 'react-native';
|
|
4
4
|
/***
|
|
@@ -7,9 +7,11 @@ import { ScrollView as NativeScrollView, View as NativeView } from 'react-native
|
|
|
7
7
|
* This first renderer uses nested native scroll regions. Only visible items are mounted;
|
|
8
8
|
* logical cells and invisible elements are never rendered.
|
|
9
9
|
*/
|
|
10
|
-
export function GridView({ items, contentWidth, contentHeight, width, height, zoom = 1, overscanPixels = 160, interactionPolicy, onViewportChange, renderItem, testID, }) {
|
|
10
|
+
export function GridView({ items, contentWidth, contentHeight, width, height, zoom = 1, overscanPixels = 160, focusedItemId, revealPaddingPixels = 8, interactionPolicy, onViewportChange, onVisibleItemIdsChange, renderItem, testID, }) {
|
|
11
11
|
const [scrollX, setScrollX] = React.useState(0);
|
|
12
12
|
const [scrollY, setScrollY] = React.useState(0);
|
|
13
|
+
const horizontalScrollRef = React.useRef(null);
|
|
14
|
+
const verticalScrollRef = React.useRef(null);
|
|
13
15
|
const scale = Math.max(0.01, zoom);
|
|
14
16
|
const viewport = React.useMemo(() => ({
|
|
15
17
|
width,
|
|
@@ -20,11 +22,41 @@ export function GridView({ items, contentWidth, contentHeight, width, height, zo
|
|
|
20
22
|
pixelsPerUnitY: scale,
|
|
21
23
|
}), [height, scale, scrollX, scrollY, width]);
|
|
22
24
|
const visibleItems = React.useMemo(() => getVisibleGridItems(items, viewport, overscanPixels), [items, overscanPixels, viewport]);
|
|
25
|
+
const viewportVisibleIds = React.useMemo(() => getVisibleGridItems(visibleItems, viewport).map((item) => item.id), [viewport, visibleItems]);
|
|
26
|
+
const previousVisibleIdsRef = React.useRef([]);
|
|
27
|
+
React.useEffect(() => {
|
|
28
|
+
if (!onVisibleItemIdsChange)
|
|
29
|
+
return;
|
|
30
|
+
const previous = previousVisibleIdsRef.current;
|
|
31
|
+
if (previous.length === viewportVisibleIds.length &&
|
|
32
|
+
previous.every((id, index) => id === viewportVisibleIds.at(index))) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
previousVisibleIdsRef.current = viewportVisibleIds;
|
|
36
|
+
onVisibleItemIdsChange(viewportVisibleIds);
|
|
37
|
+
}, [onVisibleItemIdsChange, viewportVisibleIds]);
|
|
23
38
|
React.useEffect(() => {
|
|
24
39
|
onViewportChange?.(viewport);
|
|
25
40
|
}, [onViewportChange, viewport]);
|
|
26
|
-
|
|
27
|
-
|
|
41
|
+
const viewportRef = React.useRef(viewport);
|
|
42
|
+
React.useEffect(() => {
|
|
43
|
+
viewportRef.current = viewport;
|
|
44
|
+
}, [viewport]);
|
|
45
|
+
React.useEffect(() => {
|
|
46
|
+
const currentViewport = viewportRef.current;
|
|
47
|
+
const focusedItem = items.find((item) => item.id === focusedItemId);
|
|
48
|
+
if (!focusedItem)
|
|
49
|
+
return;
|
|
50
|
+
const revealed = revealWorldRect(currentViewport, focusedItem, revealPaddingPixels);
|
|
51
|
+
if (revealed.offsetX !== currentViewport.offsetX) {
|
|
52
|
+
horizontalScrollRef.current?.scrollTo({ x: revealed.offsetX * scale, animated: true });
|
|
53
|
+
}
|
|
54
|
+
if (revealed.offsetY !== currentViewport.offsetY) {
|
|
55
|
+
verticalScrollRef.current?.scrollTo({ y: revealed.offsetY * scale, animated: true });
|
|
56
|
+
}
|
|
57
|
+
}, [focusedItemId, height, items, revealPaddingPixels, scale, width]);
|
|
58
|
+
return (<NativeScrollView ref={horizontalScrollRef} horizontal scrollEnabled={interactionPolicy !== 'passive'} scrollEventThrottle={32} showsHorizontalScrollIndicator style={{ width, height }} testID={testID} onScroll={(event) => setScrollX(event.nativeEvent.contentOffset.x)}>
|
|
59
|
+
<NativeScrollView ref={verticalScrollRef} scrollEnabled={interactionPolicy !== 'passive'} scrollEventThrottle={32} showsVerticalScrollIndicator style={{ width: contentWidth * scale, height }} onScroll={(event) => setScrollY(event.nativeEvent.contentOffset.y)}>
|
|
28
60
|
<NativeView style={{ width: contentWidth * scale, height: contentHeight * scale }}>
|
|
29
61
|
{visibleItems.map((item) => {
|
|
30
62
|
const position = worldToViewport({ x: item.x, y: item.y }, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GridView.js","sourceRoot":"","sources":["../../../../../src/features/grid-view/adapters/inbound/GridView.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"GridView.js","sourceRoot":"","sources":["../../../../../src/features/grid-view/adapters/inbound/GridView.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n getVisibleGridItems,\n type GridViewport,\n revealWorldRect,\n worldToViewport,\n} from '@ankhorage/grid-view';\nimport React from 'react';\nimport { ScrollView as NativeScrollView, View as NativeView } from 'react-native';\n\nimport type { GridViewProps } from '../../../../types/grid-view';\n\n/***\n * Renders world-positioned items on native and web using the canonical viewport/culling engine.\n *\n * This first renderer uses nested native scroll regions. Only visible items are mounted;\n * logical cells and invisible elements are never rendered.\n */\nexport function GridView({\n items,\n contentWidth,\n contentHeight,\n width,\n height,\n zoom = 1,\n overscanPixels = 160,\n focusedItemId,\n revealPaddingPixels = 8,\n interactionPolicy,\n onViewportChange,\n onVisibleItemIdsChange,\n renderItem,\n testID,\n}: GridViewProps) {\n const [scrollX, setScrollX] = React.useState(0);\n const [scrollY, setScrollY] = React.useState(0);\n const horizontalScrollRef = React.useRef<NativeScrollView>(null);\n const verticalScrollRef = React.useRef<NativeScrollView>(null);\n const scale = Math.max(0.01, zoom);\n const viewport = React.useMemo<GridViewport>(\n () => ({\n width,\n height,\n offsetX: scrollX / scale,\n offsetY: scrollY / scale,\n pixelsPerUnitX: scale,\n pixelsPerUnitY: scale,\n }),\n [height, scale, scrollX, scrollY, width],\n );\n const visibleItems = React.useMemo(\n () => getVisibleGridItems(items, viewport, overscanPixels),\n [items, overscanPixels, viewport],\n );\n const viewportVisibleIds = React.useMemo(\n () => getVisibleGridItems(visibleItems, viewport).map((item) => item.id),\n [viewport, visibleItems],\n );\n const previousVisibleIdsRef = React.useRef<readonly string[]>([]);\n\n React.useEffect(() => {\n if (!onVisibleItemIdsChange) return;\n const previous = previousVisibleIdsRef.current;\n if (\n previous.length === viewportVisibleIds.length &&\n previous.every((id, index) => id === viewportVisibleIds.at(index))\n ) {\n return;\n }\n previousVisibleIdsRef.current = viewportVisibleIds;\n onVisibleItemIdsChange(viewportVisibleIds);\n }, [onVisibleItemIdsChange, viewportVisibleIds]);\n\n React.useEffect(() => {\n onViewportChange?.(viewport);\n }, [onViewportChange, viewport]);\n\n const viewportRef = React.useRef(viewport);\n React.useEffect(() => {\n viewportRef.current = viewport;\n }, [viewport]);\n\n React.useEffect(() => {\n const currentViewport = viewportRef.current;\n const focusedItem = items.find((item) => item.id === focusedItemId);\n if (!focusedItem) return;\n const revealed = revealWorldRect(currentViewport, focusedItem, revealPaddingPixels);\n if (revealed.offsetX !== currentViewport.offsetX) {\n horizontalScrollRef.current?.scrollTo({ x: revealed.offsetX * scale, animated: true });\n }\n if (revealed.offsetY !== currentViewport.offsetY) {\n verticalScrollRef.current?.scrollTo({ y: revealed.offsetY * scale, animated: true });\n }\n }, [focusedItemId, height, items, revealPaddingPixels, scale, width]);\n\n return (\n <NativeScrollView\n ref={horizontalScrollRef}\n horizontal\n scrollEnabled={interactionPolicy !== 'passive'}\n scrollEventThrottle={32}\n showsHorizontalScrollIndicator\n style={{ width, height }}\n testID={testID}\n onScroll={(event) => setScrollX(event.nativeEvent.contentOffset.x)}\n >\n <NativeScrollView\n ref={verticalScrollRef}\n scrollEnabled={interactionPolicy !== 'passive'}\n scrollEventThrottle={32}\n showsVerticalScrollIndicator\n style={{ width: contentWidth * scale, height }}\n onScroll={(event) => setScrollY(event.nativeEvent.contentOffset.y)}\n >\n <NativeView style={{ width: contentWidth * scale, height: contentHeight * scale }}>\n {visibleItems.map((item) => {\n const position = worldToViewport(\n { x: item.x, y: item.y },\n {\n ...viewport,\n offsetX: 0,\n offsetY: 0,\n },\n );\n return (\n <NativeView\n key={item.id}\n style={{\n position: 'absolute',\n left: position.x,\n top: position.y,\n width: item.width * scale,\n height: item.height * scale,\n }}\n >\n {renderItem(item)}\n </NativeView>\n );\n })}\n </NativeView>\n </NativeScrollView>\n </NativeScrollView>\n );\n}\n"]}
|