@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
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True inside `AppShell.Rail`, which is already a named navigation landmark.
|
|
3
|
+
* A `SideNav.Root` reads it and renders no second navigation landmark: a plain
|
|
4
|
+
* block, or `role="group"` carrying its `aria-label` when it has one.
|
|
5
|
+
*/
|
|
6
|
+
export declare const InLandmarkContext: import('react').Context<boolean>;
|
|
7
|
+
//# sourceMappingURL=InLandmarkContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InLandmarkContext.d.ts","sourceRoot":"","sources":["../../src/shared/InLandmarkContext.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,kCAAuB,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigate.d.ts","sourceRoot":"","sources":["../../src/shared/navigate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"navigate.d.ts","sourceRoot":"","sources":["../../src/shared/navigate.ts"],"names":[],"mappings":"AAkBA,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAA;IAClC,cAAc,IAAI,IAAI,CAAA;CACvB;AAED;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,KAAK,IAAI,CAAA;AAEtE,0EAA0E;AAC1E,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAS3D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,UAAU,GAAG,SAAS,EAClC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GACrB,CAAC,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC,GAAG,SAAS,CAM/C"}
|
package/dist/table.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { M as F, q as gt, R as it, H as kt, t as pt, v as bt, w as ft, x as mt, b as ht, g as yt, r as St, y as Tt } from "./Table-
|
|
1
|
+
import { M as F, q as gt, R as it, H as kt, t as pt, v as bt, w as ft, x as mt, b as ht, g as yt, r as St, y as Tt } from "./Table-Cw1B14R3.js";
|
|
2
2
|
import { jsx as u, jsxs as q } from "react/jsx-runtime";
|
|
3
3
|
import { c as H, m as p, d as b } from "./compiler-runtime-BuugS4D0.js";
|
|
4
4
|
import { flexRender as E, useReactTable as rt, getPaginationRowModel as Ct, getFilteredRowModel as vt, getSortedRowModel as at, getCoreRowModel as st } from "@tanstack/react-table";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duro-app/ui",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.20.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -90,9 +90,9 @@
|
|
|
90
90
|
}
|
|
91
91
|
},
|
|
92
92
|
"dependencies": {
|
|
93
|
-
"@tanstack/react-virtual": "^3.14.
|
|
93
|
+
"@tanstack/react-virtual": "^3.14.13",
|
|
94
94
|
"qrcode": "^1.5.4",
|
|
95
|
-
"@duro-app/tokens": "^5.
|
|
95
|
+
"@duro-app/tokens": "^5.20.0"
|
|
96
96
|
},
|
|
97
97
|
"devDependencies": {
|
|
98
98
|
"@babel/preset-typescript": "^7.28.0",
|
|
@@ -14,6 +14,7 @@ import {Button} from '../Button/Button'
|
|
|
14
14
|
import {Drawer} from '../Drawer/Drawer'
|
|
15
15
|
import {Icon} from '../Icon'
|
|
16
16
|
import {useSafeAreaInsets} from '../../shared/safeArea'
|
|
17
|
+
import {InLandmarkContext} from '../../shared/InLandmarkContext'
|
|
17
18
|
import {nativeStyles} from './native.css'
|
|
18
19
|
|
|
19
20
|
export type AppShellCollapse = 'sm' | 'md' | 'lg'
|
|
@@ -117,20 +118,22 @@ function Rail({'aria-label': ariaLabel, children, footer}: RailProps) {
|
|
|
117
118
|
const foot = typeof footer === 'function' ? footer({close}) : footer
|
|
118
119
|
|
|
119
120
|
return (
|
|
120
|
-
<
|
|
121
|
-
<Drawer.
|
|
122
|
-
<Drawer.
|
|
123
|
-
<Drawer.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
<
|
|
128
|
-
{
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
121
|
+
<InLandmarkContext.Provider value={true}>
|
|
122
|
+
<Drawer.Root anchor="left" open={open} onOpenChange={setOpen}>
|
|
123
|
+
<Drawer.Portal size="sm">
|
|
124
|
+
<Drawer.Header>
|
|
125
|
+
<Drawer.Title>{ariaLabel}</Drawer.Title>
|
|
126
|
+
<Drawer.Close aria-label={closeLabel} />
|
|
127
|
+
</Drawer.Header>
|
|
128
|
+
<Drawer.Body>
|
|
129
|
+
<html.nav aria-label={ariaLabel}>
|
|
130
|
+
{content}
|
|
131
|
+
{foot != null && <html.div style={nativeStyles.drawerFooter}>{foot}</html.div>}
|
|
132
|
+
</html.nav>
|
|
133
|
+
</Drawer.Body>
|
|
134
|
+
</Drawer.Portal>
|
|
135
|
+
</Drawer.Root>
|
|
136
|
+
</InLandmarkContext.Provider>
|
|
134
137
|
)
|
|
135
138
|
}
|
|
136
139
|
|
|
@@ -348,12 +348,41 @@ export const DesktopLandmarks: Story = {
|
|
|
348
348
|
await expect(canvas.getAllByRole('banner')).toHaveLength(1)
|
|
349
349
|
const names = canvas.getAllByRole('navigation').map((nav) => nav.getAttribute('aria-label'))
|
|
350
350
|
await expect(new Set(names).size).toBe(names.length)
|
|
351
|
+
// the SideNav inside the rail is a group, not a second navigation landmark
|
|
352
|
+
await expect(canvas.getAllByRole('navigation')).toHaveLength(1)
|
|
351
353
|
await expect(canvas.queryByRole('button', {name: args.menuLabel})).toBeNull()
|
|
352
354
|
// the brand is the rail's copy
|
|
353
355
|
await expect(rail.contains(canvas.getByRole('link', {name: args.brand}))).toBe(true)
|
|
354
356
|
},
|
|
355
357
|
}
|
|
356
358
|
|
|
359
|
+
/**
|
|
360
|
+
* A SideNav without an `aria-label` inside a labelled Rail: the Rail is the one
|
|
361
|
+
* navigation landmark, the SideNav adds none.
|
|
362
|
+
*/
|
|
363
|
+
export const RailWithUnlabelledSideNav: Story = {
|
|
364
|
+
tags: TEST_ONLY,
|
|
365
|
+
render: ({width}) => (
|
|
366
|
+
<Frame px={FRAME_PX[width]}>
|
|
367
|
+
<AppShell.Root menuLabel="Menu" closeLabel="Close navigation" skipLabel="Skip to content">
|
|
368
|
+
<AppShell.Rail aria-label="Main">
|
|
369
|
+
<SideNav.Root defaultValue="/">
|
|
370
|
+
<SideNav.Item value="/">Dashboard</SideNav.Item>
|
|
371
|
+
<SideNav.Item value="/articles">Articles</SideNav.Item>
|
|
372
|
+
</SideNav.Root>
|
|
373
|
+
</AppShell.Rail>
|
|
374
|
+
<AppShell.Main>
|
|
375
|
+
<Heading level={1}>Docs</Heading>
|
|
376
|
+
</AppShell.Main>
|
|
377
|
+
</AppShell.Root>
|
|
378
|
+
</Frame>
|
|
379
|
+
),
|
|
380
|
+
play: async ({canvas}) => {
|
|
381
|
+
await expect(canvas.getAllByRole('navigation')).toHaveLength(1)
|
|
382
|
+
await expect(canvas.getByRole('navigation', {name: 'Main'})).toBeVisible()
|
|
383
|
+
},
|
|
384
|
+
}
|
|
385
|
+
|
|
357
386
|
/**
|
|
358
387
|
* The Menu button opens the rail in a Drawer. Focus moves into the drawer,
|
|
359
388
|
* and back to the Menu button when it closes — by Escape, and after a pick.
|
|
@@ -368,6 +397,7 @@ export const PhoneDrawer: Story = {
|
|
|
368
397
|
await expect(menu).toHaveAttribute('aria-expanded', 'true')
|
|
369
398
|
const drawerNav = within(dialog).getByRole('navigation', {name: 'Navigation'})
|
|
370
399
|
await waitFor(() => expect(drawerNav.contains(document.activeElement)).toBe(true))
|
|
400
|
+
await expect(within(dialog).getAllByRole('navigation')).toHaveLength(1)
|
|
371
401
|
// the rail's content is rendered once, in the drawer while it is open:
|
|
372
402
|
// counted in the DOM (a hidden copy would still duplicate a Tree's ids)
|
|
373
403
|
await expect(page().getAllByText('Articles')).toHaveLength(1)
|
|
@@ -20,6 +20,7 @@ import {Icon} from '../Icon'
|
|
|
20
20
|
import {TextLink} from '../TextLink/TextLink'
|
|
21
21
|
import {useContainerBelow} from '../../hooks/useContainerBelow'
|
|
22
22
|
import type {OnNavigate} from '../../shared/navigate'
|
|
23
|
+
import {InLandmarkContext} from '../../shared/InLandmarkContext'
|
|
23
24
|
import {styles} from './styles.css'
|
|
24
25
|
|
|
25
26
|
/* AppShell — an application's frame: a navigation rail beside the main
|
|
@@ -253,7 +254,7 @@ function Rail({'aria-label': ariaLabel, children, footer}: RailProps) {
|
|
|
253
254
|
}, [open])
|
|
254
255
|
|
|
255
256
|
return (
|
|
256
|
-
|
|
257
|
+
<InLandmarkContext.Provider value={true}>
|
|
257
258
|
{/* Rendered in one place at a time, so ids (a Tree's) and the landmark
|
|
258
259
|
name are never duplicated: the drawer only opens below the collapse
|
|
259
260
|
point, where the rail is hidden. The brand stays: the bar holds the
|
|
@@ -281,7 +282,7 @@ function Rail({'aria-label': ariaLabel, children, footer}: RailProps) {
|
|
|
281
282
|
</Drawer.Body>
|
|
282
283
|
</Drawer.Portal>
|
|
283
284
|
</Drawer.Root>
|
|
284
|
-
|
|
285
|
+
</InLandmarkContext.Provider>
|
|
285
286
|
)
|
|
286
287
|
}
|
|
287
288
|
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
useId,
|
|
10
10
|
useRef,
|
|
11
11
|
} from 'react'
|
|
12
|
-
import {
|
|
12
|
+
import {Platform, Pressable, ScrollView, StyleSheet} from 'react-native'
|
|
13
13
|
import {html} from 'react-strict-dom'
|
|
14
14
|
import {SIZES_PX} from '@duro-app/tokens/keys'
|
|
15
15
|
import {styles} from './styles.css'
|
|
@@ -18,7 +18,8 @@ import {useMenuOpen} from './useMenuOpen'
|
|
|
18
18
|
import {AnchoredOverlay} from '../../shared/portal/AnchoredOverlay'
|
|
19
19
|
import {focusForAccessibility} from '../../shared/accessibilityFocus'
|
|
20
20
|
import {mergeRefs} from '../../shared/mergeRefs'
|
|
21
|
-
import
|
|
21
|
+
import {followLink} from '../../shared/followLink.native'
|
|
22
|
+
import type {OnNavigate} from '../../shared/navigate'
|
|
22
23
|
import type {ControlSize} from '../../shared/types'
|
|
23
24
|
import {useTooltipHint} from '../Tooltip/TooltipHint'
|
|
24
25
|
import {ControlContextBoundary} from '../Toolbar/ControlContextBoundary'
|
|
@@ -223,28 +224,7 @@ interface LinkItemProps {
|
|
|
223
224
|
|
|
224
225
|
function LinkItem({href, onNavigate, children}: LinkItemProps) {
|
|
225
226
|
const {closeThen} = useMenu()
|
|
226
|
-
const follow = () =>
|
|
227
|
-
let prevented = false
|
|
228
|
-
const event: LinkClickEvent = {
|
|
229
|
-
button: 0,
|
|
230
|
-
metaKey: false,
|
|
231
|
-
ctrlKey: false,
|
|
232
|
-
shiftKey: false,
|
|
233
|
-
altKey: false,
|
|
234
|
-
get defaultPrevented() {
|
|
235
|
-
return prevented
|
|
236
|
-
},
|
|
237
|
-
preventDefault() {
|
|
238
|
-
prevented = true
|
|
239
|
-
},
|
|
240
|
-
}
|
|
241
|
-
onNavigate?.(href, event)
|
|
242
|
-
if (!prevented) {
|
|
243
|
-
Linking.openURL(href).catch((error: unknown) => {
|
|
244
|
-
console.warn(`[duro] Menu.LinkItem: could not open ${href}: ${String(error)}`)
|
|
245
|
-
})
|
|
246
|
-
}
|
|
247
|
-
}
|
|
227
|
+
const follow = () => followLink(href, onNavigate, 'Menu.LinkItem')
|
|
248
228
|
return (
|
|
249
229
|
<Pressable accessibilityRole="link" onPress={() => closeThen(follow)}>
|
|
250
230
|
{({pressed}) => (
|
|
@@ -8,8 +8,12 @@ export const nativeStyles = css.create({
|
|
|
8
8
|
nativeFlex: {
|
|
9
9
|
display: 'flex',
|
|
10
10
|
},
|
|
11
|
+
// The shared popup's `zIndex: layers.popup` is the CSS string '1050', which Android's
|
|
12
|
+
// view manager refuses (iOS ignores it). The popup sits alone in an OverlayModal
|
|
13
|
+
// window, where web layering has no job: a number is all it needs.
|
|
11
14
|
popupInPlace: {
|
|
12
15
|
position: 'relative',
|
|
16
|
+
zIndex: 0,
|
|
13
17
|
},
|
|
14
18
|
// A press shows the highlight that hover and the arrow keys show on the web.
|
|
15
19
|
itemPressed: {
|
|
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'Structural primitive for grouping content with header, body, and footer slots. A "workspace surface" for forms, settings sections, and inspectors. Sub-components are purely structural — no context required.',
|
|
5
|
+
'Structural primitive for grouping content with header, body, and footer slots. A "workspace surface" for forms, settings sections, and inspectors. Sub-components are purely structural — no context required. On native the Body does not scroll (unlike the web Body): wrap long content in ScrollArea. A TextLink inside needs onNavigate for in-app routes on native.',
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'Organizing content with distinct header/body/footer sections',
|
|
8
8
|
'Settings panels, form containers, inspector views',
|
|
@@ -7,7 +7,11 @@ export const nativeStyles = css.create({
|
|
|
7
7
|
nativeFlex: {
|
|
8
8
|
display: 'flex',
|
|
9
9
|
},
|
|
10
|
+
// The shared popup's `zIndex: layers.floating` is the CSS string '50', which Android's
|
|
11
|
+
// view manager refuses (iOS ignores it). The popup sits alone in an OverlayModal
|
|
12
|
+
// window, where web layering has no job: a number is all it needs.
|
|
10
13
|
popupInPlace: {
|
|
11
14
|
position: 'relative',
|
|
15
|
+
zIndex: 0,
|
|
12
16
|
},
|
|
13
17
|
})
|
|
@@ -4,6 +4,7 @@ export const meta: ComponentMeta = {
|
|
|
4
4
|
description:
|
|
5
5
|
'Vertical side navigation. `Section` is a labelled block that is always open — the default for primary navigation. `Group` is the same block behind a chevron (a disclosure) — reach for it only for a rare/advanced region, an unbounded data-driven list, or a rail long enough that flat stops reading as an overview; mixing the two in one rail is the healthy shape. `Item`s take an optional `icon` and show an active left-marker. Supports controlled and uncontrolled active item. Compound component — Root is required. On React Native the same parts render, and a Group knows whether it holds the active item from its Items, not from the DOM.',
|
|
6
6
|
whenToUse: [
|
|
7
|
+
'Inside `AppShell.Rail`: the Rail is the navigation landmark, so Root renders a plain block, or `role="group"` with its `aria-label` when it has one. Name the Rail "Main", not "Navigation" (screen readers would announce "Navigation navigation"). Outside a Rail, Root is a `nav` landmark.',
|
|
7
8
|
'App-level sidebar navigation — flat `Section`s, every destination visible',
|
|
8
9
|
'Settings navigation with labelled regions',
|
|
9
10
|
'`Group` for a rare or advanced region tucked below the flat sections',
|
|
@@ -13,6 +13,7 @@ import {styles} from './styles.css'
|
|
|
13
13
|
import {useControllableValue} from '../../hooks/useControllableValue'
|
|
14
14
|
import {SideNavContext, useSideNav} from './SideNavContext'
|
|
15
15
|
import {isNative} from '../../platform'
|
|
16
|
+
import {InLandmarkContext} from '../../shared/InLandmarkContext'
|
|
16
17
|
import {Chevron} from './Chevron'
|
|
17
18
|
|
|
18
19
|
/** A Group's own items: each Item inside it registers its value here. */
|
|
@@ -51,7 +52,8 @@ interface RootProps {
|
|
|
51
52
|
/**
|
|
52
53
|
* Names the navigation landmark ("Main", "Settings"). Give one whenever
|
|
53
54
|
* the page has another nav (a breadcrumb, a pager): screen readers list
|
|
54
|
-
* landmarks by name.
|
|
55
|
+
* landmarks by name. Inside `AppShell.Rail` it is not a landmark (the Rail
|
|
56
|
+
* is): the name then labels a `role="group"`.
|
|
55
57
|
*/
|
|
56
58
|
'aria-label'?: string
|
|
57
59
|
value?: string
|
|
@@ -75,6 +77,7 @@ function Root({
|
|
|
75
77
|
}
|
|
76
78
|
: undefined,
|
|
77
79
|
)
|
|
80
|
+
const inLandmark = useContext(InLandmarkContext)
|
|
78
81
|
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set())
|
|
79
82
|
const orderRef = useRef<string[]>([])
|
|
80
83
|
|
|
@@ -124,9 +127,20 @@ function Root({
|
|
|
124
127
|
orderRef,
|
|
125
128
|
}}
|
|
126
129
|
>
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
{inLandmark ? (
|
|
131
|
+
// AppShell.Rail is already the navigation landmark: keep the name as a group.
|
|
132
|
+
<html.div
|
|
133
|
+
role={ariaLabel != null ? 'group' : undefined}
|
|
134
|
+
aria-label={ariaLabel}
|
|
135
|
+
style={styles.root}
|
|
136
|
+
>
|
|
137
|
+
{children}
|
|
138
|
+
</html.div>
|
|
139
|
+
) : (
|
|
140
|
+
<html.nav role="navigation" aria-label={ariaLabel} style={styles.root}>
|
|
141
|
+
{children}
|
|
142
|
+
</html.nav>
|
|
143
|
+
)}
|
|
130
144
|
</SideNavContext.Provider>
|
|
131
145
|
)
|
|
132
146
|
}
|
|
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'Inline hyperlink for running text and standalone text links ("View all", "Edit profile"). Always underlined, so colour is never the only cue: default is accent text with an accent underline; subtle keeps the surrounding text colour with a muted underline and turns accent on hover. target="_blank" defaults rel to noopener noreferrer. onNavigate(href, event) hands a plain primary click to a client router (the app calls event.preventDefault(), then navigates); modified clicks keep the browser default.',
|
|
5
|
+
'Inline hyperlink for running text and standalone text links ("View all", "Edit profile"). Always underlined, so colour is never the only cue: default is accent text with an accent underline; subtle keeps the surrounding text colour with a muted underline and turns accent on hover. target="_blank" defaults rel to noopener noreferrer. onNavigate(href, event) hands a plain primary click to a client router (the app calls event.preventDefault(), then navigates); modified clicks keep the browser default. On native the link renders no href and a press goes through onNavigate, then the system (Linking.openURL): onNavigate is required for in-app routes on native, and a target set skips it.',
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'A link inside a sentence or paragraph',
|
|
8
8
|
'A standalone text link under a list or card ("View all requests")',
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type {ReactNode} from 'react'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import {nativeStyles} from './native.css'
|
|
4
|
+
// Explicit suffix: a native-only module has no web file for moduleSuffixes to pair.
|
|
5
|
+
import {followLink} from '../../shared/followLink.native'
|
|
6
|
+
import type {OnNavigate} from '../../shared/navigate'
|
|
7
|
+
|
|
8
|
+
export type TextLinkVariant = 'default' | 'subtle'
|
|
9
|
+
|
|
10
|
+
interface TextLinkProps {
|
|
11
|
+
href: string
|
|
12
|
+
/** `_blank` hands the URL to the system; `onNavigate` is then not called. */
|
|
13
|
+
target?: '_blank' | '_self'
|
|
14
|
+
/** Ignored on native: there is no `rel`. */
|
|
15
|
+
rel?: string
|
|
16
|
+
variant?: TextLinkVariant
|
|
17
|
+
/** Accessible name, when the visible text alone does not say where it goes. */
|
|
18
|
+
'aria-label'?: string
|
|
19
|
+
/**
|
|
20
|
+
* Client-side navigation: called for a press on a link with no `target`.
|
|
21
|
+
* Call `event.preventDefault()`, then your router's navigate. Required for
|
|
22
|
+
* an in-app route on native: without it, the URL opens with the system.
|
|
23
|
+
*/
|
|
24
|
+
onNavigate?: OnNavigate
|
|
25
|
+
children: ReactNode
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Native TextLink (ADR-0002): an inline `Text` with `role="link"` that renders
|
|
30
|
+
* NO `href`, and follows the link through `followLink` on press.
|
|
31
|
+
*
|
|
32
|
+
* holds-until: a react-strict-dom release forwards the press of an `html.a`
|
|
33
|
+
* that has an `href` (react/react-strict-dom#58). Until then RSD 0.0.55
|
|
34
|
+
* replaces that `onPress` with a dev-only "href handling is not implemented"
|
|
35
|
+
* stub (dist/native/index.js:3094-3100), so `href` stays off the element and
|
|
36
|
+
* scripts/check-native-graph.mjs refuses it. Then: pass `href`, drop the rule.
|
|
37
|
+
*/
|
|
38
|
+
export function TextLink({
|
|
39
|
+
href,
|
|
40
|
+
target,
|
|
41
|
+
variant = 'default',
|
|
42
|
+
'aria-label': ariaLabel,
|
|
43
|
+
onNavigate,
|
|
44
|
+
children,
|
|
45
|
+
}: TextLinkProps) {
|
|
46
|
+
return (
|
|
47
|
+
<html.a
|
|
48
|
+
aria-label={ariaLabel}
|
|
49
|
+
onClick={() => followLink(href, target ? undefined : onNavigate, 'TextLink')}
|
|
50
|
+
style={[nativeStyles.base, nativeStyles[variant]]}
|
|
51
|
+
>
|
|
52
|
+
{children}
|
|
53
|
+
</html.a>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
|
|
4
|
+
// Native TextLink: the same colour and underline facts as ./styles.css.ts —
|
|
5
|
+
// change both together. A copy, not a composition: the web file's base style
|
|
6
|
+
// carries transition* and :focus-visible outline keys that react-strict-dom
|
|
7
|
+
// rejects on native, and its :hover state becomes :active (a press) here.
|
|
8
|
+
export const nativeStyles = css.create({
|
|
9
|
+
base: {
|
|
10
|
+
fontFamily: 'inherit',
|
|
11
|
+
fontSize: 'inherit',
|
|
12
|
+
lineHeight: 'inherit',
|
|
13
|
+
textDecorationLine: 'underline',
|
|
14
|
+
textDecorationStyle: 'solid',
|
|
15
|
+
},
|
|
16
|
+
default: {
|
|
17
|
+
color: {
|
|
18
|
+
default: colors.accent,
|
|
19
|
+
':active': colors.accentHover,
|
|
20
|
+
},
|
|
21
|
+
textDecorationColor: {
|
|
22
|
+
default: colors.accent,
|
|
23
|
+
':active': colors.accentHover,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
subtle: {
|
|
27
|
+
color: {
|
|
28
|
+
default: 'inherit',
|
|
29
|
+
':active': colors.accent,
|
|
30
|
+
},
|
|
31
|
+
textDecorationColor: {
|
|
32
|
+
default: colors.textMuted,
|
|
33
|
+
':active': colors.accent,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
})
|
|
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'Hierarchy of items with single selection and expandable branches (the WAI-ARIA tree pattern): one tab stop, arrow keys, Home/End, typeahead. An Item with href renders its row as a real link (cmd-click opens a tab) outside the tab order; Enter/Space follows it, through onNavigate(href, event) when given. Compound component — Root is required. On React Native selection, expansion and aria-level are the same
|
|
5
|
+
'Hierarchy of items with single selection and expandable branches (the WAI-ARIA tree pattern): one tab stop, arrow keys, Home/End, typeahead. A click or Enter on a branch row with no href selects it and toggles it open or closed (a folder); a branch with href navigates, and its chevron toggles. An Item with href renders its row as a real link (cmd-click opens a tab) outside the tab order; Enter/Space follows it, through onNavigate(href, event) when given. Compound component — Root is required. On React Native each row is a pressable link (href) or button, and a branch has its own chevron control that expands it (named by the optional Root prop expandLabel(text), default the item text); selection, expansion and aria-level are the same, the keyboard model (arrows, Home/End, typeahead) is web only. On native onNavigate is required for in-app routes: without it a relative href goes to the system (Linking.openURL). On web a tree with more items than Root virtualizeThreshold (default 150, open or not) is windowed: only the rows in view mount, as flat treeitems with aria-level, aria-setsize and aria-posinset; maxHeight caps its scroll viewport. Items must be JSX descendants of Root (fragments and arrays are fine, an Item hidden inside another component is not seen by the window). Native renders every row.',
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'Browsing data that nests to any depth — a file tree, zones › services › capabilities, namespace › resource',
|
|
8
8
|
'Picking one node of a hierarchy to show its details beside the tree',
|