@ankhorage/zora 25.0.1 → 25.2.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 +13 -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 +72 -14
- 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/application/shouldDispatchExplorerPageRequest.d.ts +8 -0
- package/dist/features/explorer/application/shouldDispatchExplorerPageRequest.d.ts.map +1 -0
- package/dist/features/explorer/application/shouldDispatchExplorerPageRequest.js +9 -0
- package/dist/features/explorer/application/shouldDispatchExplorerPageRequest.js.map +1 -0
- package/dist/features/explorer/application/shouldRequestExplorerPage.d.ts +4 -0
- package/dist/features/explorer/application/shouldRequestExplorerPage.d.ts.map +1 -0
- package/dist/features/explorer/application/shouldRequestExplorerPage.js +14 -0
- package/dist/features/explorer/application/shouldRequestExplorerPage.js.map +1 -0
- package/dist/features/explorer/meta.d.ts +110 -0
- package/dist/features/explorer/meta.d.ts.map +1 -1
- package/dist/features/explorer/meta.js +25 -0
- package/dist/features/explorer/meta.js.map +1 -1
- package/dist/features/explorer/public.d.ts +2 -1
- 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/explorer.d.ts +28 -0
- package/dist/types/explorer.d.ts.map +1 -1
- package/dist/types/explorer.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 +1 -1
- package/src/capabilities/index.test.ts +10 -6
- package/src/features/explorer/adapters/inbound/Explorer.tsx +158 -53
- 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/application/shouldDispatchExplorerPageRequest.test.ts +43 -0
- package/src/features/explorer/application/shouldDispatchExplorerPageRequest.ts +18 -0
- package/src/features/explorer/application/shouldRequestExplorerPage.test.ts +93 -0
- package/src/features/explorer/application/shouldRequestExplorerPage.ts +26 -0
- package/src/features/explorer/meta.test.ts +55 -0
- package/src/features/explorer/meta.ts +25 -0
- package/src/features/explorer/public.ts +3 -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/explorer.ts +30 -0
- package/src/types/grid-view.ts +10 -0
- package/web-dist/chunks/{Accordion-b20zfsfz.js → Accordion-54k0ncs3.js} +83 -0
- package/web-dist/chunks/Accordion-kkr7xfh2.js +397 -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-0md8wbtx.js +0 -197
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 25.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 92ed809: Finish the Explorer migration with provider-neutral paging and permission presentation, and extend
|
|
8
|
+
the runnable GridView workspace example with paged, limited-access, and Uploader interactions.
|
|
9
|
+
|
|
10
|
+
## 25.1.0
|
|
11
|
+
|
|
12
|
+
### Minor Changes
|
|
13
|
+
|
|
14
|
+
- 52f40c6: Add virtualized Explorer keyboard focus navigation with published GridView reveal geometry and a runnable native/web workspace example.
|
|
15
|
+
|
|
3
16
|
## 25.0.1
|
|
4
17
|
|
|
5
18
|
### Patch 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;AAgB9E,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,18 +7,51 @@ 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 { shouldDispatchExplorerPageRequest, } from '../../application/shouldDispatchExplorerPageRequest';
|
|
11
|
+
import { shouldRequestExplorerPage } from '../../application/shouldRequestExplorerPage';
|
|
12
|
+
import { ExplorerKeyboardProxy } from './ExplorerKeyboardProxy';
|
|
13
|
+
import { useExplorerKeyboardFocus } from './useExplorerKeyboardFocus';
|
|
10
14
|
/*** Single canonical Explorer implementation for media and file catalogue presentation. */
|
|
11
15
|
export const Explorer = withZoraThemeScope(ExplorerInner);
|
|
12
16
|
/*** Presents provider-neutral asset tiles with controlled or uncontrolled selection. */
|
|
13
|
-
function ExplorerInner({ items, selectedIds, defaultSelectedIds = [], selectionMode = 'single', onSelectionChange, onActivate, width, height, tileSize = 120, zoom = 1, loading = false, errorText, emptyText = 'No items', disabled = false, readOnly = false, interactionPolicy, testID, }) {
|
|
17
|
+
function ExplorerInner({ items, selectedIds, defaultSelectedIds = [], selectionMode = 'single', onSelectionChange, onActivate, width, height, tileSize = 120, zoom = 1, loading = false, hasMore = false, loadingMore = false, pagingCollectionId, pagingRetryToken, onLoadMore, permissionStatus = 'granted', permissionText, onRequestPermission, errorText, emptyText = 'No items', disabled = false, readOnly = false, interactionPolicy, testID, }) {
|
|
14
18
|
const { theme } = useZoraTheme();
|
|
15
19
|
const [internalSelectedIds, setInternalSelectedIds] = React.useState(defaultSelectedIds);
|
|
16
20
|
const anchorId = React.useRef(null);
|
|
21
|
+
const [columns, setColumns] = React.useState(1);
|
|
22
|
+
const [visibleIds, setVisibleIds] = React.useState([]);
|
|
23
|
+
const lastPageRequestRef = React.useRef(null);
|
|
17
24
|
const effectiveSelectedIds = selectedIds ?? internalSelectedIds;
|
|
18
25
|
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);
|
|
26
|
+
const ids = React.useMemo(() => items.filter((item) => !item.disabled).map((item) => item.id), [items]);
|
|
20
27
|
const selected = new Set(effectiveSelectedIds);
|
|
21
28
|
const passive = interactionPolicy === 'passive' || disabled || readOnly;
|
|
29
|
+
React.useEffect(() => {
|
|
30
|
+
const request = {
|
|
31
|
+
collectionId: pagingCollectionId,
|
|
32
|
+
loadedItemCount: items.length,
|
|
33
|
+
retryToken: pagingRetryToken,
|
|
34
|
+
};
|
|
35
|
+
if (!onLoadMore ||
|
|
36
|
+
!shouldDispatchExplorerPageRequest(lastPageRequestRef.current, request) ||
|
|
37
|
+
!shouldRequestExplorerPage(ids, visibleIds, hasMore, loadingMore, loading, errorText, permissionStatus)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
lastPageRequestRef.current = request;
|
|
41
|
+
onLoadMore({ loadedItemCount: items.length });
|
|
42
|
+
}, [
|
|
43
|
+
errorText,
|
|
44
|
+
hasMore,
|
|
45
|
+
ids,
|
|
46
|
+
items.length,
|
|
47
|
+
loading,
|
|
48
|
+
loadingMore,
|
|
49
|
+
onLoadMore,
|
|
50
|
+
pagingCollectionId,
|
|
51
|
+
pagingRetryToken,
|
|
52
|
+
permissionStatus,
|
|
53
|
+
visibleIds,
|
|
54
|
+
]);
|
|
22
55
|
const select = (item, intent) => {
|
|
23
56
|
if (passive || item.disabled)
|
|
24
57
|
return;
|
|
@@ -29,13 +62,22 @@ function ExplorerInner({ items, selectedIds, defaultSelectedIds = [], selectionM
|
|
|
29
62
|
setInternalSelectedIds(next);
|
|
30
63
|
onSelectionChange?.({ selectedIds: next });
|
|
31
64
|
};
|
|
65
|
+
const keyboard = useExplorerKeyboardFocus(items, columns, passive, visibleIds, (targetId, originId, shiftKey) => {
|
|
66
|
+
if (!shiftKey)
|
|
67
|
+
return;
|
|
68
|
+
anchorId.current ??= originId;
|
|
69
|
+
const nextItem = itemLookup.get(targetId);
|
|
70
|
+
if (nextItem)
|
|
71
|
+
select(nextItem, 'range');
|
|
72
|
+
});
|
|
32
73
|
const renderTile = (tile) => {
|
|
33
74
|
const item = itemLookup.get(tile.id);
|
|
34
75
|
if (!item)
|
|
35
76
|
return null;
|
|
36
77
|
const isSelected = selected.has(item.id);
|
|
37
78
|
const canInteract = !passive && !item.disabled;
|
|
38
|
-
return (<
|
|
79
|
+
return (<ExplorerKeyboardProxy onKeyDown={(key, shiftKey) => keyboard.onKeyDown(item.id, key, shiftKey)}>
|
|
80
|
+
<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
81
|
const intent = resolveExplorerPressIntent(event.nativeEvent);
|
|
40
82
|
select(item, intent);
|
|
41
83
|
if (selectionMode === 'single' && intent === 'replace') {
|
|
@@ -50,24 +92,40 @@ function ExplorerInner({ items, selectedIds, defaultSelectedIds = [], selectionM
|
|
|
50
92
|
backgroundColor: theme.semantics.neutral.surface,
|
|
51
93
|
padding: 8,
|
|
52
94
|
gap: 4,
|
|
53
|
-
}} testID={`explorer-item-${item.id}`}>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
95
|
+
}} ref={(node) => keyboard.registerTile(item.id, node)} testID={`explorer-item-${item.id}`}>
|
|
96
|
+
<NativeView style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
|
97
|
+
{item.thumbnailUri ? (<Image accessibilityLabel={item.name} aspectRatio={1} source={item.thumbnailUri} width="100%"/>) : (<Icon name={item.kind === 'folder' ? 'folder-outline' : 'document-outline'} size={32}/>)}
|
|
98
|
+
</NativeView>
|
|
99
|
+
<Text numberOfLines={1} variant="caption">
|
|
100
|
+
{item.name}
|
|
101
|
+
</Text>
|
|
102
|
+
</NativePressable>
|
|
103
|
+
</ExplorerKeyboardProxy>);
|
|
61
104
|
};
|
|
105
|
+
if (permissionStatus === 'denied' || permissionStatus === 'unavailable') {
|
|
106
|
+
return (<NativeView testID={testID}>
|
|
107
|
+
<Text>{permissionText ?? 'This collection is unavailable without permission.'}</Text>
|
|
108
|
+
{permissionStatus === 'denied' && onRequestPermission ? (<NativePressable accessibilityRole="button" onPress={onRequestPermission}>
|
|
109
|
+
<Text>Grant access</Text>
|
|
110
|
+
</NativePressable>) : null}
|
|
111
|
+
</NativeView>);
|
|
112
|
+
}
|
|
62
113
|
if (errorText)
|
|
63
114
|
return <Text testID={testID}>{errorText}</Text>;
|
|
64
115
|
if (loading)
|
|
65
116
|
return <Text testID={testID}>Loading…</Text>;
|
|
66
117
|
if (items.length === 0)
|
|
67
118
|
return <Text testID={testID}>{emptyText}</Text>;
|
|
68
|
-
return (<
|
|
119
|
+
return (<NativeView>
|
|
120
|
+
{permissionStatus === 'limited' ? (<Text>{permissionText ?? 'Showing the media you have allowed.'}</Text>) : null}
|
|
121
|
+
<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}/>
|
|
122
|
+
{loadingMore ? <Text>Loading more…</Text> : null}
|
|
123
|
+
</NativeView>);
|
|
69
124
|
}
|
|
70
|
-
/***
|
|
125
|
+
/***
|
|
126
|
+
* Normalize platform press modifiers into selection intents
|
|
127
|
+
* without passing event objects into the public contracts.
|
|
128
|
+
*/
|
|
71
129
|
function resolveExplorerPressIntent(event) {
|
|
72
130
|
if (typeof event !== 'object' || event === null)
|
|
73
131
|
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":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,QAAQ,EAAE,SAAS,IAAI,eAAe,EAAE,IAAI,IAAI,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1F,OAAO,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,oDAAoD,CAAC;AACxF,OAAO,EAAE,YAAY,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,EAAE,IAAI,EAAE,MAAM,4BAA4B,CAAC;AAClD,OAAO,EAAE,wBAAwB,EAAE,MAAM,4CAA4C,CAAC;AACtF,OAAO,EAEL,iCAAiC,GAClC,MAAM,qDAAqD,CAAC;AAC7D,OAAO,EAAE,yBAAyB,EAAE,MAAM,6CAA6C,CAAC;AACxF,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAEtE,2FAA2F;AAC3F,MAAM,CAAC,MAAM,QAAQ,GAAG,kBAAkB,CAAC,aAAa,CAAC,CAAC;AAE1D,wFAAwF;AACxF,SAAS,aAAa,CAAC,EACrB,KAAK,EACL,WAAW,EACX,kBAAkB,GAAG,EAAE,EACvB,aAAa,GAAG,QAAQ,EACxB,iBAAiB,EACjB,UAAU,EACV,KAAK,EACL,MAAM,EACN,QAAQ,GAAG,GAAG,EACd,IAAI,GAAG,CAAC,EACR,OAAO,GAAG,KAAK,EACf,OAAO,GAAG,KAAK,EACf,WAAW,GAAG,KAAK,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,UAAU,EACV,gBAAgB,GAAG,SAAS,EAC5B,cAAc,EACd,mBAAmB,EACnB,SAAS,EACT,SAAS,GAAG,UAAU,EACtB,QAAQ,GAAG,KAAK,EAChB,QAAQ,GAAG,KAAK,EAChB,iBAAiB,EACjB,MAAM,GACQ;IACd,MAAM,EAAE,KAAK,EAAE,GAAG,YAAY,EAAE,CAAC;IACjC,MAAM,CAAC,mBAAmB,EAAE,sBAAsB,CAAC,GACjD,KAAK,CAAC,QAAQ,CAAoB,kBAAkB,CAAC,CAAC;IACxD,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAgB,IAAI,CAAC,CAAC;IACnD,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IAChD,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,KAAK,CAAC,QAAQ,CAAoB,EAAE,CAAC,CAAC;IAC1E,MAAM,kBAAkB,GAAG,KAAK,CAAC,MAAM,CAAsC,IAAI,CAAC,CAAC;IACnF,MAAM,oBAAoB,GAAG,WAAW,IAAI,mBAAmB,CAAC;IAChE,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAC/F,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CACvB,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EACnE,CAAC,KAAK,CAAC,CACR,CAAC;IACF,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,oBAAoB,CAAC,CAAC;IAC/C,MAAM,OAAO,GAAG,iBAAiB,KAAK,SAAS,IAAI,QAAQ,IAAI,QAAQ,CAAC;IAExE,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;QACnB,MAAM,OAAO,GAAG;YACd,YAAY,EAAE,kBAAkB;YAChC,eAAe,EAAE,KAAK,CAAC,MAAM;YAC7B,UAAU,EAAE,gBAAgB;SAC7B,CAAC;QACF,IACE,CAAC,UAAU;YACX,CAAC,iCAAiC,CAAC,kBAAkB,CAAC,OAAO,EAAE,OAAO,CAAC;YACvE,CAAC,yBAAyB,CACxB,GAAG,EACH,UAAU,EACV,OAAO,EACP,WAAW,EACX,OAAO,EACP,SAAS,EACT,gBAAgB,CACjB,EACD,CAAC;YACD,OAAO;QACT,CAAC;QACD,kBAAkB,CAAC,OAAO,GAAG,OAAO,CAAC;QACrC,UAAU,CAAC,EAAE,eAAe,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;IAChD,CAAC,EAAE;QACD,SAAS;QACT,OAAO;QACP,GAAG;QACH,KAAK,CAAC,MAAM;QACZ,OAAO;QACP,WAAW;QACX,UAAU;QACV,kBAAkB;QAClB,gBAAgB;QAChB,gBAAgB;QAChB,UAAU;KACX,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,CAAC,IAAkB,EAAE,MAAsC,EAAE,EAAE;QAC5E,IAAI,OAAO,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO;QACrC,MAAM,IAAI,GAAG,wBAAwB,CACnC,GAAG,EACH,oBAAoB,EACpB,IAAI,CAAC,EAAE,EACP,QAAQ,CAAC,OAAO,EAChB,MAAM,EACN,aAAa,CACd,CAAC;QACF,IAAI,MAAM,KAAK,OAAO;YAAE,QAAQ,CAAC,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC;QACnD,IAAI,WAAW,KAAK,SAAS;YAAE,sBAAsB,CAAC,IAAI,CAAC,CAAC;QAC5D,iBAAiB,EAAE,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,wBAAwB,CACvC,KAAK,EACL,OAAO,EACP,OAAO,EACP,UAAU,EACV,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE;QAC/B,IAAI,CAAC,QAAQ;YAAE,OAAO;QACtB,QAAQ,CAAC,OAAO,KAAK,QAAQ,CAAC;QAC9B,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC1C,IAAI,QAAQ;YAAE,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC1C,CAAC,CACF,CAAC;IAEF,MAAM,UAAU,GAAG,CAAC,IAA6B,EAAE,EAAE;QACnD,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACrC,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC;QACvB,MAAM,UAAU,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACzC,MAAM,WAAW,GAAG,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC;QAC/C,OAAO,CACL,CAAC,qBAAqB,CACpB,SAAS,CAAC,CAAC,CAAC,GAAG,EAAE,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC,CAEzE;QAAA,CAAC,eAAe,CACd,kBAAkB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAC9B,iBAAiB,CAAC,QAAQ,CAC1B,kBAAkB,CAAC,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,WAAW,EAAE,CAAC,CACrE,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CACvB,SAAS,CAAC,CAAC,WAAW,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC,EAAE,KAAK,QAAQ,CAAC,SAAS,CAAC,CAAC,CACpF,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CACzC,WAAW,CAAC,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAC1C,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE;gBACjB,MAAM,MAAM,GAAG,0BAA0B,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;gBAC7D,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;gBACrB,IAAI,aAAa,KAAK,QAAQ,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;oBACvD,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;gBAChC,CAAC;YACH,CAAC,CAAC,CACF,KAAK,CAAC,CAAC;gBACL,IAAI,EAAE,CAAC;gBACP,QAAQ,EAAE,QAAQ;gBAClB,YAAY,EAAE,CAAC;gBACf,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC/B,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO;gBAChF,eAAe,EAAE,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO;gBAChD,OAAO,EAAE,CAAC;gBACV,GAAG,EAAE,CAAC;aACP,CAAC,CACF,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,CACpD,MAAM,CAAC,CAAC,iBAAiB,IAAI,CAAC,EAAE,EAAE,CAAC,CAEnC;UAAA,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,CAC7E;YAAA,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CACnB,CAAC,KAAK,CACJ,kBAAkB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAC9B,WAAW,CAAC,CAAC,CAAC,CAAC,CACf,MAAM,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAC1B,KAAK,CAAC,MAAM,EACZ,CACH,CAAC,CAAC,CAAC,CACF,CAAC,IAAI,CACH,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,kBAAkB,CAAC,CACrE,IAAI,CAAC,CAAC,EAAE,CAAC,EACT,CACH,CACH;UAAA,EAAE,UAAU,CACZ;UAAA,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CACvC;YAAA,CAAC,IAAI,CAAC,IAAI,CACZ;UAAA,EAAE,IAAI,CACR;QAAA,EAAE,eAAe,CACnB;MAAA,EAAE,qBAAqB,CAAC,CACzB,CAAC;IACJ,CAAC,CAAC;IAEF,IAAI,gBAAgB,KAAK,QAAQ,IAAI,gBAAgB,KAAK,aAAa,EAAE,CAAC;QACxE,OAAO,CACL,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CACzB;QAAA,CAAC,IAAI,CAAC,CAAC,cAAc,IAAI,oDAAoD,CAAC,EAAE,IAAI,CACpF;QAAA,CAAC,gBAAgB,KAAK,QAAQ,IAAI,mBAAmB,CAAC,CAAC,CAAC,CACtD,CAAC,eAAe,CAAC,iBAAiB,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,mBAAmB,CAAC,CACvE;YAAA,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAC1B;UAAA,EAAE,eAAe,CAAC,CACnB,CAAC,CAAC,CAAC,IAAI,CACV;MAAA,EAAE,UAAU,CAAC,CACd,CAAC;IACJ,CAAC;IACD,IAAI,SAAS;QAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,CAAC;IAC/D,IAAI,OAAO;QAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC1D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,CAAC;IAExE,OAAO,CACL,CAAC,UAAU,CACT;MAAA,CAAC,gBAAgB,KAAK,SAAS,CAAC,CAAC,CAAC,CAChC,CAAC,IAAI,CAAC,CAAC,cAAc,IAAI,qCAAqC,CAAC,EAAE,IAAI,CAAC,CACvE,CAAC,CAAC,CAAC,IAAI,CACR;MAAA,CAAC,QAAQ,CACP,aAAa,CAAC,CAAC,QAAQ,CAAC,SAAS,IAAI,SAAS,CAAC,CAC/C,MAAM,CAAC,CAAC,MAAM,CAAC,CACf,iBAAiB,CAAC,CAAC,iBAAiB,CAAC,CACrC,eAAe,CAAC,CAAC,UAAU,CAAC,CAC5B,sBAAsB,CAAC,CAAC,aAAa,CAAC,CACtC,KAAK,CAAC,CAAC,KAAK,CAAC,CACb,UAAU,CAAC,CAAC,UAAU,CAAC,CACvB,MAAM,CAAC,CAAC,MAAM,CAAC,CACf,QAAQ,CAAC,CAAC,QAAQ,CAAC,CACnB,KAAK,CAAC,CAAC,KAAK,CAAC,CACb,IAAI,CAAC,CAAC,IAAI,CAAC,EAEb;MAAA,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAClD;IAAA,EAAE,UAAU,CAAC,CACd,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,SAAS,0BAA0B,CAAC,KAAc;IAChD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAClE,IAAI,UAAU,IAAI,KAAK,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI;QAAE,OAAO,OAAO,CAAC;IACnE,IAAI,SAAS,IAAI,KAAK,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI;QAAE,OAAO,QAAQ,CAAC;IAClE,IAAI,SAAS,IAAI,KAAK,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI;QAAE,OAAO,QAAQ,CAAC;IAClE,OAAO,SAAS,CAAC;AACnB,CAAC","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 {\n type ExplorerPageRequestSignature,\n shouldDispatchExplorerPageRequest,\n} from '../../application/shouldDispatchExplorerPageRequest';\nimport { shouldRequestExplorerPage } from '../../application/shouldRequestExplorerPage';\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 hasMore = false,\n loadingMore = false,\n pagingCollectionId,\n pagingRetryToken,\n onLoadMore,\n permissionStatus = 'granted',\n permissionText,\n onRequestPermission,\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 lastPageRequestRef = React.useRef<ExplorerPageRequestSignature | null>(null);\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 React.useEffect(() => {\n const request = {\n collectionId: pagingCollectionId,\n loadedItemCount: items.length,\n retryToken: pagingRetryToken,\n };\n if (\n !onLoadMore ||\n !shouldDispatchExplorerPageRequest(lastPageRequestRef.current, request) ||\n !shouldRequestExplorerPage(\n ids,\n visibleIds,\n hasMore,\n loadingMore,\n loading,\n errorText,\n permissionStatus,\n )\n ) {\n return;\n }\n lastPageRequestRef.current = request;\n onLoadMore({ loadedItemCount: items.length });\n }, [\n errorText,\n hasMore,\n ids,\n items.length,\n loading,\n loadingMore,\n onLoadMore,\n pagingCollectionId,\n pagingRetryToken,\n permissionStatus,\n visibleIds,\n ]);\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 (permissionStatus === 'denied' || permissionStatus === 'unavailable') {\n return (\n <NativeView testID={testID}>\n <Text>{permissionText ?? 'This collection is unavailable without permission.'}</Text>\n {permissionStatus === 'denied' && onRequestPermission ? (\n <NativePressable accessibilityRole=\"button\" onPress={onRequestPermission}>\n <Text>Grant access</Text>\n </NativePressable>\n ) : null}\n </NativeView>\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 <NativeView>\n {permissionStatus === 'limited' ? (\n <Text>{permissionText ?? 'Showing the media you have allowed.'}</Text>\n ) : null}\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 {loadingMore ? <Text>Loading more…</Text> : null}\n </NativeView>\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"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ExplorerPageRequestSignature {
|
|
2
|
+
readonly collectionId: string | undefined;
|
|
3
|
+
readonly loadedItemCount: number;
|
|
4
|
+
readonly retryToken: string | undefined;
|
|
5
|
+
}
|
|
6
|
+
/*** Preserves one request per provider page boundary until its collection, row count, or retry token changes. */
|
|
7
|
+
export declare function shouldDispatchExplorerPageRequest(previous: ExplorerPageRequestSignature | null, next: ExplorerPageRequestSignature): boolean;
|
|
8
|
+
//# sourceMappingURL=shouldDispatchExplorerPageRequest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shouldDispatchExplorerPageRequest.d.ts","sourceRoot":"","sources":["../../../../src/features/explorer/application/shouldDispatchExplorerPageRequest.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC;AAED,iHAAiH;AACjH,wBAAgB,iCAAiC,CAC/C,QAAQ,EAAE,4BAA4B,GAAG,IAAI,EAC7C,IAAI,EAAE,4BAA4B,GACjC,OAAO,CAOT"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*** Preserves one request per provider page boundary until its collection, row count, or retry token changes. */
|
|
2
|
+
export function shouldDispatchExplorerPageRequest(previous, next) {
|
|
3
|
+
if (previous === null)
|
|
4
|
+
return true;
|
|
5
|
+
return (previous.collectionId !== next.collectionId ||
|
|
6
|
+
previous.loadedItemCount !== next.loadedItemCount ||
|
|
7
|
+
previous.retryToken !== next.retryToken);
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=shouldDispatchExplorerPageRequest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shouldDispatchExplorerPageRequest.js","sourceRoot":"","sources":["../../../../src/features/explorer/application/shouldDispatchExplorerPageRequest.ts"],"names":[],"mappings":"AAMA,iHAAiH;AACjH,MAAM,UAAU,iCAAiC,CAC/C,QAA6C,EAC7C,IAAkC;IAElC,IAAI,QAAQ,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,CACL,QAAQ,CAAC,YAAY,KAAK,IAAI,CAAC,YAAY;QAC3C,QAAQ,CAAC,eAAe,KAAK,IAAI,CAAC,eAAe;QACjD,QAAQ,CAAC,UAAU,KAAK,IAAI,CAAC,UAAU,CACxC,CAAC;AACJ,CAAC","sourcesContent":["export interface ExplorerPageRequestSignature {\n readonly collectionId: string | undefined;\n readonly loadedItemCount: number;\n readonly retryToken: string | undefined;\n}\n\n/*** Preserves one request per provider page boundary until its collection, row count, or retry token changes. */\nexport function shouldDispatchExplorerPageRequest(\n previous: ExplorerPageRequestSignature | null,\n next: ExplorerPageRequestSignature,\n): boolean {\n if (previous === null) return true;\n return (\n previous.collectionId !== next.collectionId ||\n previous.loadedItemCount !== next.loadedItemCount ||\n previous.retryToken !== next.retryToken\n );\n}\n"]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ExplorerPermissionStatus } from '../../../types/explorer';
|
|
2
|
+
/*** Determines whether the mounted, accessible viewport has reached the provider-owned page boundary. */
|
|
3
|
+
export declare function shouldRequestExplorerPage(itemIds: readonly string[], visibleIds: readonly string[], hasMore: boolean, loadingMore: boolean, loading: boolean, errorText: string | undefined, permissionStatus: ExplorerPermissionStatus, threshold?: number): boolean;
|
|
4
|
+
//# sourceMappingURL=shouldRequestExplorerPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shouldRequestExplorerPage.d.ts","sourceRoot":"","sources":["../../../../src/features/explorer/application/shouldRequestExplorerPage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAExE,yGAAyG;AACzG,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,OAAO,EAAE,OAAO,EAChB,WAAW,EAAE,OAAO,EACpB,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,gBAAgB,EAAE,wBAAwB,EAC1C,SAAS,SAAK,GACb,OAAO,CAaT"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*** Determines whether the mounted, accessible viewport has reached the provider-owned page boundary. */
|
|
2
|
+
export function shouldRequestExplorerPage(itemIds, visibleIds, hasMore, loadingMore, loading, errorText, permissionStatus, threshold = 12) {
|
|
3
|
+
if (!hasMore ||
|
|
4
|
+
loadingMore ||
|
|
5
|
+
loading ||
|
|
6
|
+
errorText !== undefined ||
|
|
7
|
+
(permissionStatus !== 'granted' && permissionStatus !== 'limited') ||
|
|
8
|
+
visibleIds.length === 0) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
const firstPagingIndex = Math.max(0, itemIds.length - threshold);
|
|
12
|
+
return visibleIds.some((id) => itemIds.indexOf(id) >= firstPagingIndex);
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=shouldRequestExplorerPage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shouldRequestExplorerPage.js","sourceRoot":"","sources":["../../../../src/features/explorer/application/shouldRequestExplorerPage.ts"],"names":[],"mappings":"AAEA,yGAAyG;AACzG,MAAM,UAAU,yBAAyB,CACvC,OAA0B,EAC1B,UAA6B,EAC7B,OAAgB,EAChB,WAAoB,EACpB,OAAgB,EAChB,SAA6B,EAC7B,gBAA0C,EAC1C,SAAS,GAAG,EAAE;IAEd,IACE,CAAC,OAAO;QACR,WAAW;QACX,OAAO;QACP,SAAS,KAAK,SAAS;QACvB,CAAC,gBAAgB,KAAK,SAAS,IAAI,gBAAgB,KAAK,SAAS,CAAC;QAClE,UAAU,CAAC,MAAM,KAAK,CAAC,EACvB,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,gBAAgB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACjE,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,gBAAgB,CAAC,CAAC;AAC1E,CAAC","sourcesContent":["import type { ExplorerPermissionStatus } from '../../../types/explorer';\n\n/*** Determines whether the mounted, accessible viewport has reached the provider-owned page boundary. */\nexport function shouldRequestExplorerPage(\n itemIds: readonly string[],\n visibleIds: readonly string[],\n hasMore: boolean,\n loadingMore: boolean,\n loading: boolean,\n errorText: string | undefined,\n permissionStatus: ExplorerPermissionStatus,\n threshold = 12,\n): boolean {\n if (\n !hasMore ||\n loadingMore ||\n loading ||\n errorText !== undefined ||\n (permissionStatus !== 'granted' && permissionStatus !== 'limited') ||\n visibleIds.length === 0\n ) {\n return false;\n }\n const firstPagingIndex = Math.max(0, itemIds.length - threshold);\n return visibleIds.some((id) => itemIds.indexOf(id) >= firstPagingIndex);\n}\n"]}
|