@nxgt/material 3.19.0 → 3.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.
@@ -633,7 +633,7 @@ the grid it is in; pass `className="col-start-full"` for a full-width row.
633
633
 
634
634
  ![EntityHeader](../previews/data-display-entityheader.webp)
635
635
 
636
- Page header with icon, title, status, metadata and actions.
636
+ Page header with icon (or a `leading` control), title, status, metadata and actions.
637
637
 
638
638
  ```tsx
639
639
  import { Edit } from 'lucide-react';
@@ -651,13 +651,27 @@ import { Badge, EntityHeader } from '@nxgt/material/components';
651
651
  | --- | --- | --- | --- |
652
652
  | `title` | `ReactNode` | — | the heading (an `h1` unless `headingLevel` says otherwise); optional: without it no heading is rendered (see below) |
653
653
  | `headingLevel` | `1 \| 2 \| 3` | `1` | the title's heading level: pass `2` or `3` when the page already has its `h1` (a list title beside a detail header); the look does not change |
654
- | `icon`, `status` | `ReactNode` | — | before and after the title; `icon` is decoration, shown in an `aria-hidden` badge, so put nothing meaningful or focusable there |
654
+ | `icon`, `status` | `ReactNode` | — | before and after the title; `icon` is decoration, shown in an `aria-hidden` badge, so put nothing meaningful or focusable there; for a control, use `leading` |
655
+ | `leading` | `ReactNode` | — | an interactive control at the band's start, such as a [Switcher](selection-and-choice.md#switcher) of either variant: drawn as it is, at its natural size (at most 18rem wide beside the title, from a 384px band; narrower, it takes the band's width), in the accessibility tree, with no badge and no `aria-hidden`; on a phone it takes the band's width above the title. **It replaces `icon` when both are given** (a development warning says so). Root `data-slot="entity-header-leading"` |
655
656
  | `metadata` | `{ label: string; value: ReactNode }[]` | `[]` | facts under the title, a description list: a muted label beside its value, no colon; a link in a value is drawn in the primary text colour (`text-primary-text`), not the prose link's info blue, and `nxgt-plain-link` still opts out |
656
657
  | `stats` | `EntityStat[]` | `[]` | figures (counts, a mean) as compact `size="sm"` [StatCard](#statcard) tiles in a `StatCardGroup` (2×2 on a phone, a wrapping row beside) under the metadata; a `tone` paints a tile, `0` is a figure, an empty value drops the tile (see Stats below) |
657
658
  | `actions` | `ResponsiveButtonProps[] \| ReactNode` | `[]` | an array becomes responsive buttons (icon-only on phones) |
658
659
 
659
660
  The root carries `data-slot="entity-header"`; the metadata is a `dl` with `data-slot="entity-metadata"` (a muted `dt` label beside its `dd` value, no colon), and the actions sit in `data-slot="entity-header-actions"` (absent when there are none). Inside an EntityHeader that replaces the old `summary-data` slot and its `Label:` text, so update a selector or a test that read "Label: value".
660
661
 
662
+ **A control at the start.** `leading` takes a control, never `icon`: the icon badge is `aria-hidden`, which squashes a `Switcher` and makes axe report `aria-hidden-focus` (see [Troubleshooting](../troubleshooting.md#a-control-in-an-entityheaders-icon-is-hidden-from-assistive-tech-squashed-and-reports-aria-hidden-focus)).
663
+
664
+ ```tsx
665
+ import { Badge, EntityHeader, Switcher } from '@nxgt/material/components';
666
+
667
+ <EntityHeader
668
+ leading={<Switcher variant="expressive" label="Group" value={group} onValueChange={setGroup} options={groups} />}
669
+ title="Attendance"
670
+ status={<Badge variant="success">Active</Badge>}
671
+ metadata={[{ label: 'Year', value: '2026-2027' }]}
672
+ />
673
+ ```
674
+
661
675
  **Stats.** Figures (counts, a mean) go in `stats`, shown as compact [StatCard](#statcard) tiles (`size="sm"`) in a `StatCardGroup` (2×2 on a phone, a row that wraps beside) under the metadata; facts (dates, names) stay in `metadata`. A `tone` paints a tile (tonal), none leaves it outlined; `0` is a figure, an empty value drops the tile. The row is `data-slot="entity-stats"`, and the type `EntityStat` is exported.
662
676
 
663
677
  ```tsx
