@ankhorage/zora 22.1.3 → 23.0.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 +1 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/GraphNodeOverlay.d.ts +2 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/GraphNodeOverlay.d.ts.map +1 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/GraphNodeOverlay.js +2 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/GraphNodeOverlay.js.map +1 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.d.ts.map +1 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.js +1 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.js.map +1 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/bindGraphEvents.d.ts.map +1 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/bindGraphEvents.js +12 -4
- package/dist/features/graph-view/adapters/inbound/web-artifact/bindGraphEvents.js.map +1 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.d.ts +2 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.d.ts.map +1 -1
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.js +3 -3
- package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.js.map +1 -1
- package/dist/features/selection/adapters/inbound/SelectableItem.d.ts +1 -5
- package/dist/features/selection/adapters/inbound/SelectableItem.d.ts.map +1 -1
- package/dist/features/selection/adapters/inbound/SelectableItem.js +30 -54
- package/dist/features/selection/adapters/inbound/SelectableItem.js.map +1 -1
- package/dist/features/selection/adapters/inbound/SelectionProvider.d.ts +2 -6
- package/dist/features/selection/adapters/inbound/SelectionProvider.d.ts.map +1 -1
- package/dist/features/selection/adapters/inbound/SelectionProvider.js +36 -32
- package/dist/features/selection/adapters/inbound/SelectionProvider.js.map +1 -1
- package/dist/features/selection/application/resolveSelectionEventIntent.d.ts +5 -0
- package/dist/features/selection/application/resolveSelectionEventIntent.d.ts.map +1 -0
- package/dist/features/selection/application/resolveSelectionEventIntent.js +49 -0
- package/dist/features/selection/application/resolveSelectionEventIntent.js.map +1 -0
- package/dist/features/selection/public.d.ts +2 -0
- package/dist/features/selection/public.d.ts.map +1 -1
- package/dist/features/selection/public.js +1 -0
- package/dist/features/selection/public.js.map +1 -1
- package/dist/features/selection/utils/resolveSelectionNextIds.d.ts +4 -5
- package/dist/features/selection/utils/resolveSelectionNextIds.d.ts.map +1 -1
- package/dist/features/selection/utils/resolveSelectionNextIds.js +6 -13
- package/dist/features/selection/utils/resolveSelectionNextIds.js.map +1 -1
- package/dist/features/tree-view/adapters/inbound/TreeItem.d.ts +3 -2
- package/dist/features/tree-view/adapters/inbound/TreeItem.d.ts.map +1 -1
- package/dist/features/tree-view/adapters/inbound/TreeItem.js +11 -4
- package/dist/features/tree-view/adapters/inbound/TreeItem.js.map +1 -1
- package/dist/features/tree-view/adapters/inbound/TreeView.d.ts.map +1 -1
- package/dist/features/tree-view/adapters/inbound/TreeView.js +4 -5
- package/dist/features/tree-view/adapters/inbound/TreeView.js.map +1 -1
- package/dist/features/tree-view/adapters/inbound/web-artifact/TreeItemRow.d.ts +3 -2
- package/dist/features/tree-view/adapters/inbound/web-artifact/TreeItemRow.d.ts.map +1 -1
- package/dist/features/tree-view/adapters/inbound/web-artifact/TreeItemRow.js +17 -9
- package/dist/features/tree-view/adapters/inbound/web-artifact/TreeItemRow.js.map +1 -1
- package/dist/features/tree-view/adapters/inbound/web-artifact/TreeView.d.ts +4 -3
- package/dist/features/tree-view/adapters/inbound/web-artifact/TreeView.d.ts.map +1 -1
- package/dist/features/tree-view/adapters/inbound/web-artifact/TreeView.js +8 -8
- package/dist/features/tree-view/adapters/inbound/web-artifact/TreeView.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types/graph-view.d.ts +2 -0
- package/dist/types/graph-view.d.ts.map +1 -1
- package/dist/types/graph-view.js.map +1 -1
- package/dist/types/selection.d.ts +3 -0
- package/dist/types/selection.d.ts.map +1 -1
- package/dist/types/selection.js.map +1 -1
- package/dist/types/tree-view.d.ts +3 -2
- package/dist/types/tree-view.d.ts.map +1 -1
- package/dist/types/tree-view.js.map +1 -1
- package/package.json +4 -4
- package/src/features/graph-view/adapters/inbound/web-artifact/GraphNodeOverlay.tsx +10 -2
- package/src/features/graph-view/adapters/inbound/web-artifact/GraphView.tsx +3 -1
- package/src/features/graph-view/adapters/inbound/web-artifact/bindGraphEvents.ts +22 -5
- package/src/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.test.ts +21 -0
- package/src/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.ts +10 -3
- package/src/features/selection/adapters/inbound/SelectableItem.tsx +33 -50
- package/src/features/selection/adapters/inbound/SelectionProvider.tsx +34 -35
- package/src/features/selection/application/resolveSelectionEventIntent.test.ts +35 -0
- package/src/features/selection/application/resolveSelectionEventIntent.ts +55 -0
- package/src/features/selection/public.ts +2 -0
- package/src/features/selection/utils/resolveSelectionNextIds.test.ts +1 -14
- package/src/features/selection/utils/resolveSelectionNextIds.ts +6 -28
- package/src/features/tree-view/adapters/inbound/TreeItem.tsx +25 -9
- package/src/features/tree-view/adapters/inbound/TreeView.tsx +4 -6
- package/src/features/tree-view/adapters/inbound/web-artifact/TreeItemRow.tsx +26 -8
- package/src/features/tree-view/adapters/inbound/web-artifact/TreeView.test.tsx +77 -7
- package/src/features/tree-view/adapters/inbound/web-artifact/TreeView.tsx +11 -9
- package/src/index.ts +1 -0
- package/src/types/graph-view.ts +2 -0
- package/src/types/selection.ts +3 -0
- package/src/types/tree-view.ts +3 -2
- package/web-dist/chunks/{Accordion-j2aw3d5b.js → Accordion-01e56hb5.js} +1 -1
- package/web-dist/chunks/{Accordion-emczc5rg.js → Accordion-09se459d.js} +4 -4
- package/web-dist/chunks/{Accordion-784f23tq.js → Accordion-14p7zanv.js} +1 -1
- package/web-dist/chunks/{Accordion-cecn51ry.js → Accordion-17pht8jd.js} +3 -3
- package/web-dist/chunks/Accordion-5df6cgyf.js +55 -0
- package/web-dist/chunks/{Accordion-sr353w95.js → Accordion-78mj69zp.js} +2 -2
- package/web-dist/chunks/{Accordion-mpnb4qqw.js → Accordion-83hfn3y4.js} +5 -5
- package/web-dist/chunks/{Accordion-07rdw2vh.js → Accordion-89q14src.js} +2 -2
- package/web-dist/chunks/{Accordion-ajydqhjp.js → Accordion-8xk2zmr5.js} +2 -2
- package/web-dist/chunks/{Accordion-qvt6qdqm.js → Accordion-b3ymabze.js} +4 -4
- package/web-dist/chunks/{Accordion-avmgvcef.js → Accordion-cn8tbdvk.js} +3 -3
- package/web-dist/chunks/{Accordion-8af800ts.js → Accordion-czmnsspb.js} +5 -5
- package/web-dist/chunks/{Accordion-nc5gnvnb.js → Accordion-dp638q63.js} +2 -2
- package/web-dist/chunks/{Accordion-2fm2tm5r.js → Accordion-e633pb7z.js} +1 -1
- package/web-dist/chunks/{Accordion-yz7b7njb.js → Accordion-f1t3rzgg.js} +4 -4
- package/web-dist/chunks/{Accordion-b3qy1trj.js → Accordion-fm3jxs1f.js} +8 -8
- package/web-dist/chunks/{Accordion-zf3kzvef.js → Accordion-gj63vb6b.js} +4 -4
- package/web-dist/chunks/{Accordion-mvfz28m8.js → Accordion-gkzhd3zk.js} +3 -3
- package/web-dist/chunks/{Accordion-jj3bg8c1.js → Accordion-k7wwyx5x.js} +4 -4
- package/web-dist/chunks/{Accordion-rkv7jyaj.js → Accordion-kemavggr.js} +1 -1
- package/web-dist/chunks/{Accordion-jhwsvx3g.js → Accordion-kpnwdpb5.js} +1 -1
- package/web-dist/chunks/{Accordion-yf6vseep.js → Accordion-q0st659f.js} +82 -89
- package/web-dist/chunks/{Accordion-555gnfmw.js → Accordion-q165k3az.js} +6 -6
- package/web-dist/chunks/{Accordion-9rnepkvk.js → Accordion-q3z741vt.js} +3 -3
- package/web-dist/chunks/{Accordion-z585n0mr.js → Accordion-rjgdrn7y.js} +1 -1
- package/web-dist/chunks/{Accordion-vmz8y01b.js → Accordion-rspy7gjb.js} +6 -6
- package/web-dist/chunks/{Accordion-ycmg06sq.js → Accordion-rx2gzadh.js} +1 -1
- package/web-dist/chunks/{Accordion-45ej9brb.js → Accordion-swys7qts.js} +3 -3
- package/web-dist/chunks/{Accordion-v1dtv6v0.js → Accordion-w0k7gnn9.js} +3 -3
- package/web-dist/chunks/{Accordion-pnvt10cv.js → Accordion-w1afjp87.js} +4 -4
- package/web-dist/chunks/{Accordion-tj61sa5s.js → Accordion-w7jv9y5t.js} +4 -4
- package/web-dist/chunks/{Accordion-s1jhv8gk.js → Accordion-x13vca75.js} +1 -1
- package/web-dist/chunks/{Accordion-73m7z73v.js → Accordion-x1dg1f47.js} +5 -5
- package/web-dist/chunks/{Accordion-1cgz6f8t.js → Accordion-x4rjd7yn.js} +1 -1
- package/web-dist/chunks/{Accordion-nn38n7q4.js → Accordion-xf8cj8z4.js} +3 -3
- package/web-dist/chunks/{Accordion-8hhvtcxp.js → Accordion-xzd0tx46.js} +1 -1
- package/web-dist/components/accordion/Accordion.js +5 -5
- package/web-dist/components/accordion-item/AccordionItem.js +5 -5
- package/web-dist/components/activity-indicator/ActivityIndicator.js +1 -1
- package/web-dist/components/app-bar/AppBar.js +7 -7
- package/web-dist/components/app-shell/AppShell.js +2 -2
- package/web-dist/components/avatar/Avatar.js +5 -5
- package/web-dist/components/avatar-group/AvatarGroup.js +5 -5
- package/web-dist/components/badge/Badge.js +2 -2
- package/web-dist/components/barcode-scanner-view/BarcodeScannerView.js +10 -10
- package/web-dist/components/bottom-sheet/BottomSheet.js +3 -3
- package/web-dist/components/bottom-sheet-flat-list/BottomSheetFlatList.js +3 -3
- package/web-dist/components/bottom-sheet-scroll-view/BottomSheetScrollView.js +3 -3
- package/web-dist/components/bottom-sheet-section-list/BottomSheetSectionList.js +3 -3
- package/web-dist/components/bottom-sheet-view/BottomSheetView.js +3 -3
- package/web-dist/components/bottom-sheet-virtualized-list/BottomSheetVirtualizedList.js +3 -3
- package/web-dist/components/breadcrumbs/Breadcrumbs.js +5 -5
- package/web-dist/components/button/Button.js +3 -3
- package/web-dist/components/button-group/ButtonGroup.js +3 -3
- package/web-dist/components/camera-permission-view/CameraPermissionView.js +10 -10
- package/web-dist/components/card/Card.js +9 -9
- package/web-dist/components/card-back/CardBack.js +2 -2
- package/web-dist/components/card-hand/CardHand.js +2 -2
- package/web-dist/components/chat-list-item/ChatListItem.js +7 -7
- package/web-dist/components/checkbox/Checkbox.js +4 -4
- package/web-dist/components/checkbox-group/CheckboxGroup.js +4 -4
- package/web-dist/components/chess-board/ChessBoard.js +2 -2
- package/web-dist/components/chip/Chip.js +5 -5
- package/web-dist/components/chip-group/ChipGroup.js +5 -5
- package/web-dist/components/collection-editor/CollectionEditor.js +9 -9
- package/web-dist/components/content-rail/ContentRail.js +4 -4
- package/web-dist/components/data-table/DataTable.js +14 -14
- package/web-dist/components/date-picker/DatePicker.js +5 -5
- package/web-dist/components/dialog/Dialog.js +4 -4
- package/web-dist/components/divider/Divider.js +2 -2
- package/web-dist/components/empty-state/EmptyState.js +10 -10
- package/web-dist/components/field/Field.js +4 -4
- package/web-dist/components/flat-list/FlatList.js +5 -5
- package/web-dist/components/forgot-password-form/ForgotPasswordForm.js +8 -8
- package/web-dist/components/form/Form.js +6 -6
- package/web-dist/components/form-actions/FormActions.js +6 -6
- package/web-dist/components/form-error/FormError.js +6 -6
- package/web-dist/components/game/Game.js +3 -3
- package/web-dist/components/game-entity/GameEntity.js +3 -3
- package/web-dist/components/game-field/GameField.js +3 -3
- package/web-dist/components/game-input-zone/GameInputZone.js +3 -3
- package/web-dist/components/game-measurement-probe/GameMeasurementProbe.js +3 -3
- package/web-dist/components/game-overlay/GameOverlay.js +3 -3
- package/web-dist/components/gradient/Gradient.js +3 -3
- package/web-dist/components/gradient-renderer-provider/GradientRendererProvider.js +3 -3
- package/web-dist/components/graph-view/GraphView.js +20 -11
- package/web-dist/components/graph-view/GraphView.types-0.d.ts +2 -0
- package/web-dist/components/grid/Grid.js +2 -2
- package/web-dist/components/heading/Heading.js +2 -2
- package/web-dist/components/hero/Hero.js +9 -9
- package/web-dist/components/icon/Icon.js +2 -2
- package/web-dist/components/icon-button/IconButton.js +3 -3
- package/web-dist/components/image/Image.js +4 -4
- package/web-dist/components/keyboard-avoiding-view/KeyboardAvoidingView.js +1 -1
- package/web-dist/components/list/List.js +5 -5
- package/web-dist/components/list-item/ListItem.js +5 -5
- package/web-dist/components/list-section/ListSection.js +5 -5
- package/web-dist/components/media-card/MediaCard.js +9 -9
- package/web-dist/components/message-bubble/MessageBubble.js +7 -7
- package/web-dist/components/metric-card/MetricCard.js +9 -9
- package/web-dist/components/missing-element/MissingElement.js +4 -4
- package/web-dist/components/o-auth-provider-button/OAuthProviderButton.js +8 -8
- package/web-dist/components/o-auth-provider-list/OAuthProviderList.js +8 -8
- package/web-dist/components/opening-book/OpeningBook.js +2 -2
- package/web-dist/components/otp-form/OtpForm.js +8 -8
- package/web-dist/components/pagination/Pagination.js +4 -4
- package/web-dist/components/palette-item/PaletteItem.js +9 -9
- package/web-dist/components/playing-card/PlayingCard.js +2 -2
- package/web-dist/components/poker-training-table/PokerTrainingTable.js +2 -2
- package/web-dist/components/popover-menu/PopoverMenu.js +4 -4
- package/web-dist/components/post-card/PostCard.js +9 -9
- package/web-dist/components/product-card/ProductCard.js +9 -9
- package/web-dist/components/progress/Progress.js +4 -4
- package/web-dist/components/progress-ring/ProgressRing.js +4 -4
- package/web-dist/components/radio/Radio.js +5 -5
- package/web-dist/components/radio-group/RadioGroup.js +5 -5
- package/web-dist/components/rating/Rating.js +3 -3
- package/web-dist/components/reader-surface/ReaderSurface.js +9 -9
- package/web-dist/components/scan-overlay/ScanOverlay.js +10 -10
- package/web-dist/components/screen/Screen.js +2 -2
- package/web-dist/components/screen-section/ScreenSection.js +4 -4
- package/web-dist/components/scroll-view/ScrollView.js +2 -2
- package/web-dist/components/search-input/SearchInput.js +4 -4
- package/web-dist/components/section-header/SectionHeader.js +4 -4
- package/web-dist/components/section-list/SectionList.js +5 -5
- package/web-dist/components/select/Select.js +4 -4
- package/web-dist/components/selectable-item/SelectableItem.js +1 -1
- package/web-dist/components/selection-provider/SelectionProvider.js +1 -1
- package/web-dist/components/sign-in-form/SignInForm.js +8 -8
- package/web-dist/components/sign-up-form/SignUpForm.js +8 -8
- package/web-dist/components/skeleton/Skeleton.js +12 -12
- package/web-dist/components/skeleton-card/SkeletonCard.js +12 -12
- package/web-dist/components/skeleton-list/SkeletonList.js +12 -12
- package/web-dist/components/skeleton-text/SkeletonText.js +12 -12
- package/web-dist/components/surface/Surface.js +2 -2
- package/web-dist/components/switch/Switch.js +1 -1
- package/web-dist/components/tab/Tab.js +2 -2
- package/web-dist/components/tab-list/TabList.js +2 -2
- package/web-dist/components/tab-panel/TabPanel.js +2 -2
- package/web-dist/components/tabletop-table/TabletopTable.js +2 -2
- package/web-dist/components/tabs/Tabs.js +2 -2
- package/web-dist/components/text/Text.js +2 -2
- package/web-dist/components/text-input/TextInput.js +4 -4
- package/web-dist/components/time-picker/TimePicker.js +5 -5
- package/web-dist/components/timeline/Timeline.js +4 -4
- package/web-dist/components/toast/Toast.js +2 -2
- package/web-dist/components/toast-provider/ToastProvider.js +2 -2
- package/web-dist/components/toolbar/Toolbar.js +9 -9
- package/web-dist/components/tree-item/TreeItem.js +41 -22
- package/web-dist/components/tree-view/TreeView.d.ts +4 -3
- package/web-dist/components/tree-view/TreeView.js +25 -12
- package/web-dist/components/uploader/Uploader.js +10 -10
- package/web-dist/components/view/View.js +2 -2
- package/web-dist/manifest.json +577 -568
- package/web-dist/runtime/ZoraProvider.js +3 -3
- /package/web-dist/chunks/{Accordion-k3bc4kad.js → Accordion-13y3zx87.js} +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SelectionIntent } from '@ankhorage/utility/selection';
|
|
1
2
|
import React from 'react';
|
|
2
3
|
|
|
3
4
|
import type {
|
|
@@ -7,12 +8,17 @@ import type {
|
|
|
7
8
|
GraphViewRenderedNode,
|
|
8
9
|
GraphViewSize,
|
|
9
10
|
} from '../../../../../types/graph-view';
|
|
11
|
+
import { resolveSelectionEventIntent } from '../../../../selection/application/resolveSelectionEventIntent';
|
|
10
12
|
|
|
11
13
|
interface GraphNodeOverlayProps {
|
|
12
14
|
readonly nodes: readonly GraphViewNode[];
|
|
13
15
|
readonly renderedNodes: readonly GraphViewRenderedNode[];
|
|
14
16
|
readonly renderNode: (context: GraphViewNodeRenderContext) => React.ReactNode;
|
|
15
|
-
readonly onNodeEvent: (
|
|
17
|
+
readonly onNodeEvent: (
|
|
18
|
+
id: string,
|
|
19
|
+
type: GraphViewElementEventType,
|
|
20
|
+
selectionIntent?: SelectionIntent,
|
|
21
|
+
) => void;
|
|
16
22
|
readonly onNodeSize: (id: string, size: GraphViewSize) => void;
|
|
17
23
|
}
|
|
18
24
|
|
|
@@ -89,7 +95,9 @@ function MeasuredGraphNode(props: MeasuredGraphNodeProps) {
|
|
|
89
95
|
transform: `translate(-50%, -50%) scale(${renderedNode.zoom})`,
|
|
90
96
|
transformOrigin: 'center',
|
|
91
97
|
}}
|
|
92
|
-
|
|
98
|
+
onPointerUp={(event) =>
|
|
99
|
+
props.onNodeEvent(node.id, 'press', resolveSelectionEventIntent(event, 'pointer'))
|
|
100
|
+
}
|
|
93
101
|
onDoubleClick={() => props.onNodeEvent(node.id, 'double-press')}
|
|
94
102
|
onMouseEnter={() => props.onNodeEvent(node.id, 'pointer-enter')}
|
|
95
103
|
onMouseLeave={() => props.onNodeEvent(node.id, 'pointer-leave')}
|
|
@@ -48,7 +48,9 @@ export function GraphView(props: GraphViewProps) {
|
|
|
48
48
|
nodes={props.nodes}
|
|
49
49
|
renderedNodes={renderedNodes}
|
|
50
50
|
renderNode={props.renderNode}
|
|
51
|
-
onNodeEvent={(id, type) =>
|
|
51
|
+
onNodeEvent={(id, type, selectionIntent) =>
|
|
52
|
+
runtimeRef.current?.handleOverlayNodeEvent(id, type, selectionIntent)
|
|
53
|
+
}
|
|
52
54
|
onNodeSize={(id, size) => runtimeRef.current?.setNodeSize(id, size)}
|
|
53
55
|
/>
|
|
54
56
|
) : null}
|
|
@@ -3,8 +3,10 @@ import type { Core, EventObject } from 'cytoscape';
|
|
|
3
3
|
import type {
|
|
4
4
|
GraphViewCallbacks,
|
|
5
5
|
GraphViewController,
|
|
6
|
+
GraphViewElementEvent,
|
|
6
7
|
GraphViewElementEventType,
|
|
7
8
|
} from '../../../../../types/graph-view';
|
|
9
|
+
import { resolveSelectionEventIntent } from '../../../../selection/application/resolveSelectionEventIntent';
|
|
8
10
|
|
|
9
11
|
/*** Bind stable Cytoscape events that always read the latest React callbacks. */
|
|
10
12
|
export function bindGraphEvents(
|
|
@@ -46,10 +48,11 @@ function bindElementEvents(
|
|
|
46
48
|
const handler = (event: EventObject) => {
|
|
47
49
|
const target: unknown = event.target;
|
|
48
50
|
if (!isGraphEventTarget(target)) return;
|
|
49
|
-
emitElementEvent(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
emitElementEvent(
|
|
52
|
+
callbacksRef.current,
|
|
53
|
+
selector,
|
|
54
|
+
createElementEvent(event, target.id(), type),
|
|
55
|
+
);
|
|
53
56
|
onRuntimeChange();
|
|
54
57
|
};
|
|
55
58
|
cy.on(cytoscapeEvent, selector, handler);
|
|
@@ -57,6 +60,20 @@ function bindElementEvents(
|
|
|
57
60
|
});
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
/*** Translate one renderer event into the engine-neutral public event contract. */
|
|
64
|
+
function createElementEvent(
|
|
65
|
+
event: EventObject,
|
|
66
|
+
id: string,
|
|
67
|
+
type: GraphViewElementEventType,
|
|
68
|
+
): GraphViewElementEvent {
|
|
69
|
+
if (type !== 'press') return { id, type };
|
|
70
|
+
return {
|
|
71
|
+
id,
|
|
72
|
+
type,
|
|
73
|
+
selectionIntent: resolveSelectionEventIntent(event, 'pointer'),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
60
77
|
/*** Return whether a Cytoscape event target exposes the element identifier contract. */
|
|
61
78
|
function isGraphEventTarget(value: unknown): value is GraphEventTarget {
|
|
62
79
|
return (
|
|
@@ -72,7 +89,7 @@ interface GraphEventTarget {
|
|
|
72
89
|
function emitElementEvent(
|
|
73
90
|
callbacks: GraphViewCallbacks,
|
|
74
91
|
selector: ElementSelector,
|
|
75
|
-
event:
|
|
92
|
+
event: GraphViewElementEvent,
|
|
76
93
|
) {
|
|
77
94
|
if (selector === 'node') {
|
|
78
95
|
callbacks.onNodeEvent?.(event);
|
|
@@ -172,6 +172,27 @@ test('preserves selection and positions through hover presentation updates witho
|
|
|
172
172
|
}
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
+
test('forwards semantic selection intent from rich-node press events', async () => {
|
|
176
|
+
const ready = Promise.withResolvers<GraphViewController>();
|
|
177
|
+
const events: GraphViewElementEvent[] = [];
|
|
178
|
+
const runtime = createGraphRuntime(undefined, {
|
|
179
|
+
current: { onReady: ready.resolve, onNodeEvent: (event) => events.push(event) },
|
|
180
|
+
});
|
|
181
|
+
try {
|
|
182
|
+
runtime.update({
|
|
183
|
+
nodes: [{ id: 'a' }],
|
|
184
|
+
edges: [],
|
|
185
|
+
layout: 'grid',
|
|
186
|
+
richNodeRendering: false,
|
|
187
|
+
});
|
|
188
|
+
await ready.promise;
|
|
189
|
+
runtime.handleOverlayNodeEvent('a', 'press', 'toggle');
|
|
190
|
+
expect(events.at(-1)).toEqual({ id: 'a', type: 'press', selectionIntent: 'toggle' });
|
|
191
|
+
} finally {
|
|
192
|
+
runtime.destroy();
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
175
196
|
test('settles the pending initial layout when presentation changes before readiness', async () => {
|
|
176
197
|
const ready = Promise.withResolvers<GraphViewController>();
|
|
177
198
|
const calls = { ready: 0, sort: 0 };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SelectionIntent } from '@ankhorage/utility/selection';
|
|
1
2
|
import cytoscape, { type Core, type CytoscapeOptions } from 'cytoscape';
|
|
2
3
|
|
|
3
4
|
import type {
|
|
@@ -30,7 +31,11 @@ type RenderedNodeListener = (nodes: readonly GraphViewRenderedNode[]) => void;
|
|
|
30
31
|
|
|
31
32
|
export interface GraphRuntime {
|
|
32
33
|
destroy(): void;
|
|
33
|
-
handleOverlayNodeEvent(
|
|
34
|
+
handleOverlayNodeEvent(
|
|
35
|
+
id: string,
|
|
36
|
+
type: GraphViewElementEventType,
|
|
37
|
+
selectionIntent?: SelectionIntent,
|
|
38
|
+
): void;
|
|
34
39
|
setNodeSize(id: string, size: GraphViewSize): void;
|
|
35
40
|
setSelectedNodeIds(nodeIds: readonly string[] | undefined): void;
|
|
36
41
|
subscribeRenderedNodes(listener: RenderedNodeListener): () => void;
|
|
@@ -80,7 +85,8 @@ export function createGraphRuntime(
|
|
|
80
85
|
|
|
81
86
|
return {
|
|
82
87
|
destroy: () => destroyRuntime(state),
|
|
83
|
-
handleOverlayNodeEvent: (id, type) =>
|
|
88
|
+
handleOverlayNodeEvent: (id, type, selectionIntent) =>
|
|
89
|
+
handleOverlayNodeEvent(state, id, type, selectionIntent),
|
|
84
90
|
setNodeSize: (id, size) => setNodeSize(state, id, size),
|
|
85
91
|
setSelectedNodeIds: (nodeIds) => setSelectedNodeIds(state, nodeIds),
|
|
86
92
|
subscribeRenderedNodes: (listener) => subscribeRenderedNodes(state, listener),
|
|
@@ -401,11 +407,12 @@ function handleOverlayNodeEvent(
|
|
|
401
407
|
state: GraphRuntimeState,
|
|
402
408
|
id: string,
|
|
403
409
|
type: GraphViewElementEventType,
|
|
410
|
+
selectionIntent?: SelectionIntent,
|
|
404
411
|
) {
|
|
405
412
|
if (type === 'pointer-enter') state.hoveredNodeIds.add(id);
|
|
406
413
|
if (type === 'pointer-leave') state.hoveredNodeIds.delete(id);
|
|
407
414
|
if (type === 'press') state.cy.getElementById(id).select();
|
|
408
|
-
state.callbacksRef.current.onNodeEvent?.({ id, type });
|
|
415
|
+
state.callbacksRef.current.onNodeEvent?.({ id, type, selectionIntent });
|
|
409
416
|
emitRenderedNodes(state);
|
|
410
417
|
}
|
|
411
418
|
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
import { Pressable } from '@ankhorage/surface';
|
|
2
|
+
import type { SelectionIntent } from '@ankhorage/utility/selection';
|
|
2
3
|
import React from 'react';
|
|
3
|
-
import type
|
|
4
|
+
import { type GestureResponderEvent, Platform } from 'react-native';
|
|
4
5
|
|
|
5
6
|
import type {
|
|
6
7
|
SelectableItemProps,
|
|
7
8
|
SelectableItemState,
|
|
8
9
|
SelectionTrigger,
|
|
9
10
|
} from '../../../../types/selection';
|
|
11
|
+
import { resolveSelectionEventIntent } from '../../application/resolveSelectionEventIntent';
|
|
10
12
|
import { useSelection } from './SelectionProvider';
|
|
11
13
|
|
|
14
|
+
/*** Resolve the configured selection trigger. */
|
|
12
15
|
function resolveTrigger(trigger: SelectionTrigger | undefined): SelectionTrigger {
|
|
13
16
|
return trigger ?? 'manual';
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
/*** Narrow render-prop children without changing ordinary React-node children. */
|
|
16
20
|
function isRenderProp(
|
|
17
21
|
children: SelectableItemProps['children'],
|
|
18
22
|
): children is (state: SelectableItemState) => React.ReactNode {
|
|
19
23
|
return typeof children === 'function';
|
|
20
24
|
}
|
|
21
25
|
|
|
22
|
-
/***
|
|
23
|
-
* Adds selection behavior to arbitrary child content via render props.
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
*/
|
|
26
|
+
/*** Adds selection behavior to arbitrary child content via render props. */
|
|
27
27
|
export function SelectableItem({
|
|
28
28
|
id,
|
|
29
29
|
trigger,
|
|
@@ -36,67 +36,50 @@ export function SelectableItem({
|
|
|
36
36
|
const resolvedDisabled = selection.disabled || disabled;
|
|
37
37
|
const selected = selection.isSelected(id);
|
|
38
38
|
|
|
39
|
+
const activate = React.useCallback(
|
|
40
|
+
(intent: SelectionIntent) => {
|
|
41
|
+
if (resolvedDisabled || interactionPolicy === 'passive') return;
|
|
42
|
+
selection.activate(id, intent);
|
|
43
|
+
},
|
|
44
|
+
[id, interactionPolicy, resolvedDisabled, selection],
|
|
45
|
+
);
|
|
39
46
|
const select = React.useCallback(() => {
|
|
40
|
-
if (resolvedDisabled) return;
|
|
41
|
-
if (interactionPolicy === 'passive') return;
|
|
47
|
+
if (resolvedDisabled || interactionPolicy === 'passive') return;
|
|
42
48
|
selection.select(id);
|
|
43
|
-
}, [id,
|
|
44
|
-
|
|
49
|
+
}, [id, interactionPolicy, resolvedDisabled, selection]);
|
|
45
50
|
const toggle = React.useCallback(() => {
|
|
46
|
-
if (resolvedDisabled) return;
|
|
47
|
-
if (interactionPolicy === 'passive') return;
|
|
51
|
+
if (resolvedDisabled || interactionPolicy === 'passive') return;
|
|
48
52
|
selection.toggle(id);
|
|
49
|
-
}, [id,
|
|
50
|
-
|
|
53
|
+
}, [id, interactionPolicy, resolvedDisabled, selection]);
|
|
51
54
|
const clear = React.useCallback(() => {
|
|
52
|
-
if (selection.disabled) return;
|
|
53
|
-
if (interactionPolicy === 'passive') return;
|
|
55
|
+
if (selection.disabled || interactionPolicy === 'passive') return;
|
|
54
56
|
selection.clear();
|
|
55
57
|
}, [interactionPolicy, selection]);
|
|
56
58
|
|
|
57
|
-
const itemState = React.useMemo<SelectableItemState>(
|
|
58
|
-
|
|
59
|
+
const itemState = React.useMemo<SelectableItemState>(
|
|
60
|
+
() => ({
|
|
59
61
|
id,
|
|
60
62
|
selected,
|
|
61
63
|
disabled: resolvedDisabled,
|
|
62
64
|
mode: selection.mode,
|
|
65
|
+
activate,
|
|
63
66
|
select,
|
|
64
67
|
toggle,
|
|
65
68
|
clear,
|
|
66
|
-
}
|
|
67
|
-
|
|
69
|
+
}),
|
|
70
|
+
[activate, clear, id, resolvedDisabled, select, selected, selection.mode, toggle],
|
|
71
|
+
);
|
|
68
72
|
|
|
69
|
-
// IMPORTANT:
|
|
70
|
-
// Do not pass `children` directly into Pressable. Pressable also supports function children,
|
|
71
|
-
// but its function signature receives interaction state, not SelectableItemState.
|
|
72
73
|
const content = isRenderProp(children) ? children(itemState) : children;
|
|
74
|
+
if (resolvedTrigger === 'manual') return <>{content}</>;
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
return <>{content}</>;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const handlePress = (event: GestureResponderEvent) => {
|
|
76
|
+
const handleActivation = (event: GestureResponderEvent) => {
|
|
79
77
|
event.stopPropagation();
|
|
80
|
-
if (resolvedDisabled) return;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
selection.toggle(id);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const handleLongPress = (event: GestureResponderEvent) => {
|
|
91
|
-
event.stopPropagation();
|
|
92
|
-
if (resolvedDisabled) return;
|
|
93
|
-
if (interactionPolicy === 'passive') return;
|
|
94
|
-
if (selection.mode === 'single') {
|
|
95
|
-
selection.select(id);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
selection.toggle(id);
|
|
78
|
+
if (resolvedDisabled || interactionPolicy === 'passive') return;
|
|
79
|
+
selection.activate(
|
|
80
|
+
id,
|
|
81
|
+
resolveSelectionEventIntent(event, Platform.OS === 'web' ? 'pointer' : 'touch'),
|
|
82
|
+
);
|
|
100
83
|
};
|
|
101
84
|
|
|
102
85
|
return (
|
|
@@ -105,8 +88,8 @@ export function SelectableItem({
|
|
|
105
88
|
accessibilityRole="button"
|
|
106
89
|
accessibilityState={{ disabled: resolvedDisabled, selected }}
|
|
107
90
|
disabled={resolvedDisabled}
|
|
108
|
-
onLongPress={resolvedTrigger === 'longPress' ?
|
|
109
|
-
onPress={resolvedTrigger === 'press' ?
|
|
91
|
+
onLongPress={resolvedTrigger === 'longPress' ? handleActivation : undefined}
|
|
92
|
+
onPress={resolvedTrigger === 'press' ? handleActivation : undefined}
|
|
110
93
|
>
|
|
111
94
|
{content}
|
|
112
95
|
</Pressable>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applySelectionIntent, type SelectionIntent } from '@ankhorage/utility/selection';
|
|
1
2
|
import React from 'react';
|
|
2
3
|
|
|
3
4
|
import type {
|
|
@@ -5,13 +6,7 @@ import type {
|
|
|
5
6
|
SelectionProviderProps,
|
|
6
7
|
UseSelectionResult,
|
|
7
8
|
} from '../../../../types/selection';
|
|
8
|
-
import {
|
|
9
|
-
areIdsEqual,
|
|
10
|
-
clearIds,
|
|
11
|
-
normalizeIds,
|
|
12
|
-
selectId,
|
|
13
|
-
toggleId,
|
|
14
|
-
} from '../../utils/resolveSelectionNextIds';
|
|
9
|
+
import { areIdsEqual, clearIds, normalizeIds, selectId } from '../../utils/resolveSelectionNextIds';
|
|
15
10
|
|
|
16
11
|
const MISSING_CONTEXT_MESSAGE =
|
|
17
12
|
'ZORA selection context is missing. Wrap this tree in <SelectionProvider>.';
|
|
@@ -30,21 +25,14 @@ function resolveDisabled(disabled: boolean | undefined): boolean {
|
|
|
30
25
|
return disabled ?? false;
|
|
31
26
|
}
|
|
32
27
|
|
|
33
|
-
/***
|
|
34
|
-
* Accesses selection state provided by `SelectionProvider`.
|
|
35
|
-
*/
|
|
28
|
+
/*** Accesses selection state provided by `SelectionProvider`. */
|
|
36
29
|
export function useSelection(): UseSelectionResult {
|
|
37
30
|
const value = React.use(SelectionContext);
|
|
38
|
-
if (!value)
|
|
39
|
-
throw new Error(MISSING_CONTEXT_MESSAGE);
|
|
40
|
-
}
|
|
41
|
-
|
|
31
|
+
if (!value) throw new Error(MISSING_CONTEXT_MESSAGE);
|
|
42
32
|
return value;
|
|
43
33
|
}
|
|
44
34
|
|
|
45
|
-
/***
|
|
46
|
-
* Provides selection state for building selectable lists and grids.
|
|
47
|
-
*/
|
|
35
|
+
/*** Provides selection state for building selectable lists and grids. */
|
|
48
36
|
export function SelectionProvider({
|
|
49
37
|
children,
|
|
50
38
|
selectedIds,
|
|
@@ -57,27 +45,20 @@ export function SelectionProvider({
|
|
|
57
45
|
const resolvedMode = resolveMode(mode);
|
|
58
46
|
const resolvedDisabled = resolveDisabled(disabled);
|
|
59
47
|
const isControlled = selectedIds !== undefined;
|
|
60
|
-
|
|
61
48
|
const [uncontrolledIds, setUncontrolledIds] = React.useState<readonly string[]>(
|
|
62
49
|
defaultSelectedIds ?? [],
|
|
63
50
|
);
|
|
64
51
|
|
|
65
52
|
const rawIds = isControlled ? selectedIds : uncontrolledIds;
|
|
66
53
|
const currentNormalizedIds = normalizeIds(rawIds, resolvedMode);
|
|
67
|
-
|
|
68
54
|
const selectedIdSet = React.useMemo(() => new Set(currentNormalizedIds), [currentNormalizedIds]);
|
|
69
55
|
|
|
70
56
|
const commitSelectionChange = React.useCallback(
|
|
71
57
|
(nextNormalizedIds: readonly string[]) => {
|
|
72
|
-
if (resolvedDisabled) return;
|
|
73
|
-
if (interactionPolicy === 'passive') return;
|
|
58
|
+
if (resolvedDisabled || interactionPolicy === 'passive') return;
|
|
74
59
|
if (areIdsEqual(nextNormalizedIds, currentNormalizedIds)) return;
|
|
75
|
-
|
|
76
60
|
onSelectionChange?.(nextNormalizedIds);
|
|
77
|
-
|
|
78
|
-
if (!isControlled) {
|
|
79
|
-
setUncontrolledIds(nextNormalizedIds);
|
|
80
|
-
}
|
|
61
|
+
if (!isControlled) setUncontrolledIds(nextNormalizedIds);
|
|
81
62
|
},
|
|
82
63
|
[currentNormalizedIds, interactionPolicy, isControlled, onSelectionChange, resolvedDisabled],
|
|
83
64
|
);
|
|
@@ -89,34 +70,52 @@ export function SelectionProvider({
|
|
|
89
70
|
const select = React.useCallback(
|
|
90
71
|
(id: string) => {
|
|
91
72
|
const nextIds = selectId({ mode: resolvedMode, ids: currentNormalizedIds, id });
|
|
92
|
-
|
|
93
|
-
commitSelectionChange(nextNormalizedIds);
|
|
73
|
+
commitSelectionChange(normalizeIds(nextIds, resolvedMode));
|
|
94
74
|
},
|
|
95
75
|
[commitSelectionChange, currentNormalizedIds, resolvedMode],
|
|
96
76
|
);
|
|
97
77
|
|
|
98
78
|
const toggle = React.useCallback(
|
|
99
79
|
(id: string) => {
|
|
100
|
-
const nextIds =
|
|
101
|
-
|
|
102
|
-
commitSelectionChange(nextNormalizedIds);
|
|
80
|
+
const nextIds = applySelectionIntent(currentNormalizedIds, id, 'toggle');
|
|
81
|
+
commitSelectionChange(normalizeIds(nextIds, resolvedMode));
|
|
103
82
|
},
|
|
104
83
|
[commitSelectionChange, currentNormalizedIds, resolvedMode],
|
|
105
84
|
);
|
|
106
85
|
|
|
107
|
-
const
|
|
108
|
-
|
|
86
|
+
const activate = React.useCallback(
|
|
87
|
+
(id: string, intent: SelectionIntent) => {
|
|
88
|
+
const effectiveIntent = resolvedMode === 'single' ? 'replace' : intent;
|
|
89
|
+
const nextIds = applySelectionIntent(currentNormalizedIds, id, effectiveIntent);
|
|
90
|
+
commitSelectionChange(normalizeIds(nextIds, resolvedMode));
|
|
91
|
+
},
|
|
92
|
+
[commitSelectionChange, currentNormalizedIds, resolvedMode],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const value = React.useMemo<UseSelectionResult>(
|
|
96
|
+
() => ({
|
|
109
97
|
mode: resolvedMode,
|
|
110
98
|
disabled: resolvedDisabled,
|
|
111
99
|
selectedIds: currentNormalizedIds,
|
|
112
100
|
selectedCount: currentNormalizedIds.length,
|
|
113
101
|
hasSelection: currentNormalizedIds.length > 0,
|
|
114
102
|
isSelected: (id: string) => selectedIdSet.has(id),
|
|
103
|
+
activate,
|
|
115
104
|
select,
|
|
116
105
|
toggle,
|
|
117
106
|
clear,
|
|
118
|
-
}
|
|
119
|
-
|
|
107
|
+
}),
|
|
108
|
+
[
|
|
109
|
+
activate,
|
|
110
|
+
clear,
|
|
111
|
+
currentNormalizedIds,
|
|
112
|
+
resolvedDisabled,
|
|
113
|
+
resolvedMode,
|
|
114
|
+
select,
|
|
115
|
+
selectedIdSet,
|
|
116
|
+
toggle,
|
|
117
|
+
],
|
|
118
|
+
);
|
|
120
119
|
|
|
121
120
|
return <SelectionContext value={value}>{children}</SelectionContext>;
|
|
122
121
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { resolveSelectionEventIntent } from './resolveSelectionEventIntent';
|
|
4
|
+
|
|
5
|
+
describe('resolveSelectionEventIntent', () => {
|
|
6
|
+
test('replaces for an unmodified desktop pointer event', () => {
|
|
7
|
+
expect(resolveSelectionEventIntent({}, 'pointer')).toBe('replace');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('toggles for Command and Control modifiers', () => {
|
|
11
|
+
expect(resolveSelectionEventIntent({ metaKey: true }, 'pointer')).toBe('toggle');
|
|
12
|
+
expect(resolveSelectionEventIntent({ nativeEvent: { ctrlKey: true } }, 'pointer')).toBe(
|
|
13
|
+
'toggle',
|
|
14
|
+
);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('toggles pointer events reported as touch', () => {
|
|
18
|
+
expect(
|
|
19
|
+
resolveSelectionEventIntent({ originalEvent: { pointerType: 'touch' } }, 'pointer'),
|
|
20
|
+
).toBe('toggle');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('toggles renderer touch events without pointerType', () => {
|
|
24
|
+
expect(
|
|
25
|
+
resolveSelectionEventIntent(
|
|
26
|
+
{ originalEvent: { type: 'touchend', changedTouches: [] } },
|
|
27
|
+
'pointer',
|
|
28
|
+
),
|
|
29
|
+
).toBe('toggle');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('toggles native touch fallback without modifier data', () => {
|
|
33
|
+
expect(resolveSelectionEventIntent({}, 'touch')).toBe('toggle');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type InteractionModifiers,
|
|
3
|
+
resolveSelectionIntent,
|
|
4
|
+
type SelectionActivationKind,
|
|
5
|
+
type SelectionInteractionInput,
|
|
6
|
+
} from '@ankhorage/utility/interaction';
|
|
7
|
+
import { isRecord } from '@ankhorage/utility/object';
|
|
8
|
+
import type { SelectionIntent } from '@ankhorage/utility/selection';
|
|
9
|
+
|
|
10
|
+
/*** Normalize DOM, React Native, and renderer events into one semantic selection intent. */
|
|
11
|
+
export function resolveSelectionEventIntent(
|
|
12
|
+
event: unknown,
|
|
13
|
+
fallbackKind: SelectionActivationKind,
|
|
14
|
+
): SelectionIntent {
|
|
15
|
+
const source = readEventSource(event);
|
|
16
|
+
const kind: SelectionActivationKind = isTouchActivation(source) ? 'touch' : fallbackKind;
|
|
17
|
+
const sourceModifiers = readInteractionModifiers(source);
|
|
18
|
+
const wrapperModifiers = readInteractionModifiers(event);
|
|
19
|
+
const input: SelectionInteractionInput = {
|
|
20
|
+
kind,
|
|
21
|
+
modifiers: {
|
|
22
|
+
altKey: sourceModifiers.altKey === true || wrapperModifiers.altKey === true,
|
|
23
|
+
ctrlKey: sourceModifiers.ctrlKey === true || wrapperModifiers.ctrlKey === true,
|
|
24
|
+
metaKey: sourceModifiers.metaKey === true || wrapperModifiers.metaKey === true,
|
|
25
|
+
shiftKey: sourceModifiers.shiftKey === true || wrapperModifiers.shiftKey === true,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
return resolveSelectionIntent(input);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/*** Prefer one nested native/renderer event while preserving structural event compatibility. */
|
|
32
|
+
function readEventSource(event: unknown): Readonly<Record<string, unknown>> {
|
|
33
|
+
if (!isRecord(event)) return {};
|
|
34
|
+
if (isRecord(event.originalEvent)) return event.originalEvent;
|
|
35
|
+
if (isRecord(event.nativeEvent)) return event.nativeEvent;
|
|
36
|
+
return event;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/*** Recognize pointer and native/browser touch event shapes without depending on platform event types. */
|
|
40
|
+
function isTouchActivation(source: Readonly<Record<string, unknown>>): boolean {
|
|
41
|
+
if (source.pointerType === 'touch') return true;
|
|
42
|
+
if (typeof source.type === 'string' && source.type.startsWith('touch')) return true;
|
|
43
|
+
return 'touches' in source || 'changedTouches' in source;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/*** Read known modifiers explicitly so arbitrary event properties never become dynamic input keys. */
|
|
47
|
+
function readInteractionModifiers(value: unknown): InteractionModifiers {
|
|
48
|
+
if (!isRecord(value)) return {};
|
|
49
|
+
return {
|
|
50
|
+
altKey: value.altKey === true,
|
|
51
|
+
ctrlKey: value.ctrlKey === true,
|
|
52
|
+
metaKey: value.metaKey === true,
|
|
53
|
+
shiftKey: value.shiftKey === true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -8,3 +8,5 @@ export type {
|
|
|
8
8
|
} from '../../types/selection';
|
|
9
9
|
export { SelectableItem } from './adapters/inbound/SelectableItem';
|
|
10
10
|
export { SelectionProvider, useSelection } from './adapters/inbound/SelectionProvider';
|
|
11
|
+
export { resolveSelectionEventIntent } from './application/resolveSelectionEventIntent';
|
|
12
|
+
export type { SelectionIntent } from '@ankhorage/utility/selection';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
2
|
|
|
3
3
|
import type { SelectionMode } from '../../../types/selection';
|
|
4
|
-
import { areIdsEqual, clearIds, normalizeIds, selectId
|
|
4
|
+
import { areIdsEqual, clearIds, normalizeIds, selectId } from './resolveSelectionNextIds';
|
|
5
5
|
|
|
6
6
|
describe('resolveSelectionNextIds', () => {
|
|
7
7
|
test('normalizeIds removes duplicates, preserves order', () => {
|
|
@@ -40,19 +40,6 @@ describe('resolveSelectionNextIds', () => {
|
|
|
40
40
|
expect(selectId({ mode: 'multi', ids, id: 'b' })).toBe(ids);
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
test('toggleId clears id when already selected (single mode)', () => {
|
|
44
|
-
expect(toggleId({ mode: 'single', ids: ['a'], id: 'a' })).toEqual([]);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('toggleId selects id when not selected (single mode)', () => {
|
|
48
|
-
expect(toggleId({ mode: 'single', ids: [], id: 'a' })).toEqual(['a']);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('toggleId toggles membership (multi mode)', () => {
|
|
52
|
-
expect(toggleId({ mode: 'multi', ids: ['a'], id: 'b' })).toEqual(['a', 'b']);
|
|
53
|
-
expect(toggleId({ mode: 'multi', ids: ['a', 'b'], id: 'b' })).toEqual(['a']);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
43
|
test('mode-change normalization example', () => {
|
|
57
44
|
const internalIds = ['a', 'b'];
|
|
58
45
|
const mode: SelectionMode = 'single';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SelectionMode } from '../../../types/selection';
|
|
2
2
|
|
|
3
|
+
/*** Normalize selected ids to stable unique membership within the configured selection mode. */
|
|
3
4
|
export function normalizeIds(
|
|
4
5
|
ids: readonly string[] | undefined,
|
|
5
6
|
mode: SelectionMode,
|
|
@@ -17,6 +18,7 @@ export function normalizeIds(
|
|
|
17
18
|
return uniqueIds;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/*** Compare ordered selection ids without allocating replacement arrays. */
|
|
20
22
|
export function areIdsEqual(a: readonly string[], b: readonly string[]): boolean {
|
|
21
23
|
if (a.length !== b.length) return false;
|
|
22
24
|
for (let index = 0; index < a.length; index += 1) {
|
|
@@ -26,10 +28,12 @@ export function areIdsEqual(a: readonly string[], b: readonly string[]): boolean
|
|
|
26
28
|
return true;
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
/*** Produce the canonical empty selection. */
|
|
29
32
|
export function clearIds(): readonly string[] {
|
|
30
33
|
return [];
|
|
31
34
|
}
|
|
32
35
|
|
|
36
|
+
/*** Ensure one id is selected while preserving multi-selection membership. */
|
|
33
37
|
export function selectId({
|
|
34
38
|
mode,
|
|
35
39
|
ids,
|
|
@@ -39,33 +43,7 @@ export function selectId({
|
|
|
39
43
|
ids: readonly string[];
|
|
40
44
|
id: string;
|
|
41
45
|
}): readonly string[] {
|
|
42
|
-
if (mode === 'single')
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (ids.includes(id)) {
|
|
47
|
-
return ids;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return [...ids, id];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function toggleId({
|
|
54
|
-
mode,
|
|
55
|
-
ids,
|
|
56
|
-
id,
|
|
57
|
-
}: {
|
|
58
|
-
mode: SelectionMode;
|
|
59
|
-
ids: readonly string[];
|
|
60
|
-
id: string;
|
|
61
|
-
}): readonly string[] {
|
|
62
|
-
if (ids.includes(id)) {
|
|
63
|
-
return ids.filter((existingId) => existingId !== id);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (mode === 'single') {
|
|
67
|
-
return [id];
|
|
68
|
-
}
|
|
69
|
-
|
|
46
|
+
if (mode === 'single') return [id];
|
|
47
|
+
if (ids.includes(id)) return ids;
|
|
70
48
|
return [...ids, id];
|
|
71
49
|
}
|