@nxgt/material 3.65.0 → 3.67.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 +28 -0
- package/README.md +6 -2
- package/dist/{alert-dialog-C0nGUSCP.js → alert-dialog-BgQDxcYh.js} +1 -1
- package/dist/{async-slice-CyansBvD.js → async-slice-BCZyWgGr.js} +48 -25
- package/dist/{background-qdnet2A5.js → background-D1gC4m2R.js} +1 -1
- package/dist/backgrounds.js +1 -1
- package/dist/{button-D5k3K-gM.js → button-2de_lrix.js} +1 -1
- package/dist/charts.js +3 -3
- package/dist/{chip-lWXIJkqo.js → chip-Cs0uEjpu.js} +2 -2
- package/dist/{code-editor-aXG4CdY-.js → code-editor-BYhBAfcc.js} +5 -5
- package/dist/code-editor.js +5 -5
- package/dist/{code-language-items-CsaXyApg.js → code-language-items-xhmbF6wA.js} +2 -2
- package/dist/components/components.css +2 -0
- package/dist/components/ui/layout/app-shell/activity-backdrop.css +4 -3
- package/dist/components/ui/primitives/tabs/tabs-nav-forced.css +27 -0
- package/dist/components/ui/primitives/toggle-group/toggle-group-segmented.css +10 -2
- package/dist/components/ui/primitives/toggle-group/toggle-group.css +5 -1
- package/dist/{components-DNcTvjlv.js → components-BRWaX4Xk.js} +17477 -17205
- package/dist/components.js +28 -28
- package/dist/{compose-refs-DZ0iIVF-.js → compose-refs-G4J4kaUA.js} +1 -1
- package/dist/{dialog-DWNaxpa-.js → dialog-Co5oksU1.js} +4 -4
- package/dist/dnd.js +2 -2
- package/dist/{dropdown-menu-ChvAIQIW.js → dropdown-menu-DdzyvN76.js} +1 -1
- package/dist/editor.js +365 -365
- package/dist/{empty-state-CFLcVzKL.js → empty-state-CQ0CV0kA.js} +2 -2
- package/dist/{field-blur-CV9orzdJ.js → field-blur-B2y-bNYi.js} +1 -1
- package/dist/{field-blur-context-C8BX8IBy.js → field-blur-context-BZtwcPUV.js} +1 -1
- package/dist/{hooks-DilW-3jc.js → hooks-DjuYsY-j.js} +2 -2
- package/dist/hooks.js +2 -2
- package/dist/{hover-card-IeS_1RPH.js → hover-card-BeBVy6tB.js} +1 -1
- package/dist/i18n.js +3 -3
- package/dist/{icon-badge-Bf4SoA2i.js → icon-badge-DbYsbzwZ.js} +3 -3
- package/dist/{icon-button-l3dQTRrC.js → icon-button-Co6ttdv1.js} +1 -1
- package/dist/lib/components/ui/actions/back-link/back-link.d.ts +10 -0
- package/dist/lib/components/ui/actions/back-link/index.d.ts +1 -0
- package/dist/lib/components/ui/actions/index.d.ts +2 -0
- package/dist/lib/components/ui/actions/link/link.d.ts +1 -19
- package/dist/lib/components/ui/actions/share-button/index.d.ts +3 -0
- package/dist/lib/components/ui/actions/share-button/share-button.d.ts +17 -0
- package/dist/lib/components/ui/actions/share-button/share-or-copy.d.ts +17 -0
- package/dist/lib/components/ui/collections/dnd/index.d.ts +1 -1
- package/dist/lib/components/ui/collections/row-menu/index.d.ts +1 -0
- package/dist/lib/components/ui/collections/row-menu/row-menu.d.ts +6 -1
- package/dist/lib/components/ui/collections/row-menu/use-row-menu-focus.d.ts +30 -0
- package/dist/lib/components/ui/display/enrolment-card/use-enrolment-card-labels.d.ts +1 -1
- package/dist/lib/components/ui/display/recovery-codes/use-recovery-codes-labels.d.ts +1 -1
- package/dist/lib/components/ui/display/recovery-codes-status/use-recovery-codes-status-labels.d.ts +1 -1
- package/dist/lib/components/ui/display/stat-card/partials/stat-card-link-parts.d.ts +7 -0
- package/dist/lib/components/ui/display/stat-card/stat-card.d.ts +6 -1
- package/dist/lib/components/ui/display/stat-card/types.d.ts +14 -0
- package/dist/lib/components/ui/feedback/step-up-dialog/use-step-up-labels.d.ts +1 -1
- package/dist/lib/components/ui/fields/contacts/placeholders.d.ts +1 -1
- package/dist/lib/components/ui/fields/number-field/number-control.d.ts +4 -1
- package/dist/lib/components/ui/fields/number-field/number-stepper.d.ts +5 -0
- package/dist/lib/components/ui/fields/verification-code/use-verification-labels.d.ts +1 -1
- package/dist/lib/components/ui/layout/app-shell/route-transition.d.ts +1 -1
- package/dist/lib/components/ui/media/media-player-host/use-host-engine.d.ts +1 -1
- package/dist/lib/components/ui/primitives/link/link-component-provider.d.ts +1 -1
- package/dist/lib/components/ui/primitives/link/nav-current.d.ts +22 -0
- package/dist/lib/components/ui/primitives/link/router-link.d.ts +25 -0
- package/dist/lib/components/ui/primitives/tabs/index.d.ts +2 -0
- package/dist/lib/components/ui/primitives/tabs/internal.d.ts +2 -0
- package/dist/lib/components/ui/primitives/tabs/partials/tabs-nav-link.d.ts +22 -0
- package/dist/lib/components/ui/primitives/tabs/tab-param-url.d.ts +53 -0
- package/dist/lib/components/ui/primitives/tabs/tabs-nav.d.ts +22 -0
- package/dist/lib/components/ui/primitives/tabs/tabs-selectors.d.ts +8 -0
- package/dist/lib/components/ui/primitives/tabs/use-tab-param.d.ts +35 -0
- package/dist/lib/components/ui/primitives/tabs/use-tabs-overflow.d.ts +4 -1
- package/dist/lib/components/ui/primitives/toggle-group/index.d.ts +1 -0
- package/dist/lib/components/ui/primitives/toggle-group/segmented-nav-item.d.ts +20 -0
- package/dist/lib/components/ui/primitives/toggle-group/segmented-nav.d.ts +22 -0
- package/dist/lib/components/ui/primitives/toggle-group/toggle-group.d.ts +4 -3
- package/dist/lib/i18n/resources/en/en.d.ts +5 -0
- package/dist/lib/i18n/resources/en/material.json.d.ts +6 -1
- package/dist/lib/i18n/resources/fr/fr.d.ts +5 -0
- package/dist/lib/i18n/resources/fr/material.json.d.ts +6 -1
- package/dist/lib/i18n/resources/resources.d.ts +10 -0
- package/dist/lib/i18n/translator.d.ts +1 -1
- package/dist/lib.js +31 -54
- package/dist/main.js +42 -42
- package/dist/map.js +8 -8
- package/dist/material.css +1 -1
- package/dist/pdf-reader.js +1 -1
- package/dist/print.js +1 -1
- package/dist/{public-B-greXGt.js → public-AXwv1h3v.js} +10 -10
- package/dist/{rich-code-block-Cv6s59vB.js → rich-code-block-qSvkA5rI.js} +5 -5
- package/dist/{sortable-provider-DVmZcglU.js → sortable-provider-DPk-RVEU.js} +3 -3
- package/dist/{spinner-OQAweH2r.js → spinner-PdSk7zEs.js} +1 -1
- package/dist/theme-picker.js +2 -2
- package/dist/{theme-toggle-DPx44p6a.js → theme-toggle-CZQckLDd.js} +2 -2
- package/dist/{toggle-group-BkH23Jk4.js → toggle-group-DXXlqid2.js} +1 -1
- package/dist/{toolbar-items-CQ3S_XZw.js → toolbar-items-BmNYNaRB.js} +3 -3
- package/dist/{translate-material-BHL4g2UB.js → translate-material-BlCQ6AW9.js} +1 -1
- package/dist/{translator-BJsjXcAp.js → translator-ChpeKCO3.js} +8 -2
- package/dist/{typography-CwhEwPay.js → typography-C4JSRr2B.js} +1 -1
- package/dist/{use-chat-composer-R0vng5RU.js → use-chat-composer-OUa9akfM.js} +1725 -1719
- package/dist/{use-heading-spy-Brcn0l_J.js → use-heading-spy-K-HUp4o_.js} +4 -4
- package/dist/{use-material-translation-mY3U83TV.js → use-material-translation-DkE9J5NE.js} +1 -1
- package/dist/{use-return-focus-BQ413TNG.js → use-return-focus-KypnR7vv.js} +1 -1
- package/docs/components/README.md +3 -1
- package/docs/components/buttons-and-actions.md +50 -0
- package/docs/components/data-display.md +14 -1
- package/docs/components/data-table-filter-and-lists.md +68 -4
- package/docs/components/form-fields.md +9 -1
- package/docs/components/primitives.md +209 -0
- package/docs/previews/buttons-actions-backlink.webp +0 -0
- package/docs/previews/buttons-actions-sharebutton.webp +0 -0
- package/docs/previews/primitives-segmentednav.webp +0 -0
- package/docs/previews/primitives-tabsnav.webp +0 -0
- package/docs/roadmap.md +2 -1
- package/docs/troubleshooting.md +42 -3
- package/lib/components/components.css +2 -0
- package/lib/components/ui/layout/app-shell/activity-backdrop.css +4 -3
- package/lib/components/ui/primitives/tabs/tabs-nav-forced.css +27 -0
- package/lib/components/ui/primitives/toggle-group/toggle-group-segmented.css +10 -2
- package/lib/components/ui/primitives/toggle-group/toggle-group.css +5 -1
- package/package.json +1 -1
- /package/dist/lib/{components/ui/collections/dnd → lib}/compose-refs.d.ts +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as e } from "./utils-BLfntITJ.js";
|
|
2
|
-
import { a as t, c as n, d as r, i, n as a, r as o, t as s, u as c } from "./alert-dialog-
|
|
3
|
-
import { a as l } from "./button-
|
|
4
|
-
import { i as u } from "./translate-material-
|
|
5
|
-
import { t as d } from "./use-material-translation-
|
|
2
|
+
import { a as t, c as n, d as r, i, n as a, r as o, t as s, u as c } from "./alert-dialog-BgQDxcYh.js";
|
|
3
|
+
import { a as l } from "./button-2de_lrix.js";
|
|
4
|
+
import { i as u } from "./translate-material-BlCQ6AW9.js";
|
|
5
|
+
import { t as d } from "./use-material-translation-DkE9J5NE.js";
|
|
6
6
|
import { _ as f, g as p, h as m, n as h, v as g, y as _ } from "./number.utils-BG_eEt5g.js";
|
|
7
7
|
import { useCallback as v, useEffect as y, useMemo as b, useRef as x, useState as S } from "react";
|
|
8
8
|
import { jsx as C, jsxs as w } from "react/jsx-runtime";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { o as e, r as t } from "./dist-DVRQkaTK.js";
|
|
2
2
|
import { a as n, c as r, i, l as a, o, r as s, s as c } from "./dist-lmHmz_bu.js";
|
|
3
|
-
import { C as l, S as u, w as d } from "./spinner-
|
|
3
|
+
import { C as l, S as u, w as d } from "./spinner-PdSk7zEs.js";
|
|
4
4
|
import { c as f, i as p, l as m, o as h, r as g, s as ee } from "./in-dialog-Vwkyp-M5.js";
|
|
5
5
|
import { r as _ } from "./escape-owner-DpaP0lm1.js";
|
|
6
6
|
import * as v from "react";
|
|
@@ -14,10 +14,12 @@ What the stylesheet gives any element, not a component: the animation classes (`
|
|
|
14
14
|
|
|
15
15
|
## [Primitives](primitives.md)
|
|
16
16
|
|
|
17
|
-
Building blocks with no opinion of their own: card surface, typography, label, input, textarea, input group, icon, kbd, separator, scroll area, table, tabs, toggle, accordion, collapsible.
|
|
17
|
+
Building blocks with no opinion of their own: card surface, typography, label, input, textarea, input group, icon, kbd, separator, scroll area, table, tabs, route tabs, toggle, segmented route links, accordion, collapsible. 22 entries.
|
|
18
18
|
|
|
19
19
|
[<img src="../previews/primitives-card.webp" height="72" alt="Card">](primitives.md#card)
|
|
20
20
|
[<img src="../previews/primitives-tabs.webp" height="72" alt="Tabs">](primitives.md#tabs)
|
|
21
|
+
[<img src="../previews/primitives-tabsnav.webp" height="72" alt="TabsNav">](primitives.md#tabsnav)
|
|
22
|
+
[<img src="../previews/primitives-segmentednav.webp" height="72" alt="SegmentedNav">](primitives.md#segmentednav)
|
|
21
23
|
[<img src="../previews/primitives-inputgroup.webp" height="72" alt="InputGroup">](primitives.md#inputgroup)
|
|
22
24
|
[<img src="../previews/primitives-table.webp" height="72" alt="Table">](primitives.md#table)
|
|
23
25
|
|
|
@@ -15,7 +15,9 @@ and language switches. Everything is imported from `@nxgt/material/components`.
|
|
|
15
15
|
| <img src="../previews/buttons-actions-toolbar.webp" width="96" alt=""> | [Toolbar](#toolbar) | A row or column of icon actions, floating or docked, one tab stop, overflow into More |
|
|
16
16
|
| <img src="../previews/buttons-actions-responsivebutton.webp" width="96" alt=""> | [ResponsiveButton](#responsivebutton) | Label on desktop, icon-only on mobile |
|
|
17
17
|
| <img src="../previews/buttons-actions-linkbutton.webp" width="96" alt=""> | [LinkButton](#linkbutton) | The button styles as `react-router` links |
|
|
18
|
+
| <img src="../previews/buttons-actions-backlink.webp" width="96" alt=""> | [BackLink](#backlink) | A link to the parent page with a leading arrow |
|
|
18
19
|
| <img src="../previews/buttons-actions-copybutton.webp" width="96" alt=""> | [CopyButton](#copybutton) | Copies a value, shows a check mark |
|
|
20
|
+
| <img src="../previews/buttons-actions-sharebutton.webp" width="96" alt=""> | [ShareButton](#sharebutton) | Opens the system share sheet, copies the link where there is none |
|
|
19
21
|
| <img src="../previews/buttons-actions-highlighttext.webp" width="96" alt=""> | [HighlightText](#highlighttext) | Marks matches of a query inside a string |
|
|
20
22
|
| <img src="../previews/buttons-actions-loadmorebutton.webp" width="96" alt=""> | [LoadMoreButton](#loadmorebutton) | "Load more" for cursor pagination |
|
|
21
23
|
| <img src="../previews/buttons-actions-loadmorefooter.webp" width="96" alt=""> | [LoadMoreFooter](#loadmorefooter) | Shown-of-total line, load button and a failed state with retry, for a paged list |
|
|
@@ -889,6 +891,26 @@ Needs a `react-router` router above it.
|
|
|
889
891
|
|
|
890
892
|
---
|
|
891
893
|
|
|
894
|
+
## BackLink
|
|
895
|
+
|
|
896
|
+

|
|
897
|
+
|
|
898
|
+
A link to the parent page, for the top of a record, settings or detail page: a ghost `LinkButton` with a leading arrow.
|
|
899
|
+
|
|
900
|
+
```tsx
|
|
901
|
+
import { BackLink } from '@nxgt/material/components';
|
|
902
|
+
|
|
903
|
+
<BackLink to="/items">All items</BackLink>;
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
It renders one `<a>`. The label names the destination and is the link's accessible name; the arrow is hidden from assistive tech and turns in right-to-left pages. On a touch screen the link offers a 44px hit area, as every button does.
|
|
907
|
+
|
|
908
|
+
It takes every [`LinkButton`](#linkbutton) prop (`to`, `variant`, `color`, `size`, `replace`, `state`, `linkComponent`, ...), so it follows the router or the [`LinkComponentProvider`](./primitives.md#linkcomponentprovider). Only the default differs: `variant` is `'ghost'`.
|
|
909
|
+
|
|
910
|
+
**Storybook:** `Buttons & actions/BackLink` (`Default`, `LinkStyle`, `Rtl`) · **Vue:** none yet
|
|
911
|
+
|
|
912
|
+
---
|
|
913
|
+
|
|
892
914
|
## CopyButton
|
|
893
915
|
|
|
894
916
|

|
|
@@ -919,6 +941,34 @@ The write is awaited: "Copied" appears only once the clipboard accepted the text
|
|
|
919
941
|
|
|
920
942
|
---
|
|
921
943
|
|
|
944
|
+
## ShareButton
|
|
945
|
+
|
|
946
|
+

|
|
947
|
+
|
|
948
|
+
Shares a link, a text or a title through the system share sheet (`navigator.share`) and copies the link instead where the sheet is missing, or when it fails for a reason other than the user closing it. A closed sheet (`AbortError`) does nothing. It reuses `CopyButton`'s status: "Copied" or "Copy failed" is announced through a visually hidden `role="status"` region, and the button keeps its name.
|
|
949
|
+
|
|
950
|
+
```tsx
|
|
951
|
+
import { ShareButton } from '@nxgt/material/components';
|
|
952
|
+
|
|
953
|
+
<ShareButton title="Join the room" url="https://app.example.com/rooms/42" />;
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
| Prop | Type | Default | Effect |
|
|
957
|
+
| --- | --- | --- | --- |
|
|
958
|
+
| `url`, `text`, `title` | `string` | — | what is shared; the copy fallback puts the `url`, else the `text`, else the `title` on the clipboard |
|
|
959
|
+
| `label` | `string` | `"Share"` (translated, `material.share.share`) | the accessible name; it never changes after a click |
|
|
960
|
+
| `copiedLabel`, `failedLabel` | `string` | `material.copy.copied` / `.failed` | announced after the fallback copy |
|
|
961
|
+
| `icon` | `ReactNode` | `Share2` | the idle icon |
|
|
962
|
+
| `onShare` | `(outcome) => void` | — | `'shared'`, `'cancelled'`, `'copied'` or `'failed'` |
|
|
963
|
+
|
|
964
|
+
Other props pass through to `IconButton` (`variant` defaults to `ghost`, `color` to `default`). The types `ShareData` (`{ title?, text?, url? }`) and `ShareOutcome` (`'shared' | 'cancelled' | 'copied' | 'failed'`) are exported.
|
|
965
|
+
|
|
966
|
+
`navigator` is read when the button is clicked, never while rendering, so the component is safe to server-render. The name is also exported as `SHARE_BUTTON_LABEL` for tests (`getByRole('button', { name: SHARE_BUTTON_LABEL })`).
|
|
967
|
+
|
|
968
|
+
**Storybook:** `Buttons & actions/ShareButton` · **Vue:** not yet (tracked as a `vue` issue)
|
|
969
|
+
|
|
970
|
+
---
|
|
971
|
+
|
|
922
972
|
## HighlightText
|
|
923
973
|
|
|
924
974
|

|
|
@@ -16,7 +16,7 @@ from `@nxgt/material/components`.
|
|
|
16
16
|
| <img src="../previews/data-display-dayseparator.webp" width="96" alt=""> | [DaySeparator](#dayseparator) | "Today", "Yesterday" or the date between two hairlines |
|
|
17
17
|
| <img src="../previews/data-display-periodcards.webp" width="96" alt=""> | [PeriodCards](#periodcards) | Consecutive date ranges (terms, sprints) as cards, with a current marker and an optional link per card |
|
|
18
18
|
| <img src="../previews/data-display-chip.webp" width="96" alt=""> | [Chip](#chip) | Three looks and a dashed add pill, with slots, a selected state and a dismiss button |
|
|
19
|
-
| <img src="../previews/data-display-statcard.webp" width="96" alt=""> | [StatCard](#statcard) | KPI tile with value, hint, icon and toned delta |
|
|
19
|
+
| <img src="../previews/data-display-statcard.webp" width="96" alt=""> | [StatCard](#statcard) | KPI tile with value, hint, icon and toned delta; `href` makes it one link |
|
|
20
20
|
| <img src="../previews/data-display-metric-cards.webp" width="96" alt=""> | [Metric cards](#metric-cards) | Sixteen dashboard tiles |
|
|
21
21
|
| <img src="../previews/data-display-card-presets.webp" width="96" alt=""> | [Card presets](#card-presets) | Twenty-seven ready-made cards |
|
|
22
22
|
| <img src="../previews/data-display-statusindicator.webp" width="96" alt=""> | [StatusIndicator](#statusindicator) | Coloured status dot with a label |
|
|
@@ -591,11 +591,24 @@ import { StatCard } from '@nxgt/material/components';
|
|
|
591
591
|
| `variant` | `'outlined' \| 'tonal' \| 'filled'` | `'outlined'` | `tonal` paints the card with the tone's container and a fine outline in its text colour (3:1 against the page); `filled` paints it with the tone and draws everything on it in the on-colour. Both use `neutral` when there is no `tone`. Every text is 4.5:1 or more. In a [calm zone](../guides/theming.md#calm-zone), a `tonal` or `filled` card is the card's own surface with the tone's ring, and its icon chip a ring |
|
|
592
592
|
| `valueSize` | `'default' \| 'display'` | `'default'` | `display` draws the value larger (`headline-large`) |
|
|
593
593
|
| `size` | `'default' \| 'sm'` | `'default'` | `sm` is a compact tile for a band or a header (see below); it ignores `hint`, `delta` and `valueSize` |
|
|
594
|
+
| `href` | `string` | — | the whole card is one link: an `<a>` around the card (one tab stop), named by its label and value, with a trailing chevron (so it reads as a link without hover), a hover border and a focus-visible ring. Without `href` the card is a plain `div`. An external address (`https://`, `mailto:`) is a plain `<a target="_blank" rel="noreferrer">`. `className` and the other props still go on the card |
|
|
595
|
+
| `linkComponent` | `LinkComponent` | a plain `<a>`, or the [`LinkComponentProvider`](./primitives.md#linkcomponentprovider)'s | with `href`: renders the link with your router's link, given `href`. It must spread every prop it receives (`className`, handlers, `data-*`) and forward `ref` |
|
|
594
596
|
|
|
595
597
|
```tsx
|
|
596
598
|
<StatCard tone="success" variant="tonal" valueSize="display" label="Revenue" value="€48,200" delta={12.4} icon={<CreditCard />} />
|
|
597
599
|
```
|
|
598
600
|
|
|
601
|
+
**A tile that leads somewhere.** `href` makes the whole card one link, so a count per category can open its list; the focus ring, hover border and chevron are the card's own, with nothing to restyle.
|
|
602
|
+
|
|
603
|
+
```tsx
|
|
604
|
+
<StatCard href="/revenue" label="Revenue" value="€48,200" />
|
|
605
|
+
|
|
606
|
+
// With your router: wrap its Link so it takes `href`, once for every link in the app
|
|
607
|
+
// with <LinkComponentProvider component={MyLink}>, or for this card only:
|
|
608
|
+
const MyLink = ({ href, ...props }: LinkComponentProps) => <Link to={href} {...props} />;
|
|
609
|
+
<StatCard linkComponent={MyLink} href="/items?state=a" label="Items" value={12} />
|
|
610
|
+
```
|
|
611
|
+
|
|
599
612
|
**Compact tile (`size="sm"`).** For figures in a header band: a tile 40 to 56 px tall that hugs its content, the optional icon chip, then the value (tabular figures) over its label. It takes the same `tone` and `variant` (`tonal`, `outlined`, `filled`); `hint`, `delta` and `valueSize` are ignored (nothing is drawn for them, and `loading` shows a skeleton in place of the value only). Tiles in a `flex flex-wrap gap-2` row sit side by side and wrap on a phone; for an even layout use `StatCardGroup`.
|
|
600
613
|
|
|
601
614
|
```tsx
|
|
@@ -15,7 +15,7 @@ either can be used alone.
|
|
|
15
15
|
| --- | --- | --- |
|
|
16
16
|
| <img src="../previews/data-table-filter-lists-datatable.webp" width="96" alt=""> | [DataTable](#datatable) | TanStack Table v9 with sorting, selection, expansion and pagination, client-side or one server page at a time |
|
|
17
17
|
| <img src="../previews/data-table-filter-lists-servertable.webp" width="96" alt=""> | [ServerTable](#servertable) | A server-mode `DataTable` driven by the URL, with the Filter in its top band (a title and a description, or neither): sort, cursor paging, the empty page with a scene, an action and removable chips, one count announced |
|
|
18
|
-
| <img src="../previews/data-table-filter-lists-rowmenu.webp" width="96" alt=""> | [RowMenu](#rowmenu) | A row's "..." button named after its row ("Actions for Ada"), its menu aligned to the end with the destructive item last, a loading state while an action runs,
|
|
18
|
+
| <img src="../previews/data-table-filter-lists-rowmenu.webp" width="96" alt=""> | [RowMenu](#rowmenu) | A row's "..." button named after its row ("Actions for Ada"), its menu aligned to the end with the destructive item last, a loading state while an action runs, `rowMenuColumn` to drop both in a table, and `useRowMenuFocus` to hand the focus back to a row's button after an action |
|
|
19
19
|
| <img src="../previews/data-table-filter-lists-listempty.webp" width="96" alt=""> | [ListEmpty](#listempty) | A list's empty page: nothing yet, with a scene and an action, or nothing matches and the way out |
|
|
20
20
|
| <img src="../previews/data-table-filter-lists-pagefooter.webp" width="96" alt=""> | [PageFooter](#pagefooter) | The page footer of a feed that is not a table (cards, a news feed) under a FilterBar: the range and Previous / Next as links from the URL's cursor trail, `pageControls="icon"`; no `totalCount` for a feed with no total (no range, Previous / Next only); `hideOnSinglePage` hides it on a single page |
|
|
21
21
|
| <img src="../previews/data-table-filter-lists-datagrid.webp" width="96" alt=""> | [DataGrid](#datagrid) | An editable grid: spreadsheet keys, paste, a fill handle with series, undo, totals, saving with versions and conflicts (lazy ones fetch theirs on "Keep mine"), `onRefuse` for a refused paste or fill, row selection (`enableRowSelection`), and resizable, hideable and reorderable columns (`columnControls`, `columnLayout`, `DataGridColumnsMenu`) |
|
|
@@ -1217,6 +1217,7 @@ and `ref`, so it is the child of a `DropdownMenuTrigger asChild`.
|
|
|
1217
1217
|
| `loading` (`RowMenuTrigger`, `triggerProps.loading` of `RowMenu`) | `boolean` | `false` | an action chosen from this row's menu is running: `aria-busy`, a spinner in place of the dots (the button keeps its size), and the menu does not open again until it is over. The button stays focusable and keeps the focus. As `Button`'s `loading` |
|
|
1218
1218
|
| `items` (`RowMenu`) | `{ label, icon?, onSelect?, disabled?, destructive?, id? }[]` | `[]` | the actions; `destructive` ones are drawn in the error colour and moved last, after a separator |
|
|
1219
1219
|
| `children` (`RowMenu`) | `ReactNode` | — | menu content of your own, above `items` (put a destructive item last yourself); its menu items are 44px high on a coarse pointer too |
|
|
1220
|
+
| `rowId` (`RowMenu`) | `string` | — | the row's id, written on the button (`data-row-menu-id`) for `useRowMenuFocus`; `rowMenuColumn` sets it from the table's `getRowId` |
|
|
1220
1221
|
| `triggerProps` (`RowMenu`) | button props | — | `disabled`, `className`, `data-*` on the button |
|
|
1221
1222
|
| `open`, `onOpenChange` (`RowMenu`) | as `DropdownMenu` | — | controlled state |
|
|
1222
1223
|
| `label`, `items`, `menu` (`rowMenuColumn`) | `(row) => string` / `(row) => RowMenuItem[]` / `(row) => ReactNode` | — | the row's name, its actions as data, or its menu content as nodes |
|
|
@@ -1233,9 +1234,72 @@ the focus, so the focus does not fall to the page when the menu closes:
|
|
|
1233
1234
|
/>
|
|
1234
1235
|
```
|
|
1235
1236
|
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1237
|
+
### Handing the focus back to a row
|
|
1238
|
+
|
|
1239
|
+
A toast or a dialog opened by an action takes the focus, and when it closes the
|
|
1240
|
+
focus is on the page's body. `useRowMenuFocus()` returns a `focusRef` for the
|
|
1241
|
+
table and `focusRowMenu(rowId)`, which focuses that row's button once the
|
|
1242
|
+
action has settled or the dialog has closed. The row is matched by its id
|
|
1243
|
+
(`getRowId`'s, written by `rowMenuColumn`; `rowId` on a bare `RowMenu`) exactly,
|
|
1244
|
+
never by its label, so two "Ada" rows are told apart. The focus is
|
|
1245
|
+
taken at once, and claimed again for two frames if a dialog's focus restore or
|
|
1246
|
+
a toast takes it, so it is safe to call right after the action settles or the
|
|
1247
|
+
dialog closes. For those two frames the focus is claimed back to the trigger
|
|
1248
|
+
whatever took it, so a deliberate focus move (Tab, or a dialog or toast opened
|
|
1249
|
+
right after the call) within them is overridden: call it once the follow-up UI
|
|
1250
|
+
is done. It returns `true` when the button was found and holds the focus,
|
|
1251
|
+
and `false` when the row is gone (archived) or the button cannot take it
|
|
1252
|
+
(disabled, inert); the focus then stays where it is.
|
|
1253
|
+
|
|
1254
|
+
```tsx
|
|
1255
|
+
import { useMemo } from 'react';
|
|
1256
|
+
import {
|
|
1257
|
+
DataTable,
|
|
1258
|
+
rowMenuColumn,
|
|
1259
|
+
toast,
|
|
1260
|
+
useRowMenuFocus,
|
|
1261
|
+
} from '@nxgt/material/components';
|
|
1262
|
+
|
|
1263
|
+
const { focusRef, focusRowMenu } = useRowMenuFocus();
|
|
1264
|
+
|
|
1265
|
+
const columns = useMemo(
|
|
1266
|
+
() => [
|
|
1267
|
+
// …
|
|
1268
|
+
rowMenuColumn<Person>({
|
|
1269
|
+
label: (person) => person.name,
|
|
1270
|
+
items: (person) => [
|
|
1271
|
+
{
|
|
1272
|
+
label: 'Rename',
|
|
1273
|
+
onSelect: async () => {
|
|
1274
|
+
await rename(person.id);
|
|
1275
|
+
toast.success('Renamed');
|
|
1276
|
+
focusRowMenu(person.id);
|
|
1277
|
+
},
|
|
1278
|
+
},
|
|
1279
|
+
],
|
|
1280
|
+
}),
|
|
1281
|
+
],
|
|
1282
|
+
[focusRowMenu],
|
|
1283
|
+
);
|
|
1284
|
+
|
|
1285
|
+
<DataTable focusRef={focusRef} getRowId={(p) => p.id} columns={columns} data={people} />;
|
|
1286
|
+
```
|
|
1287
|
+
|
|
1288
|
+
`focusRef` scopes the search to that table; without it the whole document is
|
|
1289
|
+
searched. For a dialog, [`onCloseAutoFocus`](#focus-after-a-bulk-action) with
|
|
1290
|
+
`preventDefault()` is the precise hook if you want to take the focus yourself.
|
|
1291
|
+
|
|
1292
|
+
`findRowMenuTrigger(rowId, scope?)` is the lookup the hook uses: it returns the
|
|
1293
|
+
row's button (an `HTMLElement`) inside `scope` (default `document`), or `null`.
|
|
1294
|
+
Use it instead of the hook when you only need the element (to scroll it into
|
|
1295
|
+
view, to measure it) or when the rows are outside a React component.
|
|
1296
|
+
|
|
1297
|
+
The focus returns to the button by itself when the menu closes, and when a
|
|
1298
|
+
dialog opened from an item (a `ConfirmDialog`, a `Dialog`, a `Sheet`) closes:
|
|
1299
|
+
the item is gone by then, so the dialog hands the focus back to the menu's
|
|
1300
|
+
button. `useRowMenuFocus` is for the focus that is lost after that: an async
|
|
1301
|
+
action that settles later, a toast, a dialog opened later or from elsewhere, or
|
|
1302
|
+
a table re-render that remounts the button.
|
|
1239
1303
|
|
|
1240
1304
|
**Storybook:** `Data table, filter & lists/RowMenu`, `Guides/Accessibility/Touch targets/Row menu` · **Vue:** not yet (tracked as a `vue` issue)
|
|
1241
1305
|
|
|
@@ -463,6 +463,14 @@ export function Quantity() {
|
|
|
463
463
|
| `onBlur` | `FocusEventHandler` | — | after the typed text is read and `onChange` called |
|
|
464
464
|
| `ref` | `Ref<HTMLInputElement>` | — | reaches the text input (React Hook Form's `field.ref`) |
|
|
465
465
|
|
|
466
|
+
**Stepper names.** Each +/− button is named after its field: "Increase
|
|
467
|
+
Quantity" / "Decrease Quantity" (`material.number-field.increase-named` and
|
|
468
|
+
`decrease-named`, with the `label` interpolated), so a form with several number
|
|
469
|
+
fields never gives a screen reader identical buttons. The name comes from a
|
|
470
|
+
plain-text `label`, else from `aria-label`; with neither (a field named only
|
|
471
|
+
through `aria-labelledby` included), the buttons keep the bare "Increase" /
|
|
472
|
+
"Decrease".
|
|
473
|
+
|
|
466
474
|
**Decimal separator.** After blur (and after +/−) the value is shown with the
|
|
467
475
|
active language's separator, from `I18nProvider`: `1,5` in French, `1.5` in
|
|
468
476
|
English (no grouping, so it stays editable). Typing accepts both: `1,5` and
|
|
@@ -476,7 +484,7 @@ Form's `field.ref` the input itself: `setFocus('amount')` and
|
|
|
476
484
|
**Emits:** `number | null`.
|
|
477
485
|
**Form wrapper:** `NumberFormField` — `control` and `name`, see
|
|
478
486
|
[forms](../guides/forms.md#the-pair) and [what each field emits](../guides/forms.md#what-each-field-emits).
|
|
479
|
-
**Storybook:** `Form fields/NumberField` · **Vue:** `NumberField`, `NumberFormField` (the language separator is React only for now, #763)
|
|
487
|
+
**Storybook:** `Form fields/NumberField` · **Vue:** `NumberField`, `NumberFormField` (the language separator is React only for now, #763; the stepper names, #2060)
|
|
480
488
|
|
|
481
489
|
---
|
|
482
490
|
|
|
@@ -24,6 +24,8 @@ other pages are built from these.
|
|
|
24
24
|
| <img src="../previews/primitives-scrollarea.webp" width="96" alt=""> | [ScrollArea](#scrollarea) | Scroll container with a thin themed scrollbar |
|
|
25
25
|
| <img src="../previews/primitives-table.webp" width="96" alt=""> | [Table](#table) | Styled semantic table inside a scrollable wrapper |
|
|
26
26
|
| <img src="../previews/primitives-tabs.webp" width="96" alt=""> | [Tabs](#tabs) | Tabs whose triggers can take an icon |
|
|
27
|
+
| <img src="../previews/primitives-tabsnav.webp" width="96" alt=""> | [TabsNav](#tabsnav) | Route tabs as links, with `aria-current` |
|
|
28
|
+
| <img src="../previews/primitives-segmentednav.webp" width="96" alt=""> | [SegmentedNav](#segmentednav) | Segmented links, with `aria-current` and the pill |
|
|
27
29
|
| <img src="../previews/primitives-toggle.webp" width="96" alt=""> | [Toggle](#toggle) | Pressable on / off button |
|
|
28
30
|
| <img src="../previews/primitives-togglegroup.webp" width="96" alt=""> | [ToggleGroup](#togglegroup) | Connected, segmented or separate toggle group |
|
|
29
31
|
| | [Activity](#activity) | React `<Activity>` switched by a boolean |
|
|
@@ -784,6 +786,8 @@ indicator is placed inside it, measured against it, so the child must render
|
|
|
784
786
|
its children directly: a component child that wraps them in an inner element
|
|
785
787
|
hides the indicator.
|
|
786
788
|
|
|
789
|
+
For tabs that go to routes, use [TabsNav](#tabsnav): real links with `aria-current`, not tabs with panels.
|
|
790
|
+
|
|
787
791
|
`TabsTrigger asChild` makes your link the tab (`<TabsTrigger value="a" asChild><Link to="/a">A</Link></TabsTrigger>`): the indicator and the icon and label spans go inside it. `ToggleGroupItem`, `AccordionTrigger`, `SelectTrigger` and `ExtendedLabel` take `asChild` the same way.
|
|
788
792
|
|
|
789
793
|
`dir="rtl"` on `Tabs` (or Radix's `DirectionProvider`) runs the tabs from the
|
|
@@ -822,6 +826,211 @@ as `segmented`. Both still type-check and render; write `underline` or
|
|
|
822
826
|
|
|
823
827
|
---
|
|
824
828
|
|
|
829
|
+
## TabsNav
|
|
830
|
+
|
|
831
|
+

|
|
832
|
+
|
|
833
|
+
Route tabs as links: a `nav` of links in the `Tabs` look, with `aria-current="page"` on the current one, the sliding indicator and the phone scroll cue.
|
|
834
|
+
|
|
835
|
+
```tsx
|
|
836
|
+
import { TabsNav, TabsNavLink } from '@nxgt/material/components';
|
|
837
|
+
|
|
838
|
+
<TabsNav aria-label="Account sections" value="billing">
|
|
839
|
+
<TabsNavLink to="/profile" value="profile">
|
|
840
|
+
Profile
|
|
841
|
+
</TabsNavLink>
|
|
842
|
+
<TabsNavLink to="/billing" value="billing">
|
|
843
|
+
Billing
|
|
844
|
+
</TabsNavLink>
|
|
845
|
+
</TabsNav>;
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
Use `Tabs` when the panels are on the page and `TabsNav` when each tab is a
|
|
849
|
+
place to go: every tab is a real link, so it opens in a new window and its
|
|
850
|
+
address can be copied. `aria-label` names the navigation and is required.
|
|
851
|
+
|
|
852
|
+
- **Current link**: `TabsNav`'s `value` is matched against each link's
|
|
853
|
+
`value`; a link's `current` boolean overrides it (`current={false}` to
|
|
854
|
+
switch one off). The current link carries `aria-current="page"` and
|
|
855
|
+
`data-state="active"`, which is what the indicator follows.
|
|
856
|
+
- **Keyboard**: the links are plain links, so Tab moves from one to the next;
|
|
857
|
+
the arrow keys do nothing (they are not tabs, there is no `tablist`).
|
|
858
|
+
- **Looks**: `variant="underline"` (the default) or `"segmented"`, as `Tabs`.
|
|
859
|
+
A nav wider than its container scrolls inside itself, with the same edge
|
|
860
|
+
fades and scroll buttons as `Tabs`. On a coarse pointer each link is 44px
|
|
861
|
+
tall at least.
|
|
862
|
+
- **The link**: `TabsNavLink` takes `LinkButton`'s props (`to`, `replace`,
|
|
863
|
+
`state`, `target`, `reloadDocument`, `download`, `linkComponent`) and an
|
|
864
|
+
`icon`. It renders React Router's `Link`, or the `LinkComponentProvider`'s
|
|
865
|
+
component (given `href`). `preventScrollReset` is on by default, so changing
|
|
866
|
+
tab keeps the scroll position; pass `preventScrollReset={false}` to reset.
|
|
867
|
+
- A tab bound to the URL's search params: [`useTabParam`](#route-tabs-in-the-url-usetabparam).
|
|
868
|
+
|
|
869
|
+
**It needs a Router.** Without a `LinkComponentProvider`, `TabsNavLink` renders
|
|
870
|
+
React Router's `Link`, as `LinkButton` does, so it throws outside a Router
|
|
871
|
+
(`useHref() may be used only in the context of a <Router> component`): render
|
|
872
|
+
it inside your router, or set a [`LinkComponentProvider`](#linkcomponentprovider).
|
|
873
|
+
See [the troubleshooting entry](../troubleshooting.md#usehref-may-be-used-only-in-the-context-of-a-router-component-on-a-tabsnav-segmentednav-or-a-linkbutton).
|
|
874
|
+
|
|
875
|
+
| Prop | On | Type | Default | Effect |
|
|
876
|
+
| --- | --- | --- | --- | --- |
|
|
877
|
+
| `aria-label` | `TabsNav` | `string` | — (required) | names the navigation |
|
|
878
|
+
| `value` | `TabsNav` | `string` | — | the current link's `value` |
|
|
879
|
+
| `variant` | `TabsNav` | `'underline' \| 'segmented'` | `'underline'` | the look, as `Tabs` |
|
|
880
|
+
| `to` | `TabsNavLink` | `To` | — (required) | the route, as for `LinkButton` |
|
|
881
|
+
| `value` | `TabsNavLink` | `string` | — | matched against the nav's `value` |
|
|
882
|
+
| `current` | `TabsNavLink` | `boolean` | — | marks the link current or not, over `value` |
|
|
883
|
+
| `icon` | `TabsNavLink` | `ReactNode` | — | shown before the label |
|
|
884
|
+
| `preventScrollReset` | `TabsNavLink` | `boolean` | `true` | keeps the scroll position when the tab changes |
|
|
885
|
+
|
|
886
|
+
### Route tabs in the URL: `useTabParam`
|
|
887
|
+
|
|
888
|
+
`useTabParam({ param = 'tab', values, defaultValue?, explicitDefault?, keepHash? })` keeps
|
|
889
|
+
the current tab in a search param and feeds `TabsNav` or `SegmentedNav`. It
|
|
890
|
+
returns `value` (the param if it is one of `values`, else `defaultValue`, else
|
|
891
|
+
the first value: an unknown value falls back) and `hrefFor(tab)`, the `to` of a
|
|
892
|
+
tab. `hrefFor` keeps the path and the other params; the default tab writes
|
|
893
|
+
**no** param. It **drops the hash**: React Router's `ScrollRestoration` jumps
|
|
894
|
+
to a hash even with `preventScrollReset`, so a kept `#section` would scroll the
|
|
895
|
+
page on every tab click. Pass `keepHash: true` when you want it kept.
|
|
896
|
+
Each tab is a navigation, so Back walks the tabs and a
|
|
897
|
+
router blocker sees them; `preventScrollReset` (on by default) keeps the scroll.
|
|
898
|
+
|
|
899
|
+
`skipTabChange(param = 'tab')` is for the route's `shouldRevalidate`: it returns
|
|
900
|
+
`false` when a navigation changed only that param, so the loader is not read
|
|
901
|
+
again for a tab switch. A same-URL, fetcher or manual revalidation, and any
|
|
902
|
+
submission, still return `defaultShouldRevalidate`. Parent routes' loaders
|
|
903
|
+
still revalidate unless they export `skipTabChange` too. It is typed as a
|
|
904
|
+
`ShouldRevalidateFunction`.
|
|
905
|
+
|
|
906
|
+
`tabIds(value, prefix = 'tab')` returns the stable ids
|
|
907
|
+
`{ tab: '<prefix>-<value>', panel: '<prefix>-panel-<value>' }` for `id` and
|
|
908
|
+
`aria-labelledby`. The value must be id-safe (no spaces); give each tab set on a
|
|
909
|
+
page its own `prefix` to keep them apart.
|
|
910
|
+
|
|
911
|
+
`values` is a non-empty tuple read as literal types (`const` generic): `value`
|
|
912
|
+
and `hrefFor`'s argument are the union of its entries, and a misspelled tab or a
|
|
913
|
+
`defaultValue` outside `values` is a compile error.
|
|
914
|
+
|
|
915
|
+
**A loader must not read the tab param**: a skipped revalidation leaves its
|
|
916
|
+
data as it was, so anything derived from `?tab=` would go stale. Read the tab
|
|
917
|
+
in the component (`useTabParam`) instead.
|
|
918
|
+
|
|
919
|
+
```tsx
|
|
920
|
+
import { type LoaderFunctionArgs, useLoaderData } from 'react-router';
|
|
921
|
+
import { skipTabChange, TabsNav, TabsNavLink, tabIds, useTabParam } from '@nxgt/material/components';
|
|
922
|
+
|
|
923
|
+
const TABS = ['overview', 'history', 'files'] as const;
|
|
924
|
+
|
|
925
|
+
export async function loader({ params }: LoaderFunctionArgs) {
|
|
926
|
+
return getRecord(params.id); // does not read `?tab=`
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// route module: a tab switch does not reload the record
|
|
930
|
+
export const shouldRevalidate = skipTabChange();
|
|
931
|
+
|
|
932
|
+
export default function Record() {
|
|
933
|
+
const record = useLoaderData<typeof loader>();
|
|
934
|
+
// `explicitDefault`: another param implies a tab, so spell the default out
|
|
935
|
+
const { value, hrefFor } = useTabParam({ values: TABS, explicitDefault: !!record.from });
|
|
936
|
+
return (
|
|
937
|
+
<>
|
|
938
|
+
<TabsNav aria-label="Record sections" value={value}>
|
|
939
|
+
{TABS.map((tab) => (
|
|
940
|
+
<TabsNavLink key={tab} id={tabIds(tab).tab} to={hrefFor(tab)} value={tab}>
|
|
941
|
+
{tab}
|
|
942
|
+
</TabsNavLink>
|
|
943
|
+
))}
|
|
944
|
+
</TabsNav>
|
|
945
|
+
<section id={tabIds(value).panel} aria-labelledby={tabIds(value).tab}>
|
|
946
|
+
{/* mount only the shown panel */}
|
|
947
|
+
</section>
|
|
948
|
+
</>
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
| Option | Default | Effect |
|
|
954
|
+
| --- | --- | --- |
|
|
955
|
+
| `param` | `'tab'` | the search param |
|
|
956
|
+
| `values` | — (required) | the tabs a URL may name, in order |
|
|
957
|
+
| `defaultValue` | the first value | the tab shown without a param; writes no param |
|
|
958
|
+
| `explicitDefault` | `false` | writes the default tab's param out too (another param implies a tab) |
|
|
959
|
+
| `keepHash` | `false` | keeps the location's hash in `hrefFor` (it scrolls to it, see above) |
|
|
960
|
+
|
|
961
|
+
**Storybook:** `Primitives/TabsNav` (story `TabParam` for `useTabParam`) · **Vue:** none yet (#2055, #2065)
|
|
962
|
+
|
|
963
|
+
---
|
|
964
|
+
|
|
965
|
+
## SegmentedNav
|
|
966
|
+
|
|
967
|
+

|
|
968
|
+
|
|
969
|
+
The segmented `ToggleGroup` look as a `nav` of links: a period or a view kept in the address, with `aria-current="page"` and the selected pill on the current one.
|
|
970
|
+
|
|
971
|
+
```tsx
|
|
972
|
+
import { SegmentedNav, SegmentedNavItem, useTabParam } from '@nxgt/material/components';
|
|
973
|
+
|
|
974
|
+
const PERIODS = ['day', 'week'] as const;
|
|
975
|
+
|
|
976
|
+
function Period() {
|
|
977
|
+
const { value, hrefFor } = useTabParam({ param: 'p', values: PERIODS });
|
|
978
|
+
return (
|
|
979
|
+
<SegmentedNav aria-label="Period" value={value}>
|
|
980
|
+
<SegmentedNavItem to={hrefFor('day')} value="day">
|
|
981
|
+
Day
|
|
982
|
+
</SegmentedNavItem>
|
|
983
|
+
<SegmentedNavItem to={hrefFor('week')} value="week">
|
|
984
|
+
Week
|
|
985
|
+
</SegmentedNavItem>
|
|
986
|
+
</SegmentedNav>
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
Use `ToggleGroup` for a choice that stays on the page and `SegmentedNav` when
|
|
992
|
+
each segment is a place to go: every item is a real link, so it opens in a
|
|
993
|
+
new window and its address can be copied. A `ToggleGroup` of links would
|
|
994
|
+
announce each as a radio. `aria-label` names the navigation and is required.
|
|
995
|
+
|
|
996
|
+
- **Current item**: `SegmentedNav`'s `value` is matched against each item's
|
|
997
|
+
`value`; an item's `current` boolean overrides it (`current={false}` to
|
|
998
|
+
switch one off). The current item carries `aria-current="page"` and
|
|
999
|
+
`data-state="active"`, and draws the selected pill (its label in
|
|
1000
|
+
`primary-text`); it does not glide, since the nav may scroll.
|
|
1001
|
+
- **Keyboard**: the items are plain links, so Tab moves from one to the next;
|
|
1002
|
+
the arrow keys do nothing (there is no radio group).
|
|
1003
|
+
- **On a phone**: a nav wider than its container scrolls inside itself (the
|
|
1004
|
+
page does not), with a fade on the side with hidden items, and the current
|
|
1005
|
+
item is scrolled into view. On a coarse pointer each item is 44px tall at
|
|
1006
|
+
least.
|
|
1007
|
+
- **The link**: `SegmentedNavItem` takes `LinkButton`'s props (`to`, `replace`,
|
|
1008
|
+
`state`, `target`, `reloadDocument`, `download`, `linkComponent`). It renders
|
|
1009
|
+
React Router's `Link`, or the `LinkComponentProvider`'s component (given
|
|
1010
|
+
`href`). `preventScrollReset` is on by default, so changing segment keeps
|
|
1011
|
+
the scroll position; pass `preventScrollReset={false}` to reset.
|
|
1012
|
+
- A segment bound to the URL's search params: [`useTabParam`](#route-tabs-in-the-url-usetabparam) (`useTabParam({ param: 'p', values })`), as above.
|
|
1013
|
+
|
|
1014
|
+
**It needs a Router.** Without a `LinkComponentProvider`, `SegmentedNavItem`
|
|
1015
|
+
renders React Router's `Link`, as `LinkButton` does, so it throws outside a
|
|
1016
|
+
Router (`useHref() may be used only in the context of a <Router> component`):
|
|
1017
|
+
render it inside your router, or set a [`LinkComponentProvider`](#linkcomponentprovider).
|
|
1018
|
+
See [the troubleshooting entry](../troubleshooting.md#usehref-may-be-used-only-in-the-context-of-a-router-component-on-a-tabsnav-segmentednav-or-a-linkbutton).
|
|
1019
|
+
|
|
1020
|
+
| Prop | On | Type | Default | Effect |
|
|
1021
|
+
| --- | --- | --- | --- | --- |
|
|
1022
|
+
| `aria-label` | `SegmentedNav` | `string` | — (required) | names the navigation |
|
|
1023
|
+
| `value` | `SegmentedNav` | `string` | — | the current item's `value` |
|
|
1024
|
+
| `size` | `SegmentedNav` | `'sm' \| 'md' \| 'lg'` | `'md'` | 32, 36 or 44 px tall, as `ToggleGroup` |
|
|
1025
|
+
| `to` | `SegmentedNavItem` | `To` | — (required) | the route, as for `LinkButton` |
|
|
1026
|
+
| `value` | `SegmentedNavItem` | `string` | — | matched against the nav's `value` |
|
|
1027
|
+
| `current` | `SegmentedNavItem` | `boolean` | — | marks the item current or not, over `value` |
|
|
1028
|
+
| `preventScrollReset` | `SegmentedNavItem` | `boolean` | `true` | keeps the scroll position when the segment changes |
|
|
1029
|
+
|
|
1030
|
+
**Storybook:** `Primitives/SegmentedNav` · **Vue:** none yet (#2059)
|
|
1031
|
+
|
|
1032
|
+
---
|
|
1033
|
+
|
|
825
1034
|
## Toggle
|
|
826
1035
|
|
|
827
1036
|

|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/docs/roadmap.md
CHANGED
|
@@ -6,6 +6,8 @@ version they shipped in, and planned entries may name the major they target.
|
|
|
6
6
|
|
|
7
7
|
## Now
|
|
8
8
|
|
|
9
|
+
- **Link navigation** — `TabsNav` and `SegmentedNav` render route tabs and segmented navigation as a `nav` of links with `aria-current="page"`, looking like Tabs and the segmented ToggleGroup, plus `useTabParam` to keep the current tab in the URL. Tracked in [#2029](https://github.com/softistx/nxgt-material/issues/2029).
|
|
10
|
+
|
|
9
11
|
- **Barcodes and label sheets** — `@nxgt/material/barcode` draws Code 128
|
|
10
12
|
and Data Matrix as accessible SVG (themed, dark-aware, black on white in
|
|
11
13
|
print) and lays labels out on a sheet to print: label size, columns, rows,
|
|
@@ -241,7 +243,6 @@ version they shipped in, and planned entries may name the major they target.
|
|
|
241
243
|
slot; the bar's `views` slot is planned with it. They are meant to replace
|
|
242
244
|
the legacy `FilterPresets`. Tracked in [#1876](https://github.com/softistx/nxgt-material/issues/1876).
|
|
243
245
|
|
|
244
|
-
- **Link navigation** — `TabsNav` and `SegmentedNav` render route tabs and segmented navigation as a `nav` of links with `aria-current="page"`, looking like Tabs and the segmented ToggleGroup, plus `useTabParam` to keep the current tab in the URL.
|
|
245
246
|
- **RecordActionsMenu** — one menu for a record's lifecycle: archive with an Undo toast (or confirm first with `archivePolicy="confirm"`), restore, and delete that always confirms and shows the backend's refusal, with `ConfirmDialog` gaining `details` and `refusal` slots.
|
|
246
247
|
- **Cursor trail cap** — `ServerTable` and `PageFooter` keep cursor URLs short: the trail is encoded once and capped by `maxTrail` (20 by default) with an exact page count, and `totalCount` becomes optional for APIs that do not count.
|
|
247
248
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -79,6 +79,8 @@ words in your error.
|
|
|
79
79
|
|
|
80
80
|
**Buttons & actions**
|
|
81
81
|
|
|
82
|
+
- [`useHref() may be used only in the context of a <Router> component` on a `TabsNav`, `SegmentedNav` or a `LinkButton`](#usehref-may-be-used-only-in-the-context-of-a-router-component-on-a-tabsnav-segmentednav-or-a-linkbutton)
|
|
83
|
+
- [A tab switch reloads the route's data, or the default tab shows `?tab=` in the URL](#a-tab-switch-reloads-the-routes-data-or-the-default-tab-shows-tab-in-the-url)
|
|
82
84
|
- [An `ActionCard` inside a button or a link is a button inside a button](#an-actioncard-inside-a-button-or-a-link-is-a-button-inside-a-button)
|
|
83
85
|
- [A `Card` class or a `[data-slot=card]` selector no longer styles an `ActionCard`](#a-card-class-or-a-data-slotcard-selector-no-longer-styles-an-actioncard)
|
|
84
86
|
- [A `[data-slot=button]` selector no longer finds a SplitButton's half, or a class should style the whole split button](#a-data-slotbutton-selector-no-longer-finds-a-splitbuttons-half-or-a-class-should-style-the-whole-split-button)
|
|
@@ -1908,6 +1910,30 @@ return <IconButton size="lg" {...rest} />;
|
|
|
1908
1910
|
<Button size="xl">Save</Button>
|
|
1909
1911
|
```
|
|
1910
1912
|
|
|
1913
|
+
### `useHref() may be used only in the context of a <Router> component` on a `TabsNav`, `SegmentedNav` or a `LinkButton`
|
|
1914
|
+
|
|
1915
|
+
**When:** a `TabsNavLink`, `SegmentedNavItem`, `LinkButton`, `IconLinkButton` or `ResponsiveLinkButton` renders in a test, a story or a tree with no router, and no `LinkComponentProvider`.
|
|
1916
|
+
**Why:** without a provider (or a `linkComponent` prop), they render React Router's `Link`, which reads the router's context.
|
|
1917
|
+
**Fix:** wrap the tree in your router (`MemoryRouter` in a test or a story), or set a `LinkComponentProvider` once.
|
|
1918
|
+
|
|
1919
|
+
```tsx
|
|
1920
|
+
<MemoryRouter>
|
|
1921
|
+
<TabsNav aria-label="Account" value="profile">
|
|
1922
|
+
<TabsNavLink to="/profile" value="profile">Profile</TabsNavLink>
|
|
1923
|
+
</TabsNav>
|
|
1924
|
+
</MemoryRouter>
|
|
1925
|
+
```
|
|
1926
|
+
|
|
1927
|
+
### A tab switch reloads the route's data, or the default tab shows `?tab=` in the URL
|
|
1928
|
+
|
|
1929
|
+
**When:** a route using `useTabParam` re-runs its loader on every tab click, or the first tab's link carries `?tab=overview`.
|
|
1930
|
+
**Why:** a search-param change revalidates the route's loaders unless `shouldRevalidate` says otherwise; and the default tab writes no param unless `explicitDefault` is on.
|
|
1931
|
+
**Fix:** export `skipTabChange()` as the route's `shouldRevalidate`, and leave `explicitDefault` off unless another param implies a tab. Parent routes' loaders still revalidate unless they export `skipTabChange` as well.
|
|
1932
|
+
|
|
1933
|
+
```tsx
|
|
1934
|
+
export const shouldRevalidate = skipTabChange();
|
|
1935
|
+
```
|
|
1936
|
+
|
|
1911
1937
|
## Feedback
|
|
1912
1938
|
|
|
1913
1939
|
### An `EmptyState` with no `kind` shows "No Data Found" where it showed "Something Went Wrong"
|
|
@@ -2085,9 +2111,22 @@ onClick }`, or a function that calls the `dismiss` it is given): the action
|
|
|
2085
2111
|
gives the focus back to where it was before the toast first, then runs. The
|
|
2086
2112
|
dialog keeps its control, and gives the focus back to that earlier control
|
|
2087
2113
|
when it closes. A workaround that blurs the button and opens the dialog on
|
|
2088
|
-
the next frame can go.
|
|
2089
|
-
|
|
2090
|
-
|
|
2114
|
+
the next frame can go. For a row action's toast, see [The focus lands on the
|
|
2115
|
+
page after a row action](#the-focus-lands-on-the-page-after-a-row-action). A
|
|
2116
|
+
button you render in a function action and that opens a dialog without calling
|
|
2117
|
+
`dismiss` is not covered: call `dismiss()` before opening the dialog.
|
|
2118
|
+
|
|
2119
|
+
### The focus lands on the page after a row action
|
|
2120
|
+
|
|
2121
|
+
**When:** an item of a `RowMenu` runs an async action, then shows a toast or
|
|
2122
|
+
opens a dialog. When they close, `document.activeElement` is `document.body`.
|
|
2123
|
+
**Why:** the menu hands the focus back to the row's button when it closes (and
|
|
2124
|
+
when a dialog opened from an item closes), but a toast, a later dialog or a
|
|
2125
|
+
remounted row takes it afterwards.
|
|
2126
|
+
**Fix:** call `focusRowMenu(row.id)` from `useRowMenuFocus()` once the action
|
|
2127
|
+
settles, with `focusRef` on the table. It matches the row by id, not by label.
|
|
2128
|
+
See "Handing the focus back to a row" in
|
|
2129
|
+
[RowMenu](components/data-table-filter-and-lists.md#handing-the-focus-back-to-a-row).
|
|
2091
2130
|
|
|
2092
2131
|
### A `ConfirmationDialog` reopened with new content blocks clicks
|
|
2093
2132
|
|
|
@@ -122,6 +122,8 @@
|
|
|
122
122
|
@import "./ui/primitives/accordion/accordion.css";
|
|
123
123
|
@import "./ui/primitives/accordion/accordion-surfaces.css";
|
|
124
124
|
@import "./ui/primitives/collapsible/collapsible.css";
|
|
125
|
+
/* TabsNav in forced colours (the current link's mark). */
|
|
126
|
+
@import "./ui/primitives/tabs/tabs-nav-forced.css";
|
|
125
127
|
/* ToggleGroup: shared rules, then one file per variant. */
|
|
126
128
|
@import "./ui/primitives/toggle-group/toggle-group.css";
|
|
127
129
|
@import "./ui/primitives/toggle-group/toggle-group-connected.css";
|
|
@@ -149,9 +149,10 @@
|
|
|
149
149
|
backdrop-filter: blur(var(--nxgt-backdrop-blur));
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
/* Tabs: the strip is a band, its top corners
|
|
153
|
-
* has its own track.) */
|
|
154
|
-
[data-slot="activity-layout"][data-backdrop]
|
|
152
|
+
/* Tabs and route tabs (`TabsNav`): the strip is a band, its top corners
|
|
153
|
+
* rounded. (The segmented look has its own track.) */
|
|
154
|
+
[data-slot="activity-layout"][data-backdrop]
|
|
155
|
+
:is([data-slot="tabs-list"], [data-slot="tabs-nav"]) {
|
|
155
156
|
border-start-start-radius: var(--radius);
|
|
156
157
|
border-start-end-radius: var(--radius);
|
|
157
158
|
background: var(--nxgt-backdrop-fill-nav);
|