@duro-app/ui 5.18.0 → 5.20.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/dist/{Table-DnaoH-h5.js → Table-Cw1B14R3.js} +4 -2
- package/dist/Table-Cw1B14R3.js.map +1 -0
- package/dist/components/AppShell/AppShell.d.ts.map +1 -1
- package/dist/components/AppShell/AppShell.stories.d.ts +5 -0
- package/dist/components/AppShell/AppShell.stories.d.ts.map +1 -1
- package/dist/components/Menu/native.css.d.ts +1 -0
- package/dist/components/Menu/native.css.d.ts.map +1 -1
- package/dist/components/Popover/native.css.d.ts +1 -0
- package/dist/components/Popover/native.css.d.ts.map +1 -1
- package/dist/components/SideNav/SideNav.d.ts +2 -1
- package/dist/components/SideNav/SideNav.d.ts.map +1 -1
- package/dist/components/SideNav/SideNav.meta.d.ts.map +1 -1
- package/dist/components/TextLink/native.css.d.ts +19 -0
- package/dist/components/TextLink/native.css.d.ts.map +1 -0
- package/dist/components/Tree/Tree.d.ts +12 -22
- package/dist/components/Tree/Tree.d.ts.map +1 -1
- package/dist/components/Tree/Tree.stories.d.ts +33 -0
- package/dist/components/Tree/Tree.stories.d.ts.map +1 -1
- package/dist/components/Tree/TreeContext.d.ts +5 -5
- package/dist/components/Tree/TreeContext.d.ts.map +1 -1
- package/dist/components/Tree/TreeFocusStore.d.ts +25 -0
- package/dist/components/Tree/TreeFocusStore.d.ts.map +1 -0
- package/dist/components/Tree/TreeItemRow.d.ts +25 -0
- package/dist/components/Tree/TreeItemRow.d.ts.map +1 -0
- package/dist/components/Tree/TreeRow.d.ts +3 -0
- package/dist/components/Tree/TreeRow.d.ts.map +1 -0
- package/dist/components/Tree/TreeRowProps.d.ts +41 -0
- package/dist/components/Tree/TreeRowProps.d.ts.map +1 -0
- package/dist/components/Tree/TreeRows.d.ts +70 -0
- package/dist/components/Tree/TreeRows.d.ts.map +1 -0
- package/dist/components/Tree/TreeWindow.d.ts +21 -0
- package/dist/components/Tree/TreeWindow.d.ts.map +1 -0
- package/dist/components/Tree/styles.css.d.ts +69 -1
- package/dist/components/Tree/styles.css.d.ts.map +1 -1
- package/dist/components/Tree/useTreeKeyboard.d.ts +15 -9
- package/dist/components/Tree/useTreeKeyboard.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +3798 -3477
- package/dist/index.js.map +1 -1
- package/dist/shared/InLandmarkContext.d.ts +7 -0
- package/dist/shared/InLandmarkContext.d.ts.map +1 -0
- package/dist/shared/navigate.d.ts.map +1 -1
- package/dist/table.js +1 -1
- package/package.json +3 -3
- package/src/components/AppShell/AppShell.native.tsx +17 -14
- package/src/components/AppShell/AppShell.stories.tsx +30 -0
- package/src/components/AppShell/AppShell.tsx +3 -2
- package/src/components/Menu/Menu.native.tsx +4 -24
- package/src/components/Menu/native.css.ts +4 -0
- package/src/components/Panel/Panel.meta.ts +1 -1
- package/src/components/Popover/native.css.ts +4 -0
- package/src/components/SideNav/SideNav.meta.ts +1 -0
- package/src/components/SideNav/SideNav.tsx +18 -4
- package/src/components/TextLink/TextLink.meta.ts +1 -1
- package/src/components/TextLink/TextLink.native.tsx +55 -0
- package/src/components/TextLink/native.css.ts +36 -0
- package/src/components/Tree/Tree.meta.ts +1 -1
- package/src/components/Tree/Tree.stories.tsx +376 -1
- package/src/components/Tree/Tree.tsx +88 -131
- package/src/components/Tree/TreeContext.ts +5 -5
- package/src/components/Tree/TreeFocusStore.ts +73 -0
- package/src/components/Tree/TreeItemRow.tsx +71 -0
- package/src/components/Tree/TreeRow.native.tsx +96 -0
- package/src/components/Tree/TreeRow.tsx +112 -0
- package/src/components/Tree/TreeRowProps.ts +42 -0
- package/src/components/Tree/TreeRows.ts +139 -0
- package/src/components/Tree/TreeWindow.native.tsx +19 -0
- package/src/components/Tree/TreeWindow.tsx +118 -0
- package/src/components/Tree/styles.css.ts +81 -2
- package/src/components/Tree/useTreeKeyboard.ts +86 -42
- package/src/index.native.ts +4 -0
- package/src/shared/InLandmarkContext.ts +8 -0
- package/src/shared/followLink.native.ts +33 -0
- package/src/shared/navigate.ts +2 -0
- package/dist/Table-DnaoH-h5.js.map +0 -1
|
@@ -30,6 +30,37 @@ export const styles = css.create({
|
|
|
30
30
|
outlineWidth: 0,
|
|
31
31
|
outlineStyle: 'none',
|
|
32
32
|
},
|
|
33
|
+
// the scroll container of a windowed tree (role="tree"): fills its parent,
|
|
34
|
+
// or stops at `maxHeight`
|
|
35
|
+
windowScroller: {
|
|
36
|
+
position: 'relative',
|
|
37
|
+
width: '100%',
|
|
38
|
+
height: '100%',
|
|
39
|
+
minHeight: 0,
|
|
40
|
+
margin: 0,
|
|
41
|
+
padding: 0,
|
|
42
|
+
listStyleType: 'none',
|
|
43
|
+
overflowY: 'auto',
|
|
44
|
+
overscrollBehavior: 'contain',
|
|
45
|
+
},
|
|
46
|
+
windowScrollerMax: (maxHeight: number | string) => ({
|
|
47
|
+
height: 'auto',
|
|
48
|
+
maxHeight,
|
|
49
|
+
}),
|
|
50
|
+
// the full virtual height, so the scrollbar is the tree's real length
|
|
51
|
+
windowTotal: (height: number) => ({
|
|
52
|
+
position: 'relative',
|
|
53
|
+
width: '100%',
|
|
54
|
+
height,
|
|
55
|
+
}),
|
|
56
|
+
// a windowed row: flat, placed by the window
|
|
57
|
+
windowRow: (top: number) => ({
|
|
58
|
+
position: 'absolute',
|
|
59
|
+
top: 0,
|
|
60
|
+
left: 0,
|
|
61
|
+
width: '100%',
|
|
62
|
+
transform: `translateY(${top}px)`,
|
|
63
|
+
}),
|
|
33
64
|
row: {
|
|
34
65
|
display: 'flex',
|
|
35
66
|
alignItems: 'center',
|
|
@@ -73,9 +104,57 @@ export const styles = css.create({
|
|
|
73
104
|
fontWeight: typography.fontWeightMedium,
|
|
74
105
|
backgroundColor: colors.bgCardHover,
|
|
75
106
|
},
|
|
76
|
-
//
|
|
77
|
-
|
|
107
|
+
// Native: the outer row View, holding the chevron control and the press
|
|
108
|
+
// target as siblings (the level indent is added by the row).
|
|
109
|
+
nativeRow: {
|
|
78
110
|
display: 'flex',
|
|
111
|
+
flexDirection: 'row',
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
paddingRight: spacing.sm,
|
|
114
|
+
borderRadius: radii.sm,
|
|
115
|
+
},
|
|
116
|
+
// Native: the pressable part of the row.
|
|
117
|
+
pressTarget: {
|
|
118
|
+
display: 'flex',
|
|
119
|
+
flexDirection: 'row',
|
|
120
|
+
alignItems: 'center',
|
|
121
|
+
flexGrow: 1,
|
|
122
|
+
flexShrink: 1,
|
|
123
|
+
gap: spacing.xs,
|
|
124
|
+
paddingTop: microSpacing.px5,
|
|
125
|
+
paddingBottom: microSpacing.px5,
|
|
126
|
+
borderRadius: radii.sm,
|
|
127
|
+
backgroundColor: {
|
|
128
|
+
default: 'transparent',
|
|
129
|
+
':active': colors.bgCardHover,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
// Native: the branch's chevron control, at least the WCAG 2.5.8 minimum.
|
|
133
|
+
chevronControl: {
|
|
134
|
+
display: 'flex',
|
|
135
|
+
alignItems: 'center',
|
|
136
|
+
justifyContent: 'center',
|
|
137
|
+
minWidth: sizes.chip,
|
|
138
|
+
minHeight: sizes.chip,
|
|
139
|
+
flexShrink: 0,
|
|
140
|
+
},
|
|
141
|
+
chevronGlyph: {
|
|
142
|
+
color: colors.textMuted,
|
|
143
|
+
},
|
|
144
|
+
// Native: a leaf's gutter, as wide as the chevron control, inside the press target.
|
|
145
|
+
chevronSpacer: {
|
|
146
|
+
width: sizes.chip,
|
|
147
|
+
flexShrink: 0,
|
|
148
|
+
},
|
|
149
|
+
labelBox: {
|
|
150
|
+
display: 'flex',
|
|
151
|
+
flexGrow: 1,
|
|
152
|
+
flexShrink: 1,
|
|
153
|
+
minWidth: 0,
|
|
154
|
+
},
|
|
155
|
+
metaBox: {
|
|
156
|
+
display: 'flex',
|
|
157
|
+
flexShrink: 0,
|
|
79
158
|
},
|
|
80
159
|
chevron: {
|
|
81
160
|
display: 'inline-flex',
|
|
@@ -1,31 +1,70 @@
|
|
|
1
|
-
import {type
|
|
1
|
+
import {type RefObject, useEffect, useLayoutEffect, useRef} from 'react'
|
|
2
|
+
import type {TreeFocusStore} from './TreeFocusStore'
|
|
3
|
+
import type {TreeRowModel} from './TreeRows'
|
|
2
4
|
|
|
3
5
|
/** Typed characters within this many ms build one typeahead prefix. */
|
|
4
6
|
const TYPEAHEAD_RESET_MS = 500
|
|
5
7
|
|
|
6
8
|
interface TreeKeyboardInput {
|
|
7
|
-
rootRef:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
rootRef: RefObject<HTMLElement | null>
|
|
10
|
+
focus: TreeFocusStore
|
|
11
|
+
/** the visible rows when the tree is windowed (most are not in the DOM), else null */
|
|
12
|
+
windowedRows: readonly TreeRowModel[] | null
|
|
13
|
+
/** a change here re-checks the tab stop: the items or the open branches */
|
|
14
|
+
children: unknown
|
|
11
15
|
open: ReadonlyArray<string>
|
|
12
|
-
children: ReactNode
|
|
13
16
|
onSelect: (value: string) => void
|
|
14
17
|
setExpanded: (value: string, open: boolean) => void
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
/** What navigation needs of an item, from the row model or from the DOM. */
|
|
21
|
+
interface NavItem {
|
|
22
|
+
value: string
|
|
23
|
+
text: string
|
|
24
|
+
/** undefined for a leaf */
|
|
25
|
+
expanded: boolean | undefined
|
|
26
|
+
parent: string | null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const itemSelector = (value: string) => `[data-tree-value="${CSS.escape(value)}"]`
|
|
30
|
+
|
|
31
|
+
/** The rendered treeitems as nav items: DOM order IS the visible order. */
|
|
32
|
+
function readDom(root: HTMLElement): NavItem[] {
|
|
33
|
+
return Array.from(root.querySelectorAll<HTMLElement>('[role="treeitem"]')).map((el) => {
|
|
34
|
+
const expanded = el.getAttribute('aria-expanded')
|
|
35
|
+
const parent = el.parentElement?.closest<HTMLElement>('[role="treeitem"]')
|
|
36
|
+
return {
|
|
37
|
+
value: el.dataset.treeValue ?? '',
|
|
38
|
+
text: el.dataset.treeText ?? '',
|
|
39
|
+
expanded: expanded === null ? undefined : expanded === 'true',
|
|
40
|
+
parent: parent && root.contains(parent) ? (parent.dataset.treeValue ?? null) : null,
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readRows(rows: readonly TreeRowModel[]): NavItem[] {
|
|
46
|
+
return rows.map((row) => ({
|
|
47
|
+
value: row.value,
|
|
48
|
+
text: row.text,
|
|
49
|
+
expanded: row.hasChildren ? row.open : undefined,
|
|
50
|
+
parent: row.parent,
|
|
51
|
+
}))
|
|
52
|
+
}
|
|
53
|
+
|
|
17
54
|
/**
|
|
18
55
|
* Web: the tree's roving tab stop, arrow keys, Home/End, Enter and typeahead
|
|
19
|
-
* (WAI-ARIA tree pattern)
|
|
20
|
-
*
|
|
56
|
+
* (WAI-ARIA tree pattern). A nested tree reads its items from the DOM; a
|
|
57
|
+
* windowed one from the row model, since most rows are not mounted: a move to
|
|
58
|
+
* a row that is not mounted goes through the focus store, and the window
|
|
59
|
+
* scrolls to it and focuses it. Native has none of them (plan N0, Decision 6):
|
|
60
|
+
* useTreeKeyboard.native.ts.
|
|
21
61
|
*/
|
|
22
62
|
export function useTreeKeyboard({
|
|
23
63
|
rootRef,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
setKeyboardFocus,
|
|
27
|
-
open,
|
|
64
|
+
focus,
|
|
65
|
+
windowedRows,
|
|
28
66
|
children,
|
|
67
|
+
open,
|
|
29
68
|
onSelect,
|
|
30
69
|
setExpanded,
|
|
31
70
|
}: TreeKeyboardInput) {
|
|
@@ -34,32 +73,33 @@ export function useTreeKeyboard({
|
|
|
34
73
|
useLayoutEffect(() => {
|
|
35
74
|
const root = rootRef.current
|
|
36
75
|
if (!root) return
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
76
|
+
if (root.querySelector('[role="treeitem"][tabindex="0"]')) return
|
|
77
|
+
const first =
|
|
78
|
+
windowedRows?.[0]?.value ??
|
|
79
|
+
root.querySelector<HTMLElement>('[role="treeitem"]')?.dataset.treeValue ??
|
|
80
|
+
null
|
|
81
|
+
focus.setValue(first)
|
|
82
|
+
}, [rootRef, focus, windowedRows, children, open])
|
|
43
83
|
|
|
44
|
-
// Keyboard: DOM order over the rendered items IS the visible order (a
|
|
45
|
-
// closed branch renders no children), so navigation reads it directly.
|
|
46
84
|
const typeahead = useRef({text: '', at: 0})
|
|
47
85
|
useEffect(() => {
|
|
48
86
|
const root = rootRef.current
|
|
49
87
|
if (!root) return
|
|
50
88
|
const rootEl = root
|
|
51
|
-
const
|
|
52
|
-
const focusItem = (
|
|
53
|
-
if (!
|
|
54
|
-
el.
|
|
89
|
+
const read = () => (windowedRows ? readRows(windowedRows) : readDom(rootEl))
|
|
90
|
+
const focusItem = (item: NavItem | undefined) => {
|
|
91
|
+
if (!item) return
|
|
92
|
+
const el = rootEl.querySelector<HTMLElement>(itemSelector(item.value))
|
|
93
|
+
if (el) el.focus()
|
|
94
|
+
else focus.request(item.value)
|
|
55
95
|
}
|
|
56
96
|
function onKeyDown(e: KeyboardEvent) {
|
|
57
97
|
const current = (e.target as HTMLElement).closest<HTMLElement>('[role="treeitem"]')
|
|
58
98
|
if (!current || !rootEl.contains(current)) return
|
|
59
|
-
const list =
|
|
60
|
-
const i = list.indexOf(current)
|
|
99
|
+
const list = read()
|
|
61
100
|
const val = current.dataset.treeValue ?? ''
|
|
62
|
-
const
|
|
101
|
+
const i = list.findIndex((item) => item.value === val)
|
|
102
|
+
const here = list[i]
|
|
63
103
|
switch (e.key) {
|
|
64
104
|
case 'ArrowDown':
|
|
65
105
|
e.preventDefault()
|
|
@@ -71,17 +111,16 @@ export function useTreeKeyboard({
|
|
|
71
111
|
return
|
|
72
112
|
case 'ArrowRight':
|
|
73
113
|
e.preventDefault()
|
|
74
|
-
if (
|
|
75
|
-
else if (
|
|
114
|
+
if (here?.expanded === false) setExpanded(val, true)
|
|
115
|
+
else if (here?.expanded === true) focusItem(list[i + 1])
|
|
76
116
|
return
|
|
77
117
|
case 'ArrowLeft': {
|
|
78
118
|
e.preventDefault()
|
|
79
|
-
if (
|
|
119
|
+
if (here?.expanded === true) {
|
|
80
120
|
setExpanded(val, false)
|
|
81
121
|
return
|
|
82
122
|
}
|
|
83
|
-
|
|
84
|
-
if (parent && rootEl.contains(parent)) focusItem(parent)
|
|
123
|
+
focusItem(list.find((item) => item.value === here?.parent))
|
|
85
124
|
return
|
|
86
125
|
}
|
|
87
126
|
case 'Home':
|
|
@@ -99,7 +138,12 @@ export function useTreeKeyboard({
|
|
|
99
138
|
// runs onNavigate, or the browser navigates)
|
|
100
139
|
const link = current.querySelector<HTMLElement>(':scope > [data-tree-link]')
|
|
101
140
|
if (link) link.click()
|
|
102
|
-
else
|
|
141
|
+
else {
|
|
142
|
+
onSelect(val)
|
|
143
|
+
// a branch with no link toggles too, as a click on its row does
|
|
144
|
+
const expanded = current.getAttribute('aria-expanded')
|
|
145
|
+
if (expanded !== null) setExpanded(val, expanded !== 'true')
|
|
146
|
+
}
|
|
103
147
|
return
|
|
104
148
|
}
|
|
105
149
|
}
|
|
@@ -116,9 +160,9 @@ export function useTreeKeyboard({
|
|
|
116
160
|
const needle = repeat ? typed[0] : typed
|
|
117
161
|
const start = repeat ? i + 1 : i
|
|
118
162
|
for (let k = 0; k < list.length; k++) {
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
121
|
-
focusItem(
|
|
163
|
+
const item = list[(start + k) % list.length]
|
|
164
|
+
if (item.text.toLocaleLowerCase().startsWith(needle)) {
|
|
165
|
+
focusItem(item)
|
|
122
166
|
break
|
|
123
167
|
}
|
|
124
168
|
}
|
|
@@ -128,14 +172,14 @@ export function useTreeKeyboard({
|
|
|
128
172
|
function onFocusIn(e: FocusEvent) {
|
|
129
173
|
const item = (e.target as HTMLElement).closest<HTMLElement>('[role="treeitem"]')
|
|
130
174
|
const v = item?.dataset.treeValue
|
|
131
|
-
if (v)
|
|
132
|
-
|
|
175
|
+
if (v) focus.setValue(v)
|
|
176
|
+
focus.setRing(item?.matches(':focus-visible') ?? false)
|
|
133
177
|
}
|
|
134
178
|
function onFocusOut(e: FocusEvent) {
|
|
135
|
-
if (!rootEl.contains(e.relatedTarget as Node | null))
|
|
179
|
+
if (!rootEl.contains(e.relatedTarget as Node | null)) focus.setRing(false)
|
|
136
180
|
}
|
|
137
|
-
const onPointerDown = () =>
|
|
138
|
-
const onKey = () =>
|
|
181
|
+
const onPointerDown = () => focus.setRing(false)
|
|
182
|
+
const onKey = () => focus.setRing(true)
|
|
139
183
|
root.addEventListener('keydown', onKeyDown)
|
|
140
184
|
root.addEventListener('keydown', onKey)
|
|
141
185
|
root.addEventListener('focusin', onFocusIn)
|
|
@@ -148,5 +192,5 @@ export function useTreeKeyboard({
|
|
|
148
192
|
root.removeEventListener('focusout', onFocusOut)
|
|
149
193
|
root.removeEventListener('pointerdown', onPointerDown)
|
|
150
194
|
}
|
|
151
|
-
}, [rootRef,
|
|
195
|
+
}, [rootRef, focus, windowedRows, onSelect, setExpanded])
|
|
152
196
|
}
|
package/src/index.native.ts
CHANGED
|
@@ -24,6 +24,7 @@ export {Heading, type HeadingVariant} from './components/Heading/Heading'
|
|
|
24
24
|
export {Input, type InputVariant} from './components/Input/Input'
|
|
25
25
|
export {Switch} from './components/Switch/Switch'
|
|
26
26
|
export {Text, type TextVariant, type TextColor} from './components/Text/Text'
|
|
27
|
+
export {TextLink, type TextLinkVariant} from './components/TextLink/TextLink'
|
|
27
28
|
export {Textarea, type TextareaVariant} from './components/Textarea/Textarea'
|
|
28
29
|
|
|
29
30
|
// Form field wrappers (RN-safe — used by Input/Textarea/Checkbox)
|
|
@@ -95,11 +96,14 @@ export {
|
|
|
95
96
|
|
|
96
97
|
// Layout primitives
|
|
97
98
|
export {PageShell, type PageShellMaxWidth, type PageShellPadding} from './components/PageShell'
|
|
99
|
+
export {Panel} from './components/Panel/Panel'
|
|
98
100
|
export {Stack, type SpacingKey} from './components/Stack/Stack'
|
|
99
101
|
export {Inline} from './components/Inline/Inline'
|
|
100
102
|
export {Cluster} from './components/Cluster/Cluster'
|
|
101
103
|
export {Grid} from './components/Grid/Grid'
|
|
102
104
|
export type {GridTrack} from './shared/length'
|
|
105
|
+
// ADR-0002: the shape a link part's onNavigate receives (native: followLink)
|
|
106
|
+
export type {LinkClickEvent, OnNavigate} from './shared/navigate'
|
|
103
107
|
|
|
104
108
|
// Theming — resolves to ThemeProvider.native.tsx on Metro (RN-safe variant:
|
|
105
109
|
// no display:contents / portal mount; applies the theme to a flex container).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {createContext} from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* True inside `AppShell.Rail`, which is already a named navigation landmark.
|
|
5
|
+
* A `SideNav.Root` reads it and renders no second navigation landmark: a plain
|
|
6
|
+
* block, or `role="group"` carrying its `aria-label` when it has one.
|
|
7
|
+
*/
|
|
8
|
+
export const InLandmarkContext = createContext(false)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {Linking} from 'react-native'
|
|
2
|
+
import type {LinkClickEvent, OnNavigate} from './navigate'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Follows a link on native (ADR-0002, native addendum): a native link part
|
|
6
|
+
* renders no `href`, because react-strict-dom replaces the press handler of an
|
|
7
|
+
* `html.a` that has one. It builds a tracked `LinkClickEvent` (primary button,
|
|
8
|
+
* no modifier), hands it to `onNavigate`, and opens the URL with the system
|
|
9
|
+
* (`Linking.openURL`) unless `onNavigate` called `event.preventDefault()`.
|
|
10
|
+
* `source` names the part in the warning when the URL cannot be opened.
|
|
11
|
+
*/
|
|
12
|
+
export function followLink(href: string, onNavigate: OnNavigate | undefined, source: string): void {
|
|
13
|
+
let prevented = false
|
|
14
|
+
const event: LinkClickEvent = {
|
|
15
|
+
button: 0,
|
|
16
|
+
metaKey: false,
|
|
17
|
+
ctrlKey: false,
|
|
18
|
+
shiftKey: false,
|
|
19
|
+
altKey: false,
|
|
20
|
+
get defaultPrevented() {
|
|
21
|
+
return prevented
|
|
22
|
+
},
|
|
23
|
+
preventDefault() {
|
|
24
|
+
prevented = true
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
onNavigate?.(href, event)
|
|
28
|
+
if (!prevented) {
|
|
29
|
+
Linking.openURL(href).catch((error: unknown) => {
|
|
30
|
+
console.warn(`[duro] ${source}: could not open ${href}: ${String(error)}`)
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/shared/navigate.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/* Router-agnostic links (docs/adr/0002-link-parts-navigate-through-onnavigate.md).
|
|
2
|
+
* On native a link part renders no `href` and goes through `followLink`
|
|
3
|
+
* (followLink.native.ts), which calls the same `onNavigate` (ADR-0002 addendum).
|
|
2
4
|
*
|
|
3
5
|
* Every link part renders a real `<a href>`, so the browser keeps what a link
|
|
4
6
|
* does on its own: cmd/ctrl-click and middle-click open a tab, shift-click a
|