@@ -1197,7 +1197,17 @@ const RouterLink: AppShellLinkComponent = ({ href, ...props }) => <Link to={href
1197
1197
  />
1198
1198
  ```
1199
1199
 
1200
- A title that does not fit the trigger is cut with an ellipsis: the span carries the full title in `aria-label` (what `expectNoTextOverflow` of `@nxgt/playwright` reads), hovering or focusing the trigger shows it with the subtitle in a tooltip, and the trigger's accessible name is the label, the full title and the subtitle.
1200
+ A title that does not fit the trigger is cut with an ellipsis: the span carries the full title in `aria-label` (what `expectNoTextOverflow` of `@nxgt/playwright` reads), hovering or focusing the trigger shows it with the subtitle in a tooltip, and the trigger's accessible name is the label, the full title and the subtitle. An option's subtitle that does not fit, in the trigger (default variant; the expressive pill keeps it for screen readers only), the menu or the panel, is treated the same way while it is cut: the full text in `aria-label` and, in the options, in `title`, and in the trigger's tooltip. The option's accessible name still reads the title and the subtitle once each.
1201
+
1202
+ **Choosing a link closes the panel.** In link mode the popover or the phone sheet closes when an option (or a footer link) is activated, as in select mode, and focus is **not** handed back to the trigger: the navigation owns it, so a route's own focus on the new page's title runs. `open` and `onOpenChange` control the panel from the app (close it from a route effect, or open it from a shortcut):
1203
+
1204
+ ```tsx
1205
+ const [open, setOpen] = useState(false);
1206
+
1207
+ <Switcher label="Group" value={id} options={groups} getHref={(g) => `/groups/${g.id}`} linkComponent={RouterLink} open={open} onOpenChange={setOpen} />;
1208
+ ```
1209
+
1210
+ To put a Switcher at the start of a page header, pass it as [EntityHeader](data-display.md#entityheader)'s `leading` (not `icon`).
1201
1211
 
1202
1212
  | Prop | Type | Default | Effect |
1203
1213
  | --- | --- | --- | --- |
@@ -1213,6 +1223,8 @@ A title that does not fit the trigger is cut with an ellipsis: the span carries
1213
1223
  | `footer` | `{ label?, onSelect?, href? }` | — | a "Manage…" button (a link with `href`) at the end of the panel; `label` defaults to `material.switcher.manage` |
1214
1224
  | `emptyLabel` | `ReactNode` | `material.switcher.empty` | shown when the search matches nothing |
1215
1225
  | `sheet` | `boolean` | on a phone | `true` always opens a sheet, `false` never |
1226
+ | `open` | `boolean` | uncontrolled | controls the panel (popover, menu or sheet); pair with `onOpenChange` |
1227
+ | `onOpenChange` | `(open: boolean) => void` | — | called when the panel opens or closes, by the user or after a pick |
1216
1228
  | `name`, button props | `ComponentProps<'button'>` | — | spread on the trigger; `name` is its `data-pw` |
1217
1229
 
1218
1230
  `SwitcherItem`: `{ id, title, subtitle?, icon?, group?, initial?, tone?, badge?, href?, disabled? }`.
@@ -36,6 +36,8 @@ words in your error.
36
36
  - [`IconRefError` from `IconSpritesProvider`](#iconreferror-from-iconspritesprovider)
37
37
  - [An accessibility audit reports low contrast on a badge, a button, a toast or a menu entry](#an-accessibility-audit-reports-low-contrast-on-a-badge-a-button-a-toast-or-a-menu-entry)
38
38
  - [`ARIA hidden element must not be focusable or contain focusable elements`](#aria-hidden-element-must-not-be-focusable-or-contain-focusable-elements)
39
+ - [A control in an `EntityHeader`'s `icon` is hidden from assistive tech, squashed, and reports `aria-hidden-focus`](#a-control-in-an-entityheaders-icon-is-hidden-from-assistive-tech-squashed-and-reports-aria-hidden-focus)
40
+ - [A `Switcher` in link mode leaves focus on the trigger after a navigation, and the new page's title never gets it](#a-switcher-in-link-mode-leaves-focus-on-the-trigger-after-a-navigation-and-the-new-pages-title-never-gets-it)
39
41
  - [An accessibility audit reports low contrast on a tonal or outlined button, a selected list tile or a hovered badge link](#an-accessibility-audit-reports-low-contrast-on-a-tonal-or-outlined-button-a-selected-list-tile-or-a-hovered-badge-link)
40
42
  - [An accessibility audit reports low contrast on error text in the dark theme](#an-accessibility-audit-reports-low-contrast-on-error-text-in-the-dark-theme)
41
43
  - [An accessibility audit reports low contrast on primary text in the dark theme](#an-accessibility-audit-reports-low-contrast-on-primary-text-in-the-dark-theme)
@@ -956,6 +958,35 @@ Elements mounted after the layer opens are not made inert: mount them
956
958
  first, or pass them to `inertOthers` in `keep` when they must stay
957
959
  reachable.
958
960
 
961
+ ### A control in an `EntityHeader`'s `icon` is hidden from assistive tech, squashed, and reports `aria-hidden-focus`
962
+
963
+ **When:** an `EntityHeader` gets a `Switcher`, a button or a link as its `icon`.
964
+ The control is drawn small and clipped inside a filled square, a screen reader
965
+ skips it, and an accessibility audit (axe) reports `aria-hidden-focus`.
966
+ **Why:** `icon` is decoration: it is wrapped in a filled, icon-sized badge with
967
+ `aria-hidden="true"`, so anything focusable inside it is an `aria-hidden`
968
+ subtree the keyboard still reaches.
969
+ **Fix:** put the control in `leading`: it is drawn as it is, at its natural
970
+ size, beside the title, with no badge and no `aria-hidden`. When both are
971
+ given, `leading` replaces `icon` (and warns in development).
972
+
973
+ ```tsx
974
+ <EntityHeader
975
+ leading={<Switcher label="Group" value={group} onValueChange={setGroup} options={groups} />}
976
+ title="Attendance"
977
+ />
978
+ ```
979
+
980
+ ### A `Switcher` in link mode leaves focus on the trigger after a navigation, and the new page's title never gets it
981
+
982
+ **When:** a `Switcher` with `getHref` and your router's `linkComponent`: you pick an option, the URL changes, but focus sits on the Switcher's trigger (or on the clicked link while the panel is open) instead of moving to the new page's title.
983
+ **Why:** a closing overlay hands focus back to its trigger, which ran after your route's own focus handling. Older versions also gave you no way to close the panel yourself.
984
+ **Fix:** update `@nxgt/material`: activating a link option closes the popover or sheet and no longer returns focus to the trigger, so the page's focus runs. To close it from the app, control it with `open` and `onOpenChange`.
985
+
986
+ ```tsx
987
+ <Switcher label="Group" value={id} options={groups} getHref={(g) => `/groups/${g.id}`} linkComponent={RouterLink} open={open} onOpenChange={setOpen} />
988
+ ```
989
+
959
990
  ### An accessibility audit reports low contrast on a tonal or outlined button, a selected list tile or a hovered badge link
960
991
 
961
992
  **When:** axe (or Lighthouse) reports `color-contrast` on a `tonal` or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/material",
3
- "version": "3.19.0",
3
+ "version": "3.20.0",
4
4
  "license": "MIT",
5
5
  "bugs": {
6
6
  "email": "stevegires@gmail.com"