@nxgt/material 3.20.0 → 3.21.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.
@@ -399,7 +399,7 @@ import { Avatar, Chip } from '@nxgt/material/components';
399
399
  | `aria-pressed` | `boolean` | — | a toggle chip: selected while pressed, with a check when nothing leads |
400
400
  | `size` | `'default' \| 'sm' \| 'lg'` | `'default'` | 28px, 24px, or `lg`, a 44px box at every pointer (the touch target); every target stays at least 24px |
401
401
  | `color` | `'default' \| 'primary' \| 'secondary' \| 'info' \| 'success' \| 'warning' \| 'error'` | neutral | tone: the edge (outlined), the fill and label (tonal, `filled`: a status look) or the dot and tint (tag). A `tag` without `color` (or with `default`) draws no dot |
402
- | `onDismiss` | `() => void` | — | adds the ✕ button, named "Remove {label}" (`material.chip.dismiss-label`), `{label}` being the text `label` shows, a node's included; "Remove" when it shows none; the ✕ is drawn 18px (16px small) with a 24px target |
402
+ | `onDismiss` | `() => void` | — | adds the ✕ button, named "Remove {label}" (`material.chip.dismiss-label`), `{label}` being the text `label` shows, a node's included; "Remove" when it shows none; the ✕ is drawn 18px (16px small) in its own 24x24 box (44x44 on a coarse pointer) |
403
403
  | `dismissLabel` | `string \| ((label: string) => string)` | — | names the ✕ instead of "Remove {label}": a string as is, or a function of the text the chip shows |
404
404
  | `onClick` | `MouseEventHandler` | — | makes the chip (or its body, when dismissible) a button; without it the chip is a `<span>` |
405
405
  | `leading`, `avatar`, `trailing` | `ReactNode` | — | slots; `avatar` is shrunk to fit, `trailing` (a count) is never truncated |
