@scalewing/react 1.20.0 → 1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @scalewing/react
2
2
 
3
+ ## 1.21.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2d54950: `DateField` and `CalendarButton` take an optional `today` (`YYYY-MM-DD`): the day the calendar marks as today (`aria-current="date"`), opens on when the field is empty, and picks with **Today**. It defaults to the device's local date, as before. Pass the business's own day when it keeps a time zone the device may not share, so a phone in UTC at 10 PM in New York no longer rings tomorrow while `max` disables it. An empty or malformed `today` throws a `RangeError`.
8
+
3
9
  ## 1.20.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -291,7 +291,7 @@ Values outside the catalog throw a `RangeError`. There is no per-breakpoint span
291
291
 
292
292
  `DateField` is a controlled date-only field that draws its own calendar. Supply `value`, `min`, and `max` as valid `YYYY-MM-DD` dates; use an empty `value` for a blank field. Its callback returns a date-only string or an empty string, without timezone conversion. Invalid serialized dates and inverted bounds throw; a value outside the bounds remains visible with invalid styling. `required` marks the label with the same `aria-hidden` asterisk as `Field` and sets the entry's native `required`. Its label is drawn as `Field`'s (the same label words and mark inside a `<label>` that keeps the canvas type), so its label row and its gap to the entry match a `Field` beside it. For a blank required field, supply `error` when form validation runs to show the message and invalid styling.
293
293
 
294
- People type the date into a text entry in the locale's numeric order (`MM/DD/YYYY` for en-US, `DD/MM/YYYY` for es), with any non-digit separator, eight bare digits, or ISO `YYYY-MM-DD`. A keystroke calls `onChange` once the entry is complete, meaning its last field is at full width (four year digits, or two digits for a trailing day or month, as in ISO); otherwise the date commits when the person leaves the field or presses Enter, so typing `2024-03-10` never sends `2024-03-01` on the way. If the parent keeps the old value, the typed text stays on screen; any other outside change replaces it. Text that is not a date keeps the last value and, once the person leaves the field, sets `aria-invalid` and shows `labels.invalidEntry`. Like the native date input, the entry blocks form submission through `setCustomValidity`: `labels.invalidEntry` while its text is not a date, and `labels.outOfRange` while its date, typed or given, is outside `min`/`max`. The calendar button beside it opens a WAI-ARIA date picker dialog anchored under the field on the popover layer: the month grid shows today, the selected day, and days outside `min`/`max` as `aria-disabled` (padding before `0001-01-01` or after `9999-12-31` is blank and inert); arrows move by day and week, Home and End to the week's edges, PageUp and PageDown by month, Shift with them by year, Enter or Space selects and closes, and Escape closes and returns focus to the button. Month and year selectors in the header jump decades (the year list spans 120 years back and 20 ahead of today and the shown month, cut to `min` and `max`; the month list offers only months with a day inside them). A new `value`, `min`, or `max` while the calendar is open moves it to the new value or back inside the bounds. **Today** selects today; **Clear** appears only when the field is not `required`.
294
+ People type the date into a text entry in the locale's numeric order (`MM/DD/YYYY` for en-US, `DD/MM/YYYY` for es), with any non-digit separator, eight bare digits, or ISO `YYYY-MM-DD`. A keystroke calls `onChange` once the entry is complete, meaning its last field is at full width (four year digits, or two digits for a trailing day or month, as in ISO); otherwise the date commits when the person leaves the field or presses Enter, so typing `2024-03-10` never sends `2024-03-01` on the way. If the parent keeps the old value, the typed text stays on screen; any other outside change replaces it. Text that is not a date keeps the last value and, once the person leaves the field, sets `aria-invalid` and shows `labels.invalidEntry`. Like the native date input, the entry blocks form submission through `setCustomValidity`: `labels.invalidEntry` while its text is not a date, and `labels.outOfRange` while its date, typed or given, is outside `min`/`max`. The calendar button beside it opens a WAI-ARIA date picker dialog anchored under the field on the popover layer: the month grid shows today, the selected day, and days outside `min`/`max` as `aria-disabled` (padding before `0001-01-01` or after `9999-12-31` is blank and inert); arrows move by day and week, Home and End to the week's edges, PageUp and PageDown by month, Shift with them by year, Enter or Space selects and closes, and Escape closes and returns focus to the button. Month and year selectors in the header jump decades (the year list spans 120 years back and 20 ahead of today and the shown month, cut to `min` and `max`; the month list offers only months with a day inside them). A new `value`, `min`, or `max` while the calendar is open moves it to the new value or back inside the bounds. **Today** selects today; **Clear** appears only when the field is not `required`. Today is the device's local date unless `today` (a `YYYY-MM-DD` date) is given: pass the business's own day when it keeps a time zone the device may not share, so a phone in another zone after the business's midnight does not mark tomorrow as today, open on it, or pick it while `max` is the business's day. `today` moves the today mark, the empty field's starting day, **Today**, and the year list's span; it may lie outside `min`/`max`, where **Today** is disabled as before. An empty or malformed `today` throws a `RangeError`.
295
295
 
296
296
  `locale` (BCP 47) sets month and weekday names, spoken dates, and the typed order; it defaults to the nearest `lang` attribute when the field mounts, then `en-US`, and a malformed tag throws. `weekStartsOn` is `0` (Sunday, the default) or `1` (Monday). `labels` overrides the control's own words, English by default: `chooseDate`, `previousMonth`, `nextMonth`, `month`, `year`, `today`, `clear`, `invalidEntry`, `outOfRange`, and the placeholder letters `dayPlaceholder`, `monthPlaceholder`, `yearPlaceholder`. The field label names only the text entry; the calendar button is named `labels.chooseDate` and described by the field label, so tests find the entry with `getByLabel` or `getByRole('textbox', { name })`.
297
297
 
@@ -314,7 +314,7 @@ People type the date into a text entry in the locale's numeric order (`MM/DD/YYY
314
314
  />
315
315
  ```
316
316
 
317
- `CalendarButton` is an icon-only `Button` that opens the same calendar dialog for a date the page already shows, such as a day heading with its own previous and next steps: `‹ Tuesday, September 22, 2026 [calendar] ›`. It has no text entry and no empty value. Supply `label` (what pressing it does, such as "Choose survey day"), a `YYYY-MM-DD` `value`, and `onChange`; optional `min`, `max`, `disabled`, `locale`, and `weekStartsOn` behave as on `DateField`, and `id` and a `ref` reach the `<button>`. `labels` takes the calendar's words (`previousMonth`, `nextMonth`, `month`, `year`, `today`) and `nameSeparator`. The button's accessible name is `label`, `labels.nameSeparator` (default `", "`), then the spoken date (`"Choose survey day, Tuesday, September 22, 2026"`); pass the locale's own pause where a comma does not fit, such as `"、"` in Japanese. It comes with `aria-haspopup="dialog"`, `aria-expanded`, and `aria-controls` while open; the calendar dialog is named by `label`. Enter, Space, or a press opens it on `value`. As on `DateField`, a `value` outside `min`/`max` is kept rather than refused, and the calendar then opens on the nearest allowed day with `value` shown selected and disabled; a button has no invalid state, so the page that shows the date flags it if it must. Choosing a day or **Today** calls `onChange` with the new date only when it changed, closes, and returns focus to the button; Escape closes without a change and returns focus; a press outside closes without a change and leaves focus where it landed. An empty or malformed `value`, `min`, or `max`, inverted bounds, a bad `locale` or `weekStartsOn`, an empty `label`, or an empty label word throws a `RangeError`. `size` (`xs`, `sm`, `md`; default `md`) and `variant` (default `ghost`) follow `Button`; the button is square at every size, and on a coarse pointer it is at least 44 px at every size. Place it beside the heading, not inside it, so the heading's text stays only the date.
317
+ `CalendarButton` is an icon-only `Button` that opens the same calendar dialog for a date the page already shows, such as a day heading with its own previous and next steps: `‹ Tuesday, September 22, 2026 [calendar] ›`. It has no text entry and no empty value. Supply `label` (what pressing it does, such as "Choose survey day"), a `YYYY-MM-DD` `value`, and `onChange`; optional `min`, `max`, `today`, `disabled`, `locale`, and `weekStartsOn` behave as on `DateField`, and `id` and a `ref` reach the `<button>`. `labels` takes the calendar's words (`previousMonth`, `nextMonth`, `month`, `year`, `today`) and `nameSeparator`. The button's accessible name is `label`, `labels.nameSeparator` (default `", "`), then the spoken date (`"Choose survey day, Tuesday, September 22, 2026"`); pass the locale's own pause where a comma does not fit, such as `"、"` in Japanese. It comes with `aria-haspopup="dialog"`, `aria-expanded`, and `aria-controls` while open; the calendar dialog is named by `label`. Enter, Space, or a press opens it on `value`. As on `DateField`, a `value` outside `min`/`max` is kept rather than refused, and the calendar then opens on the nearest allowed day with `value` shown selected and disabled; a button has no invalid state, so the page that shows the date flags it if it must. Choosing a day or **Today** calls `onChange` with the new date only when it changed, closes, and returns focus to the button; Escape closes without a change and returns focus; a press outside closes without a change and leaves focus where it landed. An empty or malformed `value`, `min`, `max`, or `today`, inverted bounds, a bad `locale` or `weekStartsOn`, an empty `label`, or an empty label word throws a `RangeError`. `size` (`xs`, `sm`, `md`; default `md`) and `variant` (default `ghost`) follow `Button`; the button is square at every size, and on a coarse pointer it is at least 44 px at every size. Place it beside the heading, not inside it, so the heading's text stays only the date.
318
318
 
319
319
  ```tsx
320
320
  <Inline gap={1}>
@@ -19,6 +19,12 @@ export type CalendarButtonProps = {
19
19
  onChange: (value: string) => void;
20
20
  min?: string;
21
21
  max?: string;
22
+ /**
23
+ * The calendar's today `YYYY-MM-DD`: the day it marks as today and picks
24
+ * with **Today**. Defaults to the device's local date; pass the business's
25
+ * day when it keeps its own time zone.
26
+ */
27
+ today?: string;
22
28
  disabled?: boolean;
23
29
  /** The `<button>` element's id. */
24
30
  id?: string;
@@ -26,7 +26,7 @@ function setForwardedRef(ref, node) {
26
26
  * date for something the page already shows, such as a day heading. It has
27
27
  * no text entry and no empty value. The ref reaches the `<button>`.
28
28
  */
29
- export const CalendarButton = forwardRef(function CalendarButton({ label, value, onChange, min, max, disabled = false, id, locale, weekStartsOn = 0, labels, size = 'md', variant = 'ghost', }, ref) {
29
+ export const CalendarButton = forwardRef(function CalendarButton({ label, value, onChange, min, max, today, disabled = false, id, locale, weekStartsOn = 0, labels, size = 'md', variant = 'ghost', }, ref) {
30
30
  assertLabel(label);
31
31
  assertDateOnly('value', value);
32
32
  assertDateBounds(value, min, max);
@@ -36,6 +36,7 @@ export const CalendarButton = forwardRef(function CalendarButton({ label, value,
36
36
  value,
37
37
  disabled,
38
38
  onChange,
39
+ today,
39
40
  });
40
41
  const resolvedLocale = useLangLocale(locale, buttonRef);
41
42
  const labelId = useId();
@@ -9,6 +9,13 @@ export type DateFieldProps = {
9
9
  onChange: (value: string) => void;
10
10
  min?: string;
11
11
  max?: string;
12
+ /**
13
+ * The calendar's today `YYYY-MM-DD`: the day it marks as today, opens on
14
+ * when the field is empty, and picks with **Today**. Defaults to the
15
+ * device's local date; pass the business's day when it keeps its own
16
+ * time zone.
17
+ */
18
+ today?: string;
12
19
  disabled?: boolean;
13
20
  required?: boolean;
14
21
  description?: string;
@@ -23,4 +30,4 @@ export type DateFieldProps = {
23
30
  /** The control's own words; English by default. */
24
31
  labels?: Partial<DateFieldLabels>;
25
32
  };
26
- export declare function DateField({ label, value, onChange, min, max, disabled, required, description, error, locale, weekStartsOn, labels, }: DateFieldProps): import("react").JSX.Element;
33
+ export declare function DateField({ label, value, onChange, min, max, today, disabled, required, description, error, locale, weekStartsOn, labels, }: DateFieldProps): import("react").JSX.Element;
@@ -13,7 +13,7 @@ import { DateFieldGlyph } from './date-field/DateFieldGlyph.js';
13
13
  import { useCalendarPopup } from './date-field/use-calendar-popup.js';
14
14
  import { useDateEntry } from './date-field/use-date-entry.js';
15
15
  import { useLangLocale } from './date-field/use-lang-locale.js';
16
- export function DateField({ label, value, onChange, min, max, disabled = false, required = false, description, error, locale, weekStartsOn = 0, labels, }) {
16
+ export function DateField({ label, value, onChange, min, max, today, disabled = false, required = false, description, error, locale, weekStartsOn = 0, labels, }) {
17
17
  assertDateBounds(value, min, max);
18
18
  assertWeekStart(weekStartsOn);
19
19
  const words = resolveDateFieldLabels(labels);
@@ -35,6 +35,7 @@ export function DateField({ label, value, onChange, min, max, disabled = false,
35
35
  value,
36
36
  disabled,
37
37
  onChange,
38
+ today,
38
39
  beforeSelect: entry.reset,
39
40
  });
40
41
  const inputId = useId();
@@ -3,6 +3,11 @@ export type CalendarPopupOptions = {
3
3
  value: string;
4
4
  disabled: boolean;
5
5
  onChange: (value: string) => void;
6
+ /**
7
+ * The calendar's today (`YYYY-MM-DD`); the device's local date when left
8
+ * out. An empty or malformed date throws a RangeError.
9
+ */
10
+ today?: string;
6
11
  /** Runs before a chosen date closes the calendar, such as a text reset. */
7
12
  beforeSelect?: () => void;
8
13
  };
@@ -33,4 +38,4 @@ export type CalendarPopup = {
33
38
  * DateField and CalendarButton. Disabling closes it for good; choosing a
34
39
  * date closes it, returns focus to the button, and reports a changed date.
35
40
  */
36
- export declare function useCalendarPopup({ value, disabled, onChange, beforeSelect, }: CalendarPopupOptions): CalendarPopup;
41
+ export declare function useCalendarPopup({ value, disabled, onChange, today, beforeSelect, }: CalendarPopupOptions): CalendarPopup;
@@ -1,11 +1,13 @@
1
1
  import { useId, useRef, useState } from 'react';
2
- import { todayDateOnly } from '../../date-only.js';
2
+ import { assertDateOnly, todayDateOnly } from '../../date-only.js';
3
3
  /**
4
4
  * The open state of a calendar dialog toggled by one button, shared by
5
5
  * DateField and CalendarButton. Disabling closes it for good; choosing a
6
6
  * date closes it, returns focus to the button, and reports a changed date.
7
7
  */
8
- export function useCalendarPopup({ value, disabled, onChange, beforeSelect, }) {
8
+ export function useCalendarPopup({ value, disabled, onChange, today, beforeSelect, }) {
9
+ if (today !== undefined)
10
+ assertDateOnly('today', today);
9
11
  const buttonRef = useRef(null);
10
12
  const [open, setOpen] = useState(false);
11
13
  // Disabling closes the calendar for good, not just while disabled.
@@ -37,7 +39,7 @@ export function useCalendarPopup({ value, disabled, onChange, beforeSelect, }) {
37
39
  id,
38
40
  onClose: close,
39
41
  onSelect: select,
40
- today: todayDateOnly(),
42
+ today: today ?? todayDateOnly(),
41
43
  },
42
44
  };
43
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalewing/react",
3
- "version": "1.20.0",
3
+ "version": "1.21.0",
4
4
  "description": "Scalewing DOM components and package-owned CSS utilities.",
5
5
  "type": "module",
6
6
  "license": "MIT",