@scalewing/react 1.9.0 → 1.10.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 +13 -0
- package/README.md +24 -0
- package/dist/calendar-button-labels.d.ts +19 -0
- package/dist/calendar-button-labels.js +10 -0
- package/dist/calendar-labels.d.ts +7 -0
- package/dist/calendar-labels.js +9 -0
- package/dist/calendar-month.d.ts +2 -0
- package/dist/calendar-month.js +5 -0
- package/dist/components/CalendarButton.d.ts +43 -0
- package/dist/components/CalendarButton.js +47 -0
- package/dist/components/DateField.js +12 -23
- package/dist/components/date-field/DateFieldGlyph.d.ts +3 -3
- package/dist/components/date-field/DateFieldGlyph.js +3 -3
- package/dist/components/date-field/use-calendar-popup.d.ts +36 -0
- package/dist/components/date-field/use-calendar-popup.js +43 -0
- package/dist/css/css-calendar-button.d.ts +2 -0
- package/dist/css/css-calendar-button.js +33 -0
- package/dist/css/css-components.js +3 -0
- package/dist/css/stacking.js +4 -2
- package/dist/css/stylesheet.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/styles.css +22 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 964e461: New `CalendarButton`: an icon-only `Button` that opens the `DateField` calendar dialog for a date the page already shows, such as a day heading with its own previous and next buttons (`docs/requests/teisoro-calendar-button.md`).
|
|
8
|
+
|
|
9
|
+
- Props: `label`, `value` (`YYYY-MM-DD`, never empty), `onChange`, and optional `min`, `max`, `disabled`, `locale`, `weekStartsOn`, `id`, `labels` (the calendar's words `previousMonth`, `nextMonth`, `month`, `year`, `today`, and `nameSeparator`), `size` (`xs`, `sm`, `md`; default `md`), and `variant` (a Button variant; default `ghost`). A `ref` reaches the `<button>`. New types `CalendarButtonProps` and `CalendarButtonLabels`.
|
|
10
|
+
- The accessible name is `label`, `labels.nameSeparator` (default `", "`, overridable per locale, such as `"、"` in Japanese), then the spoken date ("Choose survey day, Tuesday, September 22, 2026"), with `aria-haspopup="dialog"`, `aria-expanded`, and `aria-controls` while open. Enter, Space, or a press opens the calendar on `value` (a `value` outside `min`/`max` is kept, as on `DateField`, and the calendar opens on the nearest allowed day); choosing a day calls `onChange`, closes, and returns focus to the button; Escape and a press outside close without a change.
|
|
11
|
+
- An empty or invalid `value`, invalid bounds, locale, week start, or calendar words throw a `RangeError` with `DateField`'s messages. Two errors are new: `label must be non-empty text` and `labels.nameSeparator must be non-empty text`.
|
|
12
|
+
- New generated class `sw-calendar-button`: square at every Button size, and at least the 44 px md control height on a coarse pointer. The calendar reuses `sw-date-field-calendar` and its existing stacking layer.
|
|
13
|
+
|
|
14
|
+
No new dependencies.
|
|
15
|
+
|
|
3
16
|
## 1.9.0
|
|
4
17
|
|
|
5
18
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
BarChart,
|
|
13
13
|
Box,
|
|
14
14
|
Button,
|
|
15
|
+
CalendarButton,
|
|
15
16
|
Card,
|
|
16
17
|
Checkbox,
|
|
17
18
|
RadioGroup,
|
|
@@ -170,6 +171,29 @@ People type the date into a text entry in the locale's numeric order (`MM/DD/YYY
|
|
|
170
171
|
/>
|
|
171
172
|
```
|
|
172
173
|
|
|
174
|
+
`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.
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
<Inline gap={1}>
|
|
178
|
+
<Button aria-label="Previous day" variant="ghost" onPress={previousDay}>
|
|
179
|
+
‹
|
|
180
|
+
</Button>
|
|
181
|
+
<Text as="h2" variant="title">
|
|
182
|
+
{spokenSurveyDay}
|
|
183
|
+
</Text>
|
|
184
|
+
<CalendarButton
|
|
185
|
+
label="Choose survey day"
|
|
186
|
+
value={surveyDay}
|
|
187
|
+
onChange={setSurveyDay}
|
|
188
|
+
min="2026-09-01"
|
|
189
|
+
max="2026-09-30"
|
|
190
|
+
/>
|
|
191
|
+
<Button aria-label="Next day" variant="ghost" onPress={nextDay}>
|
|
192
|
+
›
|
|
193
|
+
</Button>
|
|
194
|
+
</Inline>
|
|
195
|
+
```
|
|
196
|
+
|
|
173
197
|
```tsx
|
|
174
198
|
<Switch
|
|
175
199
|
label="Active habitats only"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type DateFieldLabels } from './date-field-labels.js';
|
|
2
|
+
/**
|
|
3
|
+
* CalendarButton's own words: the calendar's words from DateFieldLabels,
|
|
4
|
+
* plus the text between its label and the spoken date in its name.
|
|
5
|
+
*/
|
|
6
|
+
export type CalendarButtonLabels = Pick<DateFieldLabels, 'previousMonth' | 'nextMonth' | 'month' | 'year' | 'today'> & {
|
|
7
|
+
/**
|
|
8
|
+
* Joins the label and the spoken date in the button's name. Defaults to
|
|
9
|
+
* ", "; pass the locale's own pause, such as "、" in Japanese.
|
|
10
|
+
*/
|
|
11
|
+
nameSeparator: string;
|
|
12
|
+
};
|
|
13
|
+
export declare const defaultNameSeparator = ", ";
|
|
14
|
+
export type ResolvedCalendarButtonLabels = {
|
|
15
|
+
calendar: DateFieldLabels;
|
|
16
|
+
nameSeparator: string;
|
|
17
|
+
};
|
|
18
|
+
/** Consumer words over the English defaults; empty words fail closed. */
|
|
19
|
+
export declare function resolveCalendarButtonLabels(labels?: Partial<CalendarButtonLabels>): ResolvedCalendarButtonLabels;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { resolveDateFieldLabels, } from './date-field-labels.js';
|
|
2
|
+
export const defaultNameSeparator = ', ';
|
|
3
|
+
/** Consumer words over the English defaults; empty words fail closed. */
|
|
4
|
+
export function resolveCalendarButtonLabels(labels = {}) {
|
|
5
|
+
const { nameSeparator = defaultNameSeparator, ...calendar } = labels;
|
|
6
|
+
// Only an empty string is refused: a lone space is a valid separator.
|
|
7
|
+
if (typeof nameSeparator !== 'string' || nameSeparator === '')
|
|
8
|
+
throw new RangeError('labels.nameSeparator must be non-empty text');
|
|
9
|
+
return { calendar: resolveDateFieldLabels(calendar), nameSeparator };
|
|
10
|
+
}
|
|
@@ -10,3 +10,10 @@ export declare function monthNames(locale: string): string[];
|
|
|
10
10
|
export declare function monthTitle(month: CalendarMonth, locale: string): string;
|
|
11
11
|
/** A spoken date such as "Sunday, March 10, 2024"; empty for no date. */
|
|
12
12
|
export declare function formatDateLabel(value: string, locale: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* An icon-only calendar trigger's name: its purpose, a separator, then the
|
|
15
|
+
* date it holds spoken in full, such as "Choose survey day, Tuesday,
|
|
16
|
+
* September 22, 2026". `Intl.ListFormat` has no neutral join (Spanish unit
|
|
17
|
+
* lists add "y", Chinese adds nothing), so the separator is the caller's.
|
|
18
|
+
*/
|
|
19
|
+
export declare function calendarTriggerName(purpose: string, value: string, locale: string, separator: string): string;
|
package/dist/calendar-labels.js
CHANGED
|
@@ -30,3 +30,12 @@ export function formatDateLabel(value, locale) {
|
|
|
30
30
|
return '';
|
|
31
31
|
return dateFormatter(locale, { dateStyle: 'full' }).format(formatterDate(parts));
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* An icon-only calendar trigger's name: its purpose, a separator, then the
|
|
35
|
+
* date it holds spoken in full, such as "Choose survey day, Tuesday,
|
|
36
|
+
* September 22, 2026". `Intl.ListFormat` has no neutral join (Spanish unit
|
|
37
|
+
* lists add "y", Chinese adds nothing), so the separator is the caller's.
|
|
38
|
+
*/
|
|
39
|
+
export function calendarTriggerName(purpose, value, locale, separator) {
|
|
40
|
+
return `${purpose}${separator}${formatDateLabel(value, locale)}`;
|
|
41
|
+
}
|
package/dist/calendar-month.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export type CalendarMonth = {
|
|
|
5
5
|
};
|
|
6
6
|
/** 0 is Sunday, 1 is Monday, matching `Date.prototype.getDay`. */
|
|
7
7
|
export type WeekStart = 0 | 1;
|
|
8
|
+
/** Fails closed on a first weekday other than Sunday or Monday. */
|
|
9
|
+
export declare function assertWeekStart(weekStartsOn: number): void;
|
|
8
10
|
export type CalendarCell = {
|
|
9
11
|
value: string;
|
|
10
12
|
day: number;
|
package/dist/calendar-month.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { daysInMonth, formatDateOnly, isValidDateParts, parseDateOnly, } from './date-only.js';
|
|
2
|
+
/** Fails closed on a first weekday other than Sunday or Monday. */
|
|
3
|
+
export function assertWeekStart(weekStartsOn) {
|
|
4
|
+
if (weekStartsOn !== 0 && weekStartsOn !== 1)
|
|
5
|
+
throw new RangeError('weekStartsOn must be 0 or 1');
|
|
6
|
+
}
|
|
2
7
|
/*
|
|
3
8
|
* Day arithmetic runs on a proleptic Gregorian day count, so it never passes
|
|
4
9
|
* through a Date, a time zone, or a daylight-saving jump.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type CalendarButtonLabels } from '../calendar-button-labels.js';
|
|
2
|
+
import { type WeekStart } from '../calendar-month.js';
|
|
3
|
+
import { type ButtonSize, type ButtonVariant } from './Button.js';
|
|
4
|
+
export type { CalendarButtonLabels };
|
|
5
|
+
export type CalendarButtonProps = {
|
|
6
|
+
/**
|
|
7
|
+
* What pressing it does, such as "Choose survey day". The button's name
|
|
8
|
+
* is this label, `labels.nameSeparator`, then `value` spoken in full; the
|
|
9
|
+
* calendar is named by the label.
|
|
10
|
+
*/
|
|
11
|
+
label: string;
|
|
12
|
+
/**
|
|
13
|
+
* Calendar date `YYYY-MM-DD`. The button always holds a date. A value
|
|
14
|
+
* outside `min`/`max` stays the value, as on DateField; the calendar then
|
|
15
|
+
* opens on the nearest allowed day.
|
|
16
|
+
*/
|
|
17
|
+
value: string;
|
|
18
|
+
/** Receives the chosen `YYYY-MM-DD`, never converted through UTC. */
|
|
19
|
+
onChange: (value: string) => void;
|
|
20
|
+
min?: string;
|
|
21
|
+
max?: string;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
/** The `<button>` element's id. */
|
|
24
|
+
id?: string;
|
|
25
|
+
/**
|
|
26
|
+
* BCP 47 tag for the spoken date and the calendar's month and weekday
|
|
27
|
+
* names. Defaults to the nearest `lang` attribute, then en-US.
|
|
28
|
+
*/
|
|
29
|
+
locale?: string;
|
|
30
|
+
/** First column of the calendar: 0 Sunday (default) or 1 Monday. */
|
|
31
|
+
weekStartsOn?: WeekStart;
|
|
32
|
+
/** The calendar's words and the name separator; English by default. */
|
|
33
|
+
labels?: Partial<CalendarButtonLabels>;
|
|
34
|
+
/** A Button size; a coarse pointer always gets a 44 px target. */
|
|
35
|
+
size?: ButtonSize;
|
|
36
|
+
variant?: ButtonVariant;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* An icon-only button that opens DateField's calendar dialog to pick a
|
|
40
|
+
* date for something the page already shows, such as a day heading. It has
|
|
41
|
+
* no text entry and no empty value. The ref reaches the `<button>`.
|
|
42
|
+
*/
|
|
43
|
+
export declare const CalendarButton: import("react").ForwardRefExoticComponent<CalendarButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { forwardRef, useCallback, useId } from 'react';
|
|
4
|
+
import { resolveCalendarButtonLabels, } from '../calendar-button-labels.js';
|
|
5
|
+
import { calendarTriggerName } from '../calendar-labels.js';
|
|
6
|
+
import { assertWeekStart } from '../calendar-month.js';
|
|
7
|
+
import { assertDateBounds, assertDateOnly } from '../date-only.js';
|
|
8
|
+
import { Button } from './Button.js';
|
|
9
|
+
import { CalendarDialog } from './date-field/CalendarDialog.js';
|
|
10
|
+
import { DateFieldGlyph } from './date-field/DateFieldGlyph.js';
|
|
11
|
+
import { useCalendarPopup } from './date-field/use-calendar-popup.js';
|
|
12
|
+
import { useLangLocale } from './date-field/use-lang-locale.js';
|
|
13
|
+
function assertLabel(label) {
|
|
14
|
+
if (typeof label !== 'string' || label.trim() === '')
|
|
15
|
+
throw new RangeError('label must be non-empty text');
|
|
16
|
+
}
|
|
17
|
+
/** Sets a forwarded ref, callback or object, to the same node. */
|
|
18
|
+
function setForwardedRef(ref, node) {
|
|
19
|
+
if (typeof ref === 'function')
|
|
20
|
+
ref(node);
|
|
21
|
+
else if (ref)
|
|
22
|
+
ref.current = node;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* An icon-only button that opens DateField's calendar dialog to pick a
|
|
26
|
+
* date for something the page already shows, such as a day heading. It has
|
|
27
|
+
* no text entry and no empty value. The ref reaches the `<button>`.
|
|
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) {
|
|
30
|
+
assertLabel(label);
|
|
31
|
+
assertDateOnly('value', value);
|
|
32
|
+
assertDateBounds(value, min, max);
|
|
33
|
+
assertWeekStart(weekStartsOn);
|
|
34
|
+
const words = resolveCalendarButtonLabels(labels);
|
|
35
|
+
const { buttonRef, shown, toggle, trigger, dialog } = useCalendarPopup({
|
|
36
|
+
value,
|
|
37
|
+
disabled,
|
|
38
|
+
onChange,
|
|
39
|
+
});
|
|
40
|
+
const resolvedLocale = useLangLocale(locale, buttonRef);
|
|
41
|
+
const labelId = useId();
|
|
42
|
+
const setButton = useCallback((node) => {
|
|
43
|
+
buttonRef.current = node;
|
|
44
|
+
setForwardedRef(ref, node);
|
|
45
|
+
}, [buttonRef, ref]);
|
|
46
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Button, { ...trigger, className: "sw-calendar-button", disabled: disabled, id: id, onPress: toggle, ref: setButton, size: size, variant: variant, children: [_jsx("span", { className: "sw-sr-only", children: calendarTriggerName(label, value, resolvedLocale, words.nameSeparator) }), _jsx(DateFieldGlyph, { name: "calendar" })] }), shown ? (_jsxs(_Fragment, { children: [_jsx("span", { hidden: true, id: labelId, children: label }), _jsx(CalendarDialog, { ...dialog, anchorRef: buttonRef, buttonRef: buttonRef, labelId: labelId, labels: words.calendar, locale: resolvedLocale, max: max, min: min, required: true, value: value, weekStartsOn: weekStartsOn })] })) : null] }));
|
|
47
|
+
});
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useId, useMemo, useRef
|
|
3
|
+
import { useEffect, useId, useMemo, useRef } from 'react';
|
|
4
|
+
import { assertWeekStart } from '../calendar-month.js';
|
|
4
5
|
import { resolveDateFieldLabels, } from '../date-field-labels.js';
|
|
5
6
|
import { dateEntryPattern, dateEntryPlaceholder } from '../date-entry.js';
|
|
6
7
|
import { dateEntryValidity } from '../date-entry-validity.js';
|
|
7
|
-
import { assertDateBounds, isOutsideDateRange
|
|
8
|
+
import { assertDateBounds, isOutsideDateRange } from '../date-only.js';
|
|
8
9
|
import { CalendarDialog } from './date-field/CalendarDialog.js';
|
|
9
10
|
import { DateFieldGlyph } from './date-field/DateFieldGlyph.js';
|
|
11
|
+
import { useCalendarPopup } from './date-field/use-calendar-popup.js';
|
|
10
12
|
import { useDateEntry } from './date-field/use-date-entry.js';
|
|
11
13
|
import { useLangLocale } from './date-field/use-lang-locale.js';
|
|
12
14
|
export function DateField({ label, value, onChange, min, max, disabled = false, required = false, description, error, locale, weekStartsOn = 0, labels, }) {
|
|
13
15
|
assertDateBounds(value, min, max);
|
|
14
|
-
|
|
15
|
-
throw new RangeError('weekStartsOn must be 0 or 1');
|
|
16
|
+
assertWeekStart(weekStartsOn);
|
|
16
17
|
const words = resolveDateFieldLabels(labels);
|
|
17
18
|
const rootRef = useRef(null);
|
|
18
19
|
const controlRef = useRef(null);
|
|
19
|
-
const buttonRef = useRef(null);
|
|
20
20
|
const inputRef = useRef(null);
|
|
21
21
|
const resolvedLocale = useLangLocale(locale, rootRef);
|
|
22
22
|
const pattern = useMemo(() => dateEntryPattern(resolvedLocale), [resolvedLocale]);
|
|
@@ -29,14 +29,14 @@ export function DateField({ label, value, onChange, min, max, disabled = false,
|
|
|
29
29
|
useEffect(() => {
|
|
30
30
|
inputRef.current?.setCustomValidity(validity);
|
|
31
31
|
}, [validity]);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
const { buttonRef, shown, toggle, trigger, dialog } = useCalendarPopup({
|
|
33
|
+
value,
|
|
34
|
+
disabled,
|
|
35
|
+
onChange,
|
|
36
|
+
beforeSelect: entry.reset,
|
|
37
|
+
});
|
|
37
38
|
const inputId = useId();
|
|
38
39
|
const labelId = useId();
|
|
39
|
-
const dialogId = useId();
|
|
40
40
|
const descriptionId = useId();
|
|
41
41
|
const entryErrorId = useId();
|
|
42
42
|
const errorId = useId();
|
|
@@ -48,17 +48,6 @@ export function DateField({ label, value, onChange, min, max, disabled = false,
|
|
|
48
48
|
.filter((id) => id !== null)
|
|
49
49
|
.join(' ');
|
|
50
50
|
const invalid = Boolean(error) || entry.invalid || isOutsideDateRange(value, min, max);
|
|
51
|
-
function close(restoreFocus) {
|
|
52
|
-
setOpen(false);
|
|
53
|
-
if (restoreFocus)
|
|
54
|
-
buttonRef.current?.focus();
|
|
55
|
-
}
|
|
56
|
-
function select(next) {
|
|
57
|
-
entry.reset();
|
|
58
|
-
close(true);
|
|
59
|
-
if (next !== value)
|
|
60
|
-
onChange(next);
|
|
61
|
-
}
|
|
62
51
|
return (_jsxs("div", { className: "sw-date-field", ref: rootRef, children: [_jsx("label", { className: "sw-date-field-label", htmlFor: inputId, id: labelId, children: label }), _jsxs("div", { className: "sw-date-field-control", ref: controlRef, children: [_jsx("input", { "aria-describedby": describedBy || undefined, "aria-invalid": invalid, autoComplete: "off", className: "sw-date-field-input", disabled: disabled, id: inputId, onBlur: entry.check, onChange: (event) => entry.change(event.currentTarget.value), onKeyDown: (event) => {
|
|
63
52
|
if (event.key === 'Enter')
|
|
64
53
|
entry.check();
|
|
@@ -66,5 +55,5 @@ export function DateField({ label, value, onChange, min, max, disabled = false,
|
|
|
66
55
|
day: words.dayPlaceholder,
|
|
67
56
|
month: words.monthPlaceholder,
|
|
68
57
|
year: words.yearPlaceholder,
|
|
69
|
-
}), ref: inputRef, required: required, spellCheck: false, type: "text", value: entry.text }), _jsxs("button", {
|
|
58
|
+
}), ref: inputRef, required: required, spellCheck: false, type: "text", value: entry.text }), _jsxs("button", { ...trigger, "aria-describedby": labelId, className: "sw-date-field-button", disabled: disabled, onClick: toggle, ref: buttonRef, type: "button", children: [_jsx("span", { className: "sw-sr-only", children: words.chooseDate }), _jsx(DateFieldGlyph, { name: "calendar" })] })] }), description ? (_jsx("span", { className: "sw-date-field-description", id: descriptionId, children: description })) : null, entry.invalid ? (_jsx("span", { className: "sw-date-field-error", id: entryErrorId, children: words.invalidEntry })) : null, error ? (_jsx("span", { className: "sw-date-field-error", id: errorId, children: error })) : null, shown ? (_jsx(CalendarDialog, { ...dialog, buttonRef: buttonRef, anchorRef: controlRef, labelId: labelId, labels: words, locale: resolvedLocale, max: max, min: min, required: required, value: value, weekStartsOn: weekStartsOn })) : null] }));
|
|
70
59
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export type DateFieldGlyphName = 'calendar' | 'previous' | 'next';
|
|
2
2
|
/**
|
|
3
|
-
* Private control chrome for DateField. Stroke, size,
|
|
4
|
-
* the generated `sw-date-field-glyph` rule; the
|
|
5
|
-
* right-to-left text.
|
|
3
|
+
* Private control chrome for DateField and CalendarButton. Stroke, size,
|
|
4
|
+
* and color come from the generated `sw-date-field-glyph` rule; the
|
|
5
|
+
* chevrons mirror in right-to-left text.
|
|
6
6
|
*/
|
|
7
7
|
export declare function DateFieldGlyph({ name }: {
|
|
8
8
|
name: DateFieldGlyphName;
|
|
@@ -6,9 +6,9 @@ const PATHS = {
|
|
|
6
6
|
next: 'M9.5 6l6 6-6 6',
|
|
7
7
|
};
|
|
8
8
|
/**
|
|
9
|
-
* Private control chrome for DateField. Stroke, size,
|
|
10
|
-
* the generated `sw-date-field-glyph` rule; the
|
|
11
|
-
* right-to-left text.
|
|
9
|
+
* Private control chrome for DateField and CalendarButton. Stroke, size,
|
|
10
|
+
* and color come from the generated `sw-date-field-glyph` rule; the
|
|
11
|
+
* chevrons mirror in right-to-left text.
|
|
12
12
|
*/
|
|
13
13
|
export function DateFieldGlyph({ name }) {
|
|
14
14
|
return (_jsx("svg", { "aria-hidden": "true", className: cx('sw-date-field-glyph', name !== 'calendar' && 'sw-date-field-glyph-directional'), focusable: "false", viewBox: "0 0 24 24", children: _jsx("path", { d: PATHS[name] }) }));
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
export type CalendarPopupOptions = {
|
|
3
|
+
value: string;
|
|
4
|
+
disabled: boolean;
|
|
5
|
+
onChange: (value: string) => void;
|
|
6
|
+
/** Runs before a chosen date closes the calendar, such as a text reset. */
|
|
7
|
+
beforeSelect?: () => void;
|
|
8
|
+
};
|
|
9
|
+
export type CalendarPopup = {
|
|
10
|
+
/** The button that toggles the calendar and takes focus back. */
|
|
11
|
+
buttonRef: RefObject<HTMLButtonElement | null>;
|
|
12
|
+
shown: boolean;
|
|
13
|
+
toggle: () => void;
|
|
14
|
+
/** ARIA for the toggling button, as the WAI-ARIA date picker asks. */
|
|
15
|
+
trigger: {
|
|
16
|
+
'aria-controls': string | undefined;
|
|
17
|
+
'aria-expanded': boolean;
|
|
18
|
+
'aria-haspopup': 'dialog';
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* CalendarDialog props that belong to the open and close cycle; pass
|
|
22
|
+
* `buttonRef` beside them.
|
|
23
|
+
*/
|
|
24
|
+
dialog: {
|
|
25
|
+
id: string;
|
|
26
|
+
onClose: (restoreFocus: boolean) => void;
|
|
27
|
+
onSelect: (value: string) => void;
|
|
28
|
+
today: string;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The open state of a calendar dialog toggled by one button, shared by
|
|
33
|
+
* DateField and CalendarButton. Disabling closes it for good; choosing a
|
|
34
|
+
* date closes it, returns focus to the button, and reports a changed date.
|
|
35
|
+
*/
|
|
36
|
+
export declare function useCalendarPopup({ value, disabled, onChange, beforeSelect, }: CalendarPopupOptions): CalendarPopup;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useId, useRef, useState } from 'react';
|
|
2
|
+
import { todayDateOnly } from '../../date-only.js';
|
|
3
|
+
/**
|
|
4
|
+
* The open state of a calendar dialog toggled by one button, shared by
|
|
5
|
+
* DateField and CalendarButton. Disabling closes it for good; choosing a
|
|
6
|
+
* date closes it, returns focus to the button, and reports a changed date.
|
|
7
|
+
*/
|
|
8
|
+
export function useCalendarPopup({ value, disabled, onChange, beforeSelect, }) {
|
|
9
|
+
const buttonRef = useRef(null);
|
|
10
|
+
const [open, setOpen] = useState(false);
|
|
11
|
+
// Disabling closes the calendar for good, not just while disabled.
|
|
12
|
+
if (disabled && open)
|
|
13
|
+
setOpen(false);
|
|
14
|
+
const shown = open && !disabled;
|
|
15
|
+
const id = useId();
|
|
16
|
+
function close(restoreFocus) {
|
|
17
|
+
setOpen(false);
|
|
18
|
+
if (restoreFocus)
|
|
19
|
+
buttonRef.current?.focus();
|
|
20
|
+
}
|
|
21
|
+
function select(next) {
|
|
22
|
+
beforeSelect?.();
|
|
23
|
+
close(true);
|
|
24
|
+
if (next !== value)
|
|
25
|
+
onChange(next);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
buttonRef,
|
|
29
|
+
shown,
|
|
30
|
+
toggle: () => (shown ? close(true) : setOpen(true)),
|
|
31
|
+
trigger: {
|
|
32
|
+
'aria-controls': shown ? id : undefined,
|
|
33
|
+
'aria-expanded': shown,
|
|
34
|
+
'aria-haspopup': 'dialog',
|
|
35
|
+
},
|
|
36
|
+
dialog: {
|
|
37
|
+
id,
|
|
38
|
+
onClose: close,
|
|
39
|
+
onSelect: select,
|
|
40
|
+
today: todayDateOnly(),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { buttonSizes } from '@scalewing/tokens';
|
|
2
|
+
/* The comfortable touch target: the md control height, 44 px, both ways. */
|
|
3
|
+
const touchTarget = 'var(--sw-control-md-min-height)';
|
|
4
|
+
/* Square at every Button size: as wide as the size's control height. */
|
|
5
|
+
function squareRules() {
|
|
6
|
+
return buttonSizes
|
|
7
|
+
.map((size) => `.sw-button-${size}.sw-calendar-button { min-width: var(--sw-control-${size}-min-height); }`)
|
|
8
|
+
.join('\n');
|
|
9
|
+
}
|
|
10
|
+
export function cssCalendarButtonClasses() {
|
|
11
|
+
return `.sw-button.sw-calendar-button {
|
|
12
|
+
flex: none;
|
|
13
|
+
padding-inline: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
${squareRules()}
|
|
17
|
+
|
|
18
|
+
.sw-calendar-button:not(.sw-button-md) .sw-date-field-glyph {
|
|
19
|
+
height: var(--sw-space-4);
|
|
20
|
+
width: var(--sw-space-4);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* A coarse pointer gets the full touch target at the smaller sizes too. */
|
|
24
|
+
@media (pointer: coarse) {
|
|
25
|
+
.sw-button.sw-calendar-button {
|
|
26
|
+
min-height: ${touchTarget};
|
|
27
|
+
min-width: ${touchTarget};
|
|
28
|
+
}
|
|
29
|
+
}`;
|
|
30
|
+
}
|
|
31
|
+
export function calendarButtonClassCatalog() {
|
|
32
|
+
return ['sw-calendar-button'];
|
|
33
|
+
}
|
|
@@ -4,6 +4,7 @@ import { cssActionBarClasses } from './css-action-bar.js';
|
|
|
4
4
|
import { cssActionMenuClasses } from './css-action-menu.js';
|
|
5
5
|
import { cssAccordionClasses } from './css-accordion.js';
|
|
6
6
|
import { cssButtonClasses } from './css-button.js';
|
|
7
|
+
import { cssCalendarButtonClasses } from './css-calendar-button.js';
|
|
7
8
|
import { cssChartClasses } from './css-chart.js';
|
|
8
9
|
import { cssCheckboxClasses } from './css-checkbox.js';
|
|
9
10
|
import { cssRadioGroupClasses } from './css-radio-group.js';
|
|
@@ -133,6 +134,8 @@ ${cssDataClasses()}
|
|
|
133
134
|
|
|
134
135
|
${cssDateFieldClasses()}
|
|
135
136
|
|
|
137
|
+
${cssCalendarButtonClasses()}
|
|
138
|
+
|
|
136
139
|
${cssChartClasses()}
|
|
137
140
|
|
|
138
141
|
${cssChromeClasses()}
|
package/dist/css/stacking.js
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* 4. `topChrome`: the sticky AppHeader, over a lifted surface that scrolls up
|
|
14
14
|
* under it (the header is the page's way back, and a popup opens downward).
|
|
15
15
|
* 5. `popup`: a popup outside any lifted surface (Select list, ActionMenu
|
|
16
|
-
* list, Tooltip, DateField calendar) over
|
|
16
|
+
* list, Tooltip, the DateField and CalendarButton calendar) over
|
|
17
|
+
* everything in the page.
|
|
17
18
|
*
|
|
18
19
|
* Dialog and Toast sit on the browser's top layer, above all of these.
|
|
19
20
|
*/
|
|
@@ -31,7 +32,8 @@ const openPopups = [
|
|
|
31
32
|
'.sw-select-list',
|
|
32
33
|
'.sw-action-menu-list:not([hidden])',
|
|
33
34
|
'.sw-tooltip:not([hidden])',
|
|
34
|
-
// Rendered only while open; on the
|
|
35
|
+
// DateField and CalendarButton share it. Rendered only while open; on the
|
|
36
|
+
// top layer where the popover API exists.
|
|
35
37
|
'.sw-date-field-calendar',
|
|
36
38
|
].join(', ');
|
|
37
39
|
const popupHosts = ['.sw-card-glass', '.sw-accordion'];
|
package/dist/css/stylesheet.js
CHANGED
|
@@ -4,6 +4,7 @@ import { actionMenuClassCatalog } from './css-action-menu.js';
|
|
|
4
4
|
import { accordionClassCatalog } from './css-accordion.js';
|
|
5
5
|
import { switchClassCatalog } from './css-switch.js';
|
|
6
6
|
import { dateFieldClassCatalog } from './css-date-field.js';
|
|
7
|
+
import { calendarButtonClassCatalog } from './css-calendar-button.js';
|
|
7
8
|
import { checkboxClassCatalog } from './css-checkbox.js';
|
|
8
9
|
import { radioGroupClassCatalog } from './css-radio-group.js';
|
|
9
10
|
import { spinnerClassCatalog } from './css-spinner.js';
|
|
@@ -57,6 +58,7 @@ export function utilityClassCatalog() {
|
|
|
57
58
|
...actionBarClassCatalog(),
|
|
58
59
|
...switchClassCatalog(),
|
|
59
60
|
...dateFieldClassCatalog(),
|
|
61
|
+
...calendarButtonClassCatalog(),
|
|
60
62
|
...checkboxClassCatalog(),
|
|
61
63
|
...radioGroupClassCatalog(),
|
|
62
64
|
...spinnerClassCatalog(),
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { Accordion } from './components/Accordion.js';
|
|
|
4
4
|
export type { AccordionProps, AccordionSize } from './components/Accordion.js';
|
|
5
5
|
export { Switch, type SwitchProps } from './components/Switch.js';
|
|
6
6
|
export { DateField, type DateFieldLabels, type DateFieldProps, type WeekStart, } from './components/DateField.js';
|
|
7
|
+
export { CalendarButton, type CalendarButtonLabels, type CalendarButtonProps, } from './components/CalendarButton.js';
|
|
7
8
|
export { Checkbox, type CheckboxProps } from './components/Checkbox.js';
|
|
8
9
|
export { Spinner, type SpinnerProps, type SpinnerSize, } from './components/Spinner.js';
|
|
9
10
|
export { Progress, type ProgressProps, type ProgressTone, } from './components/Progress.js';
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { ActionBar } from './components/ActionBar.js';
|
|
|
3
3
|
export { Accordion } from './components/Accordion.js';
|
|
4
4
|
export { Switch } from './components/Switch.js';
|
|
5
5
|
export { DateField, } from './components/DateField.js';
|
|
6
|
+
export { CalendarButton, } from './components/CalendarButton.js';
|
|
6
7
|
export { Checkbox } from './components/Checkbox.js';
|
|
7
8
|
export { Spinner, } from './components/Spinner.js';
|
|
8
9
|
export { Progress, } from './components/Progress.js';
|
package/dist/styles.css
CHANGED
|
@@ -1544,6 +1544,28 @@ body {
|
|
|
1544
1544
|
}
|
|
1545
1545
|
}
|
|
1546
1546
|
|
|
1547
|
+
.sw-button.sw-calendar-button {
|
|
1548
|
+
flex: none;
|
|
1549
|
+
padding-inline: 0;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
.sw-button-xs.sw-calendar-button { min-width: var(--sw-control-xs-min-height); }
|
|
1553
|
+
.sw-button-sm.sw-calendar-button { min-width: var(--sw-control-sm-min-height); }
|
|
1554
|
+
.sw-button-md.sw-calendar-button { min-width: var(--sw-control-md-min-height); }
|
|
1555
|
+
|
|
1556
|
+
.sw-calendar-button:not(.sw-button-md) .sw-date-field-glyph {
|
|
1557
|
+
height: var(--sw-space-4);
|
|
1558
|
+
width: var(--sw-space-4);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
/* A coarse pointer gets the full touch target at the smaller sizes too. */
|
|
1562
|
+
@media (pointer: coarse) {
|
|
1563
|
+
.sw-button.sw-calendar-button {
|
|
1564
|
+
min-height: var(--sw-control-md-min-height);
|
|
1565
|
+
min-width: var(--sw-control-md-min-height);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1547
1569
|
.sw-bar-chart {
|
|
1548
1570
|
display: grid;
|
|
1549
1571
|
gap: var(--sw-space-2);
|