@@ -186,10 +186,14 @@ export function Delivery() {
186
186
  The calendar opens on the month of `value`; with no value, `defaultMonth` sets
187
187
  it.
188
188
 
189
+ **Migration:** a typed date outside `minDate`/`maxDate`/`disablePast`/`disableFuture` is no longer moved to the nearest allowed day on blur. It is kept, and the form refuses it. If your schema has no min/max rule, add one.
190
+
189
191
  Every other `Calendar` prop (`disablePast`, `minDate`, …) is passed to the
190
192
  calendar. A typed date outside `minDate`, `maxDate`, `disablePast` or
191
- `disableFuture` is no value while it is typed, and moves to the nearest day
192
- inside them once focus leaves the field. A typed day that `disabledDays`
193
+ `disableFuture` is no value while it is typed, and is kept as entered once
194
+ focus leaves the field (never moved to another day: the form's rule refuses
195
+ it, the calendar keeps the day unpickable). A value you set from outside
196
+ (`setValue`) is shown and kept the same way. A typed day that `disabledDays`
193
197
  rules out is refused: it stays on screen with no value (it is not moved to
194
198
  another day). An Invalid Date is no value. `id` goes on the first
195
199
  segment (the label's target); `aria-describedby` and `data-*` attributes on
@@ -362,8 +366,8 @@ is `{ from, to: undefined }`, and no start is `undefined`. An end before the
362
366
  start is no `to` (it stays on screen, and becomes the `to` once the start is
363
367
  moved before it), and a start typed past the end drops the end, as a click in
364
368
  the calendar does. A date outside `minDate`, `maxDate`, `disablePast` or
365
- `disableFuture` is no value while it is typed and moves to the nearest day
366
- inside them once focus leaves the field; a day `disabledDays` rules out is
369
+ `disableFuture` is no value while it is typed and is kept as entered once
370
+ focus leaves the field (the form refuses it); a day `disabledDays` rules out is
367
371
  refused, kept on screen with no value. Retyping the start keeps the end
368
372
  while the start is incomplete. Typing a day keeps the time of day
369
373
  of the end it replaces (`withTime`). The ends sit on one line while they fit
@@ -182,6 +182,8 @@ words in your error.
182
182
  - [`Module '"@nxgt/material"' has no exported member 'formatAmount'`](#module-nxgtmaterial-has-no-exported-member-formatamount)
183
183
  - [An amount in yen shows no decimals, or one in dinars shows three](#an-amount-in-yen-shows-no-decimals-or-one-in-dinars-shows-three)
184
184
  - [A date shows the day before](#a-date-shows-the-day-before)
185
+ - [A `DateField` with `disableFuture` (or `disablePast`, `minDate`, `maxDate`) moved my value to the previous day](#a-datefield-with-disablefuture-or-disablepast-mindate-maxdate-moved-my-value-to-the-previous-day)
186
+ - [axe `target-size` flags a chip's ✕](#axe-target-size-flags-a-chips-)
185
187
  - [`Invalid hook call` in a `PrintDocument` `footer` function](#invalid-hook-call-in-a-printdocument-footer-function)
186
188
  - [Body text runs under the footer or the page number](#body-text-runs-under-the-footer-or-the-page-number)
187
189
  - [The `LOGO` box in `PrintHeader` is gone after upgrading](#the-logo-box-in-printheader-is-gone-after-upgrading)
@@ -1093,7 +1095,7 @@ by default; `hitArea: false` brings back the box check), so these controls pass
1093
1095
  on a coarse pointer when their `::after` can be hit: not under a sticky bar or
1094
1096
  a toast, not clipped by an ancestor's `overflow`, and with its 44px square on
1095
1097
  screen. Rows, the app shell's triggers and the calendar's days are real 44px
1096
- boxes and pass with either version.
1098
+ boxes and pass with either version. A chip's ✕ is a real box too (24x24, 44x44 on a coarse pointer).
1097
1099
  See [Theming, touch targets](./guides/theming.md#touch-targets).
1098
1100
 
1099
1101
  ### A Switch looks bigger, or a test reads `translate-x-*`
@@ -3261,6 +3263,37 @@ formatAmount(1234.5, 'JPY', { fractionDigits: 2 }); // '¥1,234.50'
3261
3263
  <CurrencyField label="Price" currency="JPY" fractionDigits={2} value={price} onChange={setPrice} />
3262
3264
  ```
3263
3265
 
3266
+ ### A `DateField` with `disableFuture` (or `disablePast`, `minDate`, `maxDate`) moved my value to the previous day
3267
+
3268
+ **When:** before the fix, a value past a bound (a `setValue` of "now + 1 h"
3269
+ shortly before midnight, or a date typed past `maxDate`) came back from the
3270
+ field about 24 hours earlier, and a form rule such as "not in the future"
3271
+ passed on a date nobody chose.
3272
+ **Why:** when focus left the field, it moved the value onto the bound's day and
3273
+ kept its time of day, so tomorrow 00:30 became today 00:30.
3274
+ **Fix:** upgrade: the field keeps the value it is given or that was typed, the
3275
+ calendar keeps the out-of-bounds days unpickable, and your form refuses the
3276
+ value. Add the rule to the schema (and a test that does not depend on the
3277
+ clock):
3278
+
3279
+ ```tsx
3280
+ const schema = z.object({
3281
+ at: z.date().refine((d) => d <= new Date(), 'Must not be in the future'),
3282
+ });
3283
+ <DateFormField control={form.control} name="at" label="Recorded at" showTimePicker disableFuture />;
3284
+ ```
3285
+
3286
+ ### axe `target-size` flags a chip's ✕
3287
+
3288
+ **When:** before the fix, a scan of a desktop (fine-pointer) page flagged
3289
+ `[data-slot="chip-dismiss"]` (a `Chip` with `onDismiss`, the `Filter` bar's pills
3290
+ and active chips, the `ServerTable` chips) for a 16-18px box.
3291
+ **Why:** the 24px target was a `::after` pseudo-element, which axe does not
3292
+ measure.
3293
+ **Fix:** upgrade: the ✕ button's own box is 24x24 (44x44 on a coarse pointer),
3294
+ drawn and laid out as before. Remove any exclusion of `chip-dismiss` from your
3295
+ axe scan.
3296
+
3264
3297
  ### A date shows the day before
3265
3298
 
3266
3299
  **When:** before 2.2.0, `formatDate('2026-03-01')` printed `Feb 28, 2026` on a
@@ -1,5 +1,5 @@
1
1
  /* Chip: focus, the dismiss button (drawn
2
- * small, a 24x24 target through ::after, its own ring), disabled, the dashed
2
+ * small in a 24x24 box, 44x44 on a coarse pointer, its own ring), disabled, the dashed
3
3
  * "add" chip and the tones. */
4
4
  @layer components {
5
5
  /* Focus: the chip's ring (2px primary-text, 2px away); the ✕ its own. */
@@ -8,54 +8,41 @@
8
8
  outline-color: var(--primary-text);
9
9
  outline-width: 2px;
10
10
  }
11
+ /* The ✕ is drawn small (--chip-x), but its own box is the target (#1584):
12
+ * axe's `target-size` reads the bounding box, never a pseudo-element. The
13
+ * box is 24x24 (44x44 on a coarse pointer, #1067, #1102); the padding with
14
+ * `background-clip: content-box` keeps the hover disc and the focus ring at
15
+ * the drawn size, and the negative margin takes the extra back, so the chip
16
+ * and the glyph are unchanged. */
11
17
  .nxgt-chip > [data-slot="chip-dismiss"] {
18
+ --chip-hit: 24px;
19
+ --chip-pad: calc((var(--chip-hit) - var(--chip-x)) / 2);
12
20
  position: relative;
13
21
  display: inline-grid;
14
22
  place-items: center;
15
23
  flex: none;
16
- width: var(--chip-x);
17
- height: var(--chip-x);
18
- padding: 0;
24
+ box-sizing: border-box;
25
+ width: var(--chip-hit);
26
+ height: var(--chip-hit);
27
+ margin: calc(-1 * var(--chip-pad));
28
+ padding: var(--chip-pad);
19
29
  border: 0;
20
30
  border-radius: 9999px;
21
31
  background: transparent;
32
+ background-clip: content-box;
22
33
  color: color-mix(in oklab, var(--foreground) 62%, transparent);
23
34
  cursor: pointer;
24
35
  /* Width 0 at rest: forced colours would paint a transparent outline. */
25
36
  outline: 0 solid transparent;
26
- outline-offset: 1px;
37
+ outline-offset: calc(1px - var(--chip-pad));
27
38
  transition:
28
39
  background-color 120ms ease-out,
29
40
  color 120ms ease-out,
30
41
  outline-color 120ms ease-out;
31
42
  }
32
- .nxgt-chip > [data-slot="chip-dismiss"]::after {
33
- content: "";
34
- position: absolute;
35
- inset: calc((var(--chip-x) - 24px) / 2);
36
- border-radius: 9999px;
37
- }
38
- /* Touch (#1067, #1102): on a coarse pointer the ✕ is a real 44x44 box,
39
- * not a pseudo-element, which a tap-target check and axe do not measure.
40
- * The negative margin takes the extra back, so the chip is unchanged, and
41
- * the padding with `background-clip: content-box` keeps the hover disc
42
- * and the focus ring at the drawn size (the outline is pulled in by the
43
- * padding). */
44
43
  @media (pointer: coarse) {
45
44
  .nxgt-chip > [data-slot="chip-dismiss"] {
46
- --chip-pad: calc((44px - var(--chip-x)) / 2);
47
- box-sizing: border-box;
48
- width: 44px;
49
- height: 44px;
50
- margin: calc(-1 * var(--chip-pad));
51
- padding: var(--chip-pad);
52
- background-clip: content-box;
53
- outline-offset: calc(1px - var(--chip-pad));
54
- }
55
- /* The box is the target now: the 24px pseudo-element would reach
56
- * past it. */
57
- .nxgt-chip > [data-slot="chip-dismiss"]::after {
58
- inset: 0;
45
+ --chip-hit: 44px;
59
46
  }
60
47
  }
61
48
  .nxgt-chip > [data-slot="chip-dismiss"] > svg {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/material",
3
- "version": "3.20.0",
3
+ "version": "3.21.0",
4
4
  "license": "MIT",
5
5
  "bugs": {
6
6
  "email": "stevegires@gmail.com"