@scalewing/react 1.9.0 → 1.10.1
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 +41 -0
- package/README.md +29 -1
- package/dist/anchored-position.d.ts +17 -5
- package/dist/anchored-position.js +19 -4
- 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/ActionMenu.js +2 -58
- 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/action-menu/ActionMenuList.d.ts +21 -0
- package/dist/components/action-menu/ActionMenuList.js +27 -0
- package/dist/components/date-field/CalendarDialog.js +1 -1
- 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/components/use-anchored-popover.d.ts +14 -0
- package/dist/components/{date-field/use-anchored-popover.js → use-anchored-popover.js} +31 -9
- package/dist/css/css-action-menu.js +25 -6
- package/dist/css/css-calendar-button.d.ts +2 -0
- package/dist/css/css-calendar-button.js +32 -0
- package/dist/css/css-components.js +5 -25
- package/dist/css/css-date-field.js +2 -1
- package/dist/css/css-palettes.js +11 -1
- package/dist/css/css-text.d.ts +1 -0
- package/dist/css/css-text.js +42 -0
- package/dist/css/stacking.js +6 -3
- package/dist/css/stylesheet.js +2 -0
- package/dist/css/touch-target.d.ts +2 -0
- package/dist/css/touch-target.js +7 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/palette/amber.css +2 -0
- package/dist/palette/capri.css +2 -0
- package/dist/palette/cerulean.css +2 -0
- package/dist/palette/cornflower.css +2 -0
- package/dist/palette/fuchsia.css +2 -0
- package/dist/palette/harvest.css +2 -0
- package/dist/palette/ink.css +2 -0
- package/dist/palette/mocha.css +2 -0
- package/dist/palette/navy.css +2 -0
- package/dist/palette/postcard.css +2 -0
- package/dist/palette/raspberry.css +2 -0
- package/dist/palette/signal.css +2 -0
- package/dist/palette/sky.css +2 -0
- package/dist/palette/soft-blue.css +2 -0
- package/dist/palette/sunburst.css +2 -0
- package/dist/palette/synthwave.css +2 -0
- package/dist/palette/tangerine.css +2 -0
- package/dist/styles.css +92 -13
- package/package.json +1 -1
- package/dist/components/date-field/use-anchored-popover.d.ts +0 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# @scalewing/react
|
|
2
2
|
|
|
3
|
+
## 1.10.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7c27935: `ActionMenu` places its menu with the shared anchored-popover code that the `DateField` and `CalendarButton` calendar use (`docs/requests/teisoro-action-menu.md`, 2026-09-28 follow-up). The menu now opens a `space-1` (4 px) gap below its trigger, or above it when it only fits there, keeps at least `space-2` (8 px) from every viewport edge, and aligns to the trigger's inline start (its right edge in right-to-left), or to the trigger's other edge when the trigger ends a row and the menu would not fit from its start. Before, it sat directly on the trigger and was pushed flush against the screen edge.
|
|
8
|
+
|
|
9
|
+
- The menu element is rendered only while it is open, like the calendar; it was a `hidden` element before. `aria-controls` already pointed at it only while open. A test that looked for the closed menu with `{ hidden: true }` now finds nothing.
|
|
10
|
+
- `.sw-action-menu-list` is as wide as its longest command (`width: max-content`, at least the 44 px control height) up to the viewport less both insets, so a long command wraps inside the screen instead of running off it. It resets the popover layer's `inset`, so the placed position holds in right-to-left documents too. Commands take the label line height with a `space-1` block padding, still 28 px tall on one line.
|
|
11
|
+
- The unused `.sw-action-menu-list[hidden]` rule is gone.
|
|
12
|
+
|
|
13
|
+
No API change and no new dependencies.
|
|
14
|
+
|
|
15
|
+
- ab61ca6: `ActionMenu` gives a coarse pointer the 44 px touch target (`docs/requests/teisoro-action-menu.md`, 2026-09-28 touch-target follow-up). Under `@media (pointer: coarse)` the trigger is at least `--sw-control-md-min-height` (44 px) tall and wide, and each command at least 44 px tall; a fine pointer keeps the compact xs height. This follows `CalendarButton`'s coarse-pointer rule, and both now read the target from one internal module.
|
|
16
|
+
|
|
17
|
+
Visual change on touch screens only. No API change and no new dependencies.
|
|
18
|
+
|
|
19
|
+
- df1fa80: The `ActionMenu` menu and the `DateField` and `CalendarButton` calendar are placed again when the popover or its anchor changes size while open, such as commands that change while the menu is open or a month with another row of weeks (`docs/requests/teisoro-action-menu.md`, 2026-09-28 placement follow-up). Before, their shared placement ran only on opening, scroll and window resize. It uses `ResizeObserver` where the runtime has it.
|
|
20
|
+
|
|
21
|
+
No API change and no new dependencies.
|
|
22
|
+
|
|
23
|
+
- 91665ce: The `DateField` and `CalendarButton` calendar and the `ActionMenu` menu now fit the viewport a classic scrollbar leaves (`docs/requests/teisoro-action-menu.md`, 2026-09-28 placement follow-up). Their shared placement measured `window.innerWidth` and `innerHeight`, which include a classic scrollbar, so a popover clamped to the right edge could sit under the scrollbar instead of `space-2` from it. It now measures the root element's client box. The calendar's `max-width` is `100%` of the top layer less both insets instead of `100vw`, as the menu's is.
|
|
24
|
+
|
|
25
|
+
Only pages with classic (non-overlay) scrollbars change. No API change and no new dependencies.
|
|
26
|
+
|
|
27
|
+
- 7b4058e: `Text` drops the browser's paragraph and heading margins (`docs/requests/teisoro-text.md`). Its `p` and `h1`–`h4` elements kept about 1em of margin above and below, which a flex `Stack` or `Inline` adds to its `gap`. A generated `:where(.sw-text-*) { margin: 0; }` rule removes it at zero specificity, so an authored margin (`sw-sr-only`, a consumer class) still wins.
|
|
28
|
+
|
|
29
|
+
Visual change: text in a `Stack` or `Inline` sits at the `gap` the page asked for, so dialog titles, section headings and card text are tighter. A page that needs more space raises its `gap`. No API change and no new dependencies.
|
|
30
|
+
|
|
31
|
+
## 1.10.0
|
|
32
|
+
|
|
33
|
+
### Minor Changes
|
|
34
|
+
|
|
35
|
+
- 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`).
|
|
36
|
+
|
|
37
|
+
- 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`.
|
|
38
|
+
- 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.
|
|
39
|
+
- 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`.
|
|
40
|
+
- 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.
|
|
41
|
+
|
|
42
|
+
No new dependencies.
|
|
43
|
+
|
|
3
44
|
## 1.9.0
|
|
4
45
|
|
|
5
46
|
### 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,
|
|
@@ -93,7 +94,7 @@ Import the CSS once at the application entry. Do not copy it into your source tr
|
|
|
93
94
|
</Stack>
|
|
94
95
|
```
|
|
95
96
|
|
|
96
|
-
`ActionMenu` opens independent commands from a labelled button. Provide localized command labels and callbacks; use `disabled` for unavailable commands and `destructive` for a dangerous command's presentation. Escape and choosing a command both return focus to the trigger (before the command runs, so a `Dialog` it opens hands focus back to the trigger on close), and outside interaction dismisses the menu.
|
|
97
|
+
`ActionMenu` opens independent commands from a labelled button. Provide localized command labels and callbacks; use `disabled` for unavailable commands and `destructive` for a dangerous command's presentation. Escape and choosing a command both return focus to the trigger (before the command runs, so a `Dialog` it opens hands focus back to the trigger on close), and outside interaction dismisses the menu. The open menu sits on the popover layer a `space-1` gap below its trigger (above it when it only fits there), at least `space-2` from every viewport edge (a classic scrollbar is not part of the viewport), and aligned to the trigger's inline start (its right edge in right-to-left), or to its other edge when the trigger ends a row and the menu would not fit from its start. The menu is as wide as its longest command up to the viewport less both insets, and a longer command wraps. The menu follows its trigger through scroll, window resize, and a change in the menu's or the trigger's size. The menu is in the DOM only while it is open. The trigger and its commands use the compact xs control height for a fine pointer; on a coarse pointer the trigger is at least 44 × 44 px (the md control height, as on `CalendarButton`) and every command is at least 44 px tall.
|
|
97
98
|
|
|
98
99
|
`Dialog` is a modal `<dialog>` controlled by `open`. Escape, a backdrop press, a platform close request, and a `<form method="dialog">` submit (or a submitter with `formmethod="dialog"`) are each prevented and call `onClose`; none of them closes the dialog itself. A consumer `onKeyDown`, `onCancel`, `onSubmit` or `onPointerDown` that prevents the event vetoes that request. Escape in a search field that holds text clears the field first, and a descendant's own `cancel` (a dismissed file picker) is not a close request. Set `open` to false to close it, or keep it true (for example while a form is saving) and it stays shown. `onClose` is not called when `open` turns false, and it is required: a dialog that must not be dismissed passes a callback that keeps `open` true.
|
|
99
100
|
|
|
@@ -113,8 +114,12 @@ Import the CSS once at the application entry. Do not copy it into your source tr
|
|
|
113
114
|
|
|
114
115
|
`Card` defaults to `glass`; `outlined` and `elevated` are solid surfaces, and `filled` is a quiet `--sw-color-subtle` fill with no border for plain information apart from pressable rows.
|
|
115
116
|
|
|
117
|
+
`--sw-color-accentSubtle` is the accent tint from `@scalewing/tokens`: a quiet fill behind accent text or glyphs that keeps 4.5:1. The stylesheet and every palette file set it, and a palette that moves the accent or surface retints it.
|
|
118
|
+
|
|
116
119
|
`Text` steps `display` and `heading` down to the compact token sizes below the `md` breakpoint; consumers do not size type per viewport.
|
|
117
120
|
|
|
121
|
+
`Text` has no margin of its own. Its default `p` and `h1`–`h4` elements drop the browser's block margins, so the `gap` of the `Stack` or `Inline` around it alone sets the spacing; put more space in that `gap`, not in a margin. The reset has zero specificity (`:where(.sw-text-*)`), so an authored margin, such as `sw-sr-only` or a consumer class, still applies.
|
|
122
|
+
|
|
118
123
|
`Text` takes an optional `align` (`start`, `center`, `end`) mapped to generated `sw-text-align-*` classes, for a heading that must stay centered when it wraps.
|
|
119
124
|
|
|
120
125
|
`Grid` places children in one to four (or six) equal-width columns with a token `gap` step. `columnsBelow={{ md: 2 }}` drops to fewer columns below the `md` breakpoint so tiles and stat cards stay readable on a phone. It accepts every `Box` prop.
|
|
@@ -170,6 +175,29 @@ People type the date into a text entry in the locale's numeric order (`MM/DD/YYY
|
|
|
170
175
|
/>
|
|
171
176
|
```
|
|
172
177
|
|
|
178
|
+
`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.
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
<Inline gap={1}>
|
|
182
|
+
<Button aria-label="Previous day" variant="ghost" onPress={previousDay}>
|
|
183
|
+
‹
|
|
184
|
+
</Button>
|
|
185
|
+
<Text as="h2" variant="title">
|
|
186
|
+
{spokenSurveyDay}
|
|
187
|
+
</Text>
|
|
188
|
+
<CalendarButton
|
|
189
|
+
label="Choose survey day"
|
|
190
|
+
value={surveyDay}
|
|
191
|
+
onChange={setSurveyDay}
|
|
192
|
+
min="2026-09-01"
|
|
193
|
+
max="2026-09-30"
|
|
194
|
+
/>
|
|
195
|
+
<Button aria-label="Next day" variant="ghost" onPress={nextDay}>
|
|
196
|
+
›
|
|
197
|
+
</Button>
|
|
198
|
+
</Inline>
|
|
199
|
+
```
|
|
200
|
+
|
|
173
201
|
```tsx
|
|
174
202
|
<Switch
|
|
175
203
|
label="Active habitats only"
|
|
@@ -16,13 +16,25 @@ export type AnchoredPosition = {
|
|
|
16
16
|
left: number;
|
|
17
17
|
top: number;
|
|
18
18
|
};
|
|
19
|
+
export type AnchoredPositionOptions = {
|
|
20
|
+
/** Space between the anchor and the popover, in px. */
|
|
21
|
+
gap?: number;
|
|
22
|
+
/** Space the popover keeps from every viewport edge, in px. */
|
|
23
|
+
inset?: number;
|
|
24
|
+
/** Right-to-left text: the anchor's inline start is its right edge. */
|
|
25
|
+
rtl?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* When the popover would cross the viewport's inline end aligned to the
|
|
28
|
+
* anchor's start, align it to the anchor's end instead (a menu opened from
|
|
29
|
+
* a trigger at the end of a row), as long as that fits.
|
|
30
|
+
*/
|
|
31
|
+
flipInline?: boolean;
|
|
32
|
+
};
|
|
19
33
|
/**
|
|
20
34
|
* Where a fixed popover goes beside its anchor: below and aligned to the
|
|
21
35
|
* anchor's inline start (its end in right-to-left), above when it does not
|
|
22
36
|
* fit below but does above, and always kept inside the viewport by `inset`.
|
|
37
|
+
* With `flipInline`, a popover that does not fit start-aligned lines up with
|
|
38
|
+
* the anchor's end instead.
|
|
23
39
|
*/
|
|
24
|
-
export declare function anchoredPosition(anchor: AnchorRect, size: PopoverSize, viewport: Viewport,
|
|
25
|
-
gap?: number | undefined;
|
|
26
|
-
inset?: number | undefined;
|
|
27
|
-
rtl?: boolean | undefined;
|
|
28
|
-
}): AnchoredPosition;
|
|
40
|
+
export declare function anchoredPosition(anchor: AnchorRect, size: PopoverSize, viewport: Viewport, options?: AnchoredPositionOptions): AnchoredPosition;
|
|
@@ -1,12 +1,27 @@
|
|
|
1
|
+
function fitsInline(left, width, viewport, inset) {
|
|
2
|
+
return left >= inset && left + width <= viewport.width - inset;
|
|
3
|
+
}
|
|
4
|
+
function inlineLeft(anchor, size, viewport, { inset = 0, rtl = false, flipInline = false }) {
|
|
5
|
+
const startAligned = rtl ? anchor.right - size.width : anchor.left;
|
|
6
|
+
const endAligned = rtl ? anchor.left : anchor.right - size.width;
|
|
7
|
+
const preferred = flipInline &&
|
|
8
|
+
!fitsInline(startAligned, size.width, viewport, inset) &&
|
|
9
|
+
fitsInline(endAligned, size.width, viewport, inset)
|
|
10
|
+
? endAligned
|
|
11
|
+
: startAligned;
|
|
12
|
+
const maxLeft = viewport.width - inset - size.width;
|
|
13
|
+
return Math.max(inset, Math.min(preferred, maxLeft));
|
|
14
|
+
}
|
|
1
15
|
/**
|
|
2
16
|
* Where a fixed popover goes beside its anchor: below and aligned to the
|
|
3
17
|
* anchor's inline start (its end in right-to-left), above when it does not
|
|
4
18
|
* fit below but does above, and always kept inside the viewport by `inset`.
|
|
19
|
+
* With `flipInline`, a popover that does not fit start-aligned lines up with
|
|
20
|
+
* the anchor's end instead.
|
|
5
21
|
*/
|
|
6
|
-
export function anchoredPosition(anchor, size, viewport,
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const left = Math.max(inset, Math.min(preferredLeft, maxLeft));
|
|
22
|
+
export function anchoredPosition(anchor, size, viewport, options = {}) {
|
|
23
|
+
const { gap = 0, inset = 0 } = options;
|
|
24
|
+
const left = inlineLeft(anchor, size, viewport, options);
|
|
10
25
|
const below = anchor.bottom + gap;
|
|
11
26
|
const above = anchor.top - gap - size.height;
|
|
12
27
|
const fitsBelow = below + size.height <= viewport.height - inset;
|
|
@@ -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.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useId, useLayoutEffect, useRef, useState, } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { ActionMenuList } from './action-menu/ActionMenuList.js';
|
|
5
5
|
function nextEnabled(items, current, direction) {
|
|
6
6
|
if (items.length === 0)
|
|
7
7
|
return -1;
|
|
@@ -14,9 +14,7 @@ function nextEnabled(items, current, direction) {
|
|
|
14
14
|
}
|
|
15
15
|
export function ActionMenu({ label, items, trigger, disabled = false, }) {
|
|
16
16
|
const menuId = useId();
|
|
17
|
-
const rootRef = useRef(null);
|
|
18
17
|
const triggerRef = useRef(null);
|
|
19
|
-
const menuRef = useRef(null);
|
|
20
18
|
const openRef = useRef(false);
|
|
21
19
|
const [open, setOpen] = useState(false);
|
|
22
20
|
const [focusIndex, setFocusIndex] = useState(-1);
|
|
@@ -41,55 +39,6 @@ export function ActionMenu({ label, items, trigger, disabled = false, }) {
|
|
|
41
39
|
setFocusIndex(index);
|
|
42
40
|
setOpen(true);
|
|
43
41
|
}
|
|
44
|
-
function positionMenu() {
|
|
45
|
-
const triggerNode = triggerRef.current;
|
|
46
|
-
const menuNode = menuRef.current;
|
|
47
|
-
if (!triggerNode || !menuNode)
|
|
48
|
-
return;
|
|
49
|
-
const rect = triggerNode.getBoundingClientRect();
|
|
50
|
-
const width = menuNode.offsetWidth;
|
|
51
|
-
const height = menuNode.offsetHeight;
|
|
52
|
-
menuNode.style.left = `${Math.max(0, Math.min(rect.left, window.innerWidth - width))}px`;
|
|
53
|
-
menuNode.style.top = `${Math.max(0, rect.bottom + height <= window.innerHeight ? rect.bottom : rect.top - height)}px`;
|
|
54
|
-
}
|
|
55
|
-
useLayoutEffect(() => {
|
|
56
|
-
const menuNode = menuRef.current;
|
|
57
|
-
if (!menuNode)
|
|
58
|
-
return;
|
|
59
|
-
const supportsPopover = typeof menuNode.showPopover === 'function';
|
|
60
|
-
if (supportsPopover)
|
|
61
|
-
menuNode.setAttribute('popover', 'manual');
|
|
62
|
-
if (!open) {
|
|
63
|
-
if (supportsPopover && menuNode.matches(':popover-open'))
|
|
64
|
-
menuNode.hidePopover();
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
if (supportsPopover && !menuNode.matches(':popover-open'))
|
|
68
|
-
menuNode.showPopover();
|
|
69
|
-
positionMenu();
|
|
70
|
-
const commands = menuNode.querySelectorAll('[role="menuitem"]');
|
|
71
|
-
commands[focusIndex]?.focus();
|
|
72
|
-
}, [focusIndex, open]);
|
|
73
|
-
useLayoutEffect(() => {
|
|
74
|
-
if (!open)
|
|
75
|
-
return;
|
|
76
|
-
function onPointerDown(event) {
|
|
77
|
-
const target = event.target;
|
|
78
|
-
if (target instanceof Node && !rootRef.current?.contains(target))
|
|
79
|
-
close();
|
|
80
|
-
}
|
|
81
|
-
function onMove() {
|
|
82
|
-
positionMenu();
|
|
83
|
-
}
|
|
84
|
-
document.addEventListener('pointerdown', onPointerDown);
|
|
85
|
-
window.addEventListener('resize', onMove);
|
|
86
|
-
window.addEventListener('scroll', onMove, true);
|
|
87
|
-
return () => {
|
|
88
|
-
document.removeEventListener('pointerdown', onPointerDown);
|
|
89
|
-
window.removeEventListener('resize', onMove);
|
|
90
|
-
window.removeEventListener('scroll', onMove, true);
|
|
91
|
-
};
|
|
92
|
-
}, [open]);
|
|
93
42
|
function onTriggerKeyDown(event) {
|
|
94
43
|
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
95
44
|
event.preventDefault();
|
|
@@ -135,10 +84,5 @@ export function ActionMenu({ label, items, trigger, disabled = false, }) {
|
|
|
135
84
|
close(true);
|
|
136
85
|
item.onSelect();
|
|
137
86
|
}
|
|
138
|
-
return (_jsxs("div", { className: "sw-action-menu",
|
|
139
|
-
if (event.key === 'Enter' || event.key === ' ') {
|
|
140
|
-
event.preventDefault();
|
|
141
|
-
select(item);
|
|
142
|
-
}
|
|
143
|
-
}, role: "menuitem", tabIndex: index === focusIndex ? 0 : -1, type: "button", children: [item.icon ? _jsx("span", { "aria-hidden": "true", children: item.icon }) : null, _jsx("span", { children: item.label })] }, item.id))) })] }));
|
|
87
|
+
return (_jsxs("div", { className: "sw-action-menu", children: [_jsx("button", { "aria-controls": open ? menuId : undefined, "aria-expanded": open, "aria-haspopup": "menu", "aria-label": label, className: "sw-action-menu-trigger", disabled: !enabled, onClick: () => (open ? close(true) : openMenu()), onKeyDown: onTriggerKeyDown, ref: triggerRef, type: "button", children: trigger ?? label }), open ? (_jsx(ActionMenuList, { focusIndex: focusIndex, id: menuId, items: items, label: label, onDismiss: () => close(), onFocusIndex: setFocusIndex, onKeyDown: onMenuKeyDown, onSelect: select, triggerRef: triggerRef })) : null] }));
|
|
144
88
|
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type KeyboardEvent, type RefObject } from 'react';
|
|
2
|
+
import type { ActionMenuItem } from '../ActionMenu.js';
|
|
3
|
+
type ActionMenuListProps = {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
items: readonly ActionMenuItem[];
|
|
7
|
+
focusIndex: number;
|
|
8
|
+
triggerRef: RefObject<HTMLButtonElement | null>;
|
|
9
|
+
onDismiss: () => void;
|
|
10
|
+
onFocusIndex: (index: number) => void;
|
|
11
|
+
onKeyDown: (event: KeyboardEvent<HTMLDivElement>) => void;
|
|
12
|
+
onSelect: (item: ActionMenuItem) => void;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The open menu: on the popover layer beside the trigger (a gap below or
|
|
16
|
+
* above it, inset from the viewport edges, and lined up with the trigger's
|
|
17
|
+
* end when it would not fit from its start), with the focused command
|
|
18
|
+
* holding DOM focus.
|
|
19
|
+
*/
|
|
20
|
+
export declare function ActionMenuList({ id, label, items, focusIndex, triggerRef, onDismiss, onFocusIndex, onKeyDown, onSelect, }: ActionMenuListProps): import("react").JSX.Element;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useLayoutEffect, useRef, } from 'react';
|
|
4
|
+
import { cx } from '../../class-names.js';
|
|
5
|
+
import { useAnchoredPopover } from '../use-anchored-popover.js';
|
|
6
|
+
/**
|
|
7
|
+
* The open menu: on the popover layer beside the trigger (a gap below or
|
|
8
|
+
* above it, inset from the viewport edges, and lined up with the trigger's
|
|
9
|
+
* end when it would not fit from its start), with the focused command
|
|
10
|
+
* holding DOM focus.
|
|
11
|
+
*/
|
|
12
|
+
export function ActionMenuList({ id, label, items, focusIndex, triggerRef, onDismiss, onFocusIndex, onKeyDown, onSelect, }) {
|
|
13
|
+
const menuRef = useRef(null);
|
|
14
|
+
useAnchoredPopover(menuRef, triggerRef, triggerRef, onDismiss, {
|
|
15
|
+
flipInline: true,
|
|
16
|
+
});
|
|
17
|
+
useLayoutEffect(() => {
|
|
18
|
+
const commands = menuRef.current?.querySelectorAll('[role="menuitem"]');
|
|
19
|
+
commands?.[focusIndex]?.focus();
|
|
20
|
+
}, [focusIndex]);
|
|
21
|
+
return (_jsx("div", { "aria-label": label, className: "sw-action-menu-list", id: id, onKeyDown: onKeyDown, ref: menuRef, role: "menu", children: items.map((item, index) => (_jsxs("button", { className: cx('sw-action-menu-item', item.destructive && 'sw-action-menu-item-danger'), disabled: item.disabled, onClick: () => onSelect(item), onFocus: () => onFocusIndex(index), onKeyDown: (event) => {
|
|
22
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
23
|
+
event.preventDefault();
|
|
24
|
+
onSelect(item);
|
|
25
|
+
}
|
|
26
|
+
}, role: "menuitem", tabIndex: index === focusIndex ? 0 : -1, type: "button", children: [item.icon ? _jsx("span", { "aria-hidden": "true", children: item.icon }) : null, _jsx("span", { children: item.label })] }, item.id))) }));
|
|
27
|
+
}
|
|
@@ -5,7 +5,7 @@ import { clampDateOnly, daysInMonth, formatDateOnly, isOutsideDateRange, parseDa
|
|
|
5
5
|
import { Button } from '../Button.js';
|
|
6
6
|
import { CalendarGrid } from './CalendarGrid.js';
|
|
7
7
|
import { CalendarHeader } from './CalendarHeader.js';
|
|
8
|
-
import { useAnchoredPopover } from '
|
|
8
|
+
import { useAnchoredPopover } from '../use-anchored-popover.js';
|
|
9
9
|
/** The dialog's tab stops: enabled buttons and the grid's roving day. */
|
|
10
10
|
function isTabStop(node) {
|
|
11
11
|
if (node instanceof HTMLButtonElement)
|
|
@@ -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] }) }));
|