@astryxdesign/core 0.4.3 → 0.4.4-canary.0e8af82
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 +60 -0
- package/README.md +79 -45
- package/dist/Banner/Banner.d.ts +3 -0
- package/dist/Banner/Banner.d.ts.map +1 -1
- package/dist/Banner/Banner.js +13 -3
- package/dist/BottomSheet/BottomSheet.d.ts +75 -0
- package/dist/BottomSheet/BottomSheet.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheet.js +396 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts +63 -0
- package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetPanel.js +441 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts +61 -0
- package/dist/BottomSheet/BottomSheetSwitcher.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetSwitcher.js +410 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts +21 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.d.ts.map +1 -0
- package/dist/BottomSheet/BottomSheetSwitcherContext.js +13 -0
- package/dist/BottomSheet/index.d.ts +11 -0
- package/dist/BottomSheet/index.d.ts.map +1 -0
- package/dist/BottomSheet/index.js +12 -0
- package/dist/BottomSheet/snapOffsets.d.ts +109 -0
- package/dist/BottomSheet/snapOffsets.d.ts.map +1 -0
- package/dist/BottomSheet/snapOffsets.js +206 -0
- package/dist/BottomSheet/useMobileKeyboard.d.ts +35 -0
- package/dist/BottomSheet/useMobileKeyboard.d.ts.map +1 -0
- package/dist/BottomSheet/useMobileKeyboard.js +373 -0
- package/dist/BottomSheet/useSheetGestures.d.ts +185 -0
- package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -0
- package/dist/BottomSheet/useSheetGestures.js +1038 -0
- package/dist/Calendar/Calendar.d.ts +17 -0
- package/dist/Calendar/Calendar.d.ts.map +1 -1
- package/dist/Calendar/Calendar.js +25 -1
- package/dist/Calendar/hooks/useCalendarConstraints.d.ts +22 -1
- package/dist/Calendar/hooks/useCalendarConstraints.d.ts.map +1 -1
- package/dist/Calendar/hooks/useCalendarConstraints.js +26 -4
- package/dist/Chat/ChatComposerInput.d.ts.map +1 -1
- package/dist/Chat/ChatComposerInput.js +3 -4
- package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
- package/dist/CheckboxInput/CheckboxInput.js +9 -0
- package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
- package/dist/ComplexSelector/ComplexSelector.js +18 -7
- package/dist/ContextMenu/ContextMenu.d.ts.map +1 -1
- package/dist/ContextMenu/ContextMenu.js +4 -2
- package/dist/DateInput/DateInput.d.ts.map +1 -1
- package/dist/DateInput/DateInput.js +15 -2
- package/dist/DateRangeInput/DateRangeInput.d.ts +23 -1
- package/dist/DateRangeInput/DateRangeInput.d.ts.map +1 -1
- package/dist/DateRangeInput/DateRangeInput.js +38 -3
- package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
- package/dist/DateTimeInput/DateTimeInput.js +38 -3
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +2 -1
- package/dist/Field/FieldLabel.d.ts +2 -1
- package/dist/Field/FieldLabel.d.ts.map +1 -1
- package/dist/Field/FieldLabel.js +21 -3
- package/dist/Field/InputClearButton.d.ts +2 -1
- package/dist/Field/InputClearButton.d.ts.map +1 -1
- package/dist/Field/InputClearButton.js +6 -1
- package/dist/FormLayout/FormLayout.d.ts +24 -2
- package/dist/FormLayout/FormLayout.d.ts.map +1 -1
- package/dist/FormLayout/FormLayout.js +7 -2
- package/dist/FormLayout/FormLayoutContext.d.ts +15 -2
- package/dist/FormLayout/FormLayoutContext.d.ts.map +1 -1
- package/dist/FormLayout/FormLayoutContext.js +19 -4
- package/dist/FormLayout/index.d.ts +1 -1
- package/dist/FormLayout/index.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.d.ts.map +1 -1
- package/dist/Lightbox/Lightbox.js +9 -3
- package/dist/MobileNav/MobileNav.d.ts +12 -0
- package/dist/MobileNav/MobileNav.d.ts.map +1 -1
- package/dist/MobileNav/MobileNav.js +102 -30
- package/dist/MultiSelector/MultiSelector.d.ts.map +1 -1
- package/dist/MultiSelector/MultiSelector.js +27 -8
- package/dist/NumberInput/NumberInput.d.ts.map +1 -1
- package/dist/NumberInput/NumberInput.js +15 -2
- package/dist/PowerSearch/PowerSearchEditPopover.js +1 -1
- package/dist/RadioList/RadioList.d.ts.map +1 -1
- package/dist/RadioList/RadioList.js +12 -1
- package/dist/Selector/Selector.d.ts.map +1 -1
- package/dist/Selector/Selector.js +20 -4
- package/dist/StatusDot/StatusDot.d.ts +43 -3
- package/dist/StatusDot/StatusDot.d.ts.map +1 -1
- package/dist/StatusDot/StatusDot.js +42 -5
- package/dist/Switch/Switch.d.ts.map +1 -1
- package/dist/Switch/Switch.js +14 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.d.ts.map +1 -1
- package/dist/Table/plugins/rowExpansion/useTableRowExpansion.js +7 -2
- package/dist/TextArea/TextArea.d.ts.map +1 -1
- package/dist/TextArea/TextArea.js +6 -1
- package/dist/TextInput/TextInput.d.ts.map +1 -1
- package/dist/TextInput/TextInput.js +6 -1
- package/dist/TimeInput/TimeInput.d.ts.map +1 -1
- package/dist/TimeInput/TimeInput.js +14 -2
- package/dist/Tokenizer/Tokenizer.js +11 -5
- package/dist/Tooltip/useTooltip.d.ts.map +1 -1
- package/dist/Tooltip/useTooltip.js +4 -1
- package/dist/Typeahead/BaseTypeahead.d.ts.map +1 -1
- package/dist/Typeahead/BaseTypeahead.js +5 -3
- package/dist/Typeahead/Typeahead.d.ts.map +1 -1
- package/dist/Typeahead/Typeahead.js +10 -1
- package/dist/astryx.css +21 -0
- package/dist/hooks/index.d.ts +8 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +8 -1
- package/dist/hooks/useFocusTrap.d.ts +0 -11
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.js +1 -11
- package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
- package/dist/hooks/useInputStatusIcon.js +1 -0
- package/dist/hooks/useResolvedRequired.d.ts +19 -0
- package/dist/hooks/useResolvedRequired.d.ts.map +1 -0
- package/dist/hooks/useResolvedRequired.js +40 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/theme/defineTheme.d.ts +22 -4
- package/dist/theme/defineTheme.d.ts.map +1 -1
- package/dist/theme/expandColorScale.d.ts +17 -2
- package/dist/theme/expandColorScale.d.ts.map +1 -1
- package/dist/theme/expandColorScale.js +63 -33
- package/dist/utils/ime.d.ts +5 -0
- package/dist/utils/ime.d.ts.map +1 -0
- package/dist/utils/ime.js +63 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -1
- package/dist/utils/plainDate.d.ts +6 -0
- package/dist/utils/plainDate.d.ts.map +1 -1
- package/dist/utils/plainDate.js +12 -0
- package/locales/af-ZA.json +958 -0
- package/locales/ar-SA.json +998 -0
- package/locales/ca-ES.json +974 -0
- package/locales/cs-CZ.json +990 -0
- package/locales/da-DK.json +966 -0
- package/locales/de-DE.json +966 -0
- package/locales/el-GR.json +990 -0
- package/locales/en.json +52 -0
- package/locales/es-ES.json +978 -0
- package/locales/fi-FI.json +990 -0
- package/locales/fr-FR.json +928 -0
- package/locales/he-IL.json +990 -0
- package/locales/hu-HU.json +990 -0
- package/locales/it-IT.json +978 -0
- package/locales/ja-JP.json +998 -0
- package/locales/ko-KR.json +990 -0
- package/locales/nl-NL.json +954 -0
- package/locales/no-NO.json +978 -0
- package/locales/pl-PL.json +982 -0
- package/locales/pseudo.json +39 -0
- package/locales/pt-BR.json +982 -0
- package/locales/pt-PT.json +982 -0
- package/locales/ro-RO.json +982 -0
- package/locales/ru-RU.json +990 -0
- package/locales/sr-SP.json +990 -0
- package/locales/sv-SE.json +986 -0
- package/locales/tr-TR.json +990 -0
- package/locales/uk-UA.json +990 -0
- package/locales/vi-VN.json +986 -0
- package/locales/zh-CN.json +998 -0
- package/locales/zh-TW.json +998 -0
- package/package.json +9 -8
- package/src/Banner/Banner.doc.mjs +3 -3
- package/src/Banner/Banner.test.tsx +40 -0
- package/src/Banner/Banner.tsx +27 -2
- package/src/BottomSheet/BottomSheet.doc.mjs +299 -0
- package/src/BottomSheet/BottomSheet.test.tsx +2265 -0
- package/src/BottomSheet/BottomSheet.tsx +559 -0
- package/src/BottomSheet/BottomSheetPanel.test.tsx +240 -0
- package/src/BottomSheet/BottomSheetPanel.tsx +601 -0
- package/src/BottomSheet/BottomSheetSwitcher.doc.mjs +219 -0
- package/src/BottomSheet/BottomSheetSwitcher.test.tsx +850 -0
- package/src/BottomSheet/BottomSheetSwitcher.tsx +628 -0
- package/src/BottomSheet/BottomSheetSwitcherContext.ts +51 -0
- package/src/BottomSheet/index.ts +19 -0
- package/src/BottomSheet/snapOffsets.test.ts +254 -0
- package/src/BottomSheet/snapOffsets.ts +245 -0
- package/src/BottomSheet/useMobileKeyboard.ts +499 -0
- package/src/BottomSheet/useSheetGestures.test.ts +1112 -0
- package/src/BottomSheet/useSheetGestures.ts +1503 -0
- package/src/Calendar/Calendar.doc.mjs +16 -0
- package/src/Calendar/Calendar.test.tsx +96 -0
- package/src/Calendar/Calendar.tsx +55 -0
- package/src/Calendar/hooks/useCalendarConstraints.ts +54 -3
- package/src/Chat/ChatComposerInput.tsx +3 -4
- package/src/CheckboxInput/CheckboxInput.tsx +6 -0
- package/src/ComplexSelector/ComplexSelector.tsx +15 -6
- package/src/ContextMenu/ContextMenu.tsx +4 -2
- package/src/DateInput/DateInput.test.tsx +21 -0
- package/src/DateInput/DateInput.tsx +12 -2
- package/src/DateRangeInput/DateRangeInput.doc.mjs +16 -0
- package/src/DateRangeInput/DateRangeInput.test.tsx +81 -1
- package/src/DateRangeInput/DateRangeInput.tsx +72 -1
- package/src/DateTimeInput/DateTimeInput.doc.mjs +4 -0
- package/src/DateTimeInput/DateTimeInput.test.tsx +137 -0
- package/src/DateTimeInput/DateTimeInput.tsx +43 -4
- package/src/Dialog/Dialog.tsx +2 -1
- package/src/Field/Field.doc.mjs +1 -0
- package/src/Field/FieldLabel.tsx +20 -4
- package/src/Field/InputClearButton.test.tsx +25 -0
- package/src/Field/InputClearButton.tsx +4 -1
- package/src/FileInput/FileInput.test.tsx +53 -9
- package/src/FormLayout/FormLayout.doc.mjs +13 -0
- package/src/FormLayout/FormLayout.test.tsx +181 -4
- package/src/FormLayout/FormLayout.tsx +33 -2
- package/src/FormLayout/FormLayoutContext.ts +22 -7
- package/src/FormLayout/index.ts +1 -1
- package/src/Lightbox/Lightbox.test.tsx +49 -28
- package/src/Lightbox/Lightbox.tsx +7 -3
- package/src/MobileNav/MobileNav.tsx +128 -37
- package/src/MobileNav/MobileNavCloseEdgeCases.test.tsx +426 -0
- package/src/MobileNav/MobileNavCloseTiming.test.ts +58 -0
- package/src/MobileNav/MobileNavCloseVisibility.test.tsx +172 -0
- package/src/MultiSelector/MultiSelector.test.tsx +195 -51
- package/src/MultiSelector/MultiSelector.tsx +25 -9
- package/src/NumberInput/NumberInput.test.tsx +18 -0
- package/src/NumberInput/NumberInput.tsx +12 -2
- package/src/PowerSearch/PowerSearchEditPopover.tsx +1 -1
- package/src/RadioList/RadioList.tsx +9 -1
- package/src/Selector/Selector.test.tsx +85 -26
- package/src/Selector/Selector.tsx +16 -5
- package/src/StatusDot/StatusDot.doc.mjs +13 -0
- package/src/StatusDot/StatusDot.test.tsx +115 -8
- package/src/StatusDot/StatusDot.tsx +74 -9
- package/src/Switch/Switch.test.tsx +16 -0
- package/src/Switch/Switch.tsx +13 -0
- package/src/Table/plugins/rowExpansion/useTableRowExpansion.tsx +20 -2
- package/src/TextArea/TextArea.tsx +3 -1
- package/src/TextInput/TextInput.tsx +3 -1
- package/src/TimeInput/TimeInput.test.tsx +23 -0
- package/src/TimeInput/TimeInput.tsx +11 -1
- package/src/Tokenizer/Tokenizer.test.tsx +50 -32
- package/src/Tokenizer/Tokenizer.tsx +5 -5
- package/src/Tooltip/useTooltip.tsx +4 -1
- package/src/Typeahead/BaseTypeahead.tsx +10 -3
- package/src/Typeahead/Typeahead.test.tsx +116 -7
- package/src/Typeahead/Typeahead.tsx +10 -1
- package/src/__tests__/stubMatchMedia.test.ts +79 -0
- package/src/__tests__/stubMatchMedia.ts +95 -0
- package/src/hooks/index.ts +9 -5
- package/src/hooks/useFocusTrap.ts +1 -14
- package/src/hooks/useInputStatusIcon.test.tsx +19 -0
- package/src/hooks/useInputStatusIcon.tsx +7 -0
- package/src/hooks/useResolvedRequired.ts +42 -0
- package/src/index.ts +1 -0
- package/src/theme/defineTheme.ts +22 -4
- package/src/theme/expandColorScale.test.ts +138 -0
- package/src/theme/expandColorScale.ts +89 -44
- package/src/utils/ime.test.ts +32 -0
- package/src/utils/ime.ts +69 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/plainDate.test.ts +50 -0
- package/src/utils/plainDate.ts +12 -0
- package/dist/astryx.umd.js +0 -147
- package/dist/astryx.umd.js.map +0 -7
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file stubMatchMedia.ts
|
|
5
|
+
* @input Uses vitest
|
|
6
|
+
* @output stubMatchMedia — a query-aware window.matchMedia stub
|
|
7
|
+
* @position Shared test helper; imported by component tests that render below
|
|
8
|
+
* a breakpoint while also exercising motion-sensitive behaviour
|
|
9
|
+
*
|
|
10
|
+
* jsdom has no matchMedia, so tests stub it. The usual stub answers every query
|
|
11
|
+
* with one boolean:
|
|
12
|
+
*
|
|
13
|
+
* vi.stubGlobal('matchMedia', vi.fn().mockReturnValue({matches: true, ...}))
|
|
14
|
+
*
|
|
15
|
+
* That is enough to put AppShell below its breakpoint — and it silently also
|
|
16
|
+
* matches `prefers-reduced-motion`, rerouting any code that branches on motion.
|
|
17
|
+
* MobileNav's close delay is capped at 0 under reduced motion, so a suite using
|
|
18
|
+
* the blanket stub runs against an immediate close while its test names claim
|
|
19
|
+
* it is exercising the real delay. The failure mode is invisible: every test
|
|
20
|
+
* still passes.
|
|
21
|
+
*
|
|
22
|
+
* So the two answers are separate knobs here. `reduceMotion` answers
|
|
23
|
+
* `prefers-reduced-motion` only; `matches` answers everything else (breakpoint
|
|
24
|
+
* width queries, which tests below a breakpoint want to be true).
|
|
25
|
+
*
|
|
26
|
+
* SYNC: When modified, update this header.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import {vi} from 'vitest';
|
|
30
|
+
|
|
31
|
+
export interface StubMatchMediaOptions {
|
|
32
|
+
/** Whether the user prefers reduced motion. */
|
|
33
|
+
reduceMotion?: boolean;
|
|
34
|
+
/** Answer for every other query — width breakpoints, hover, etc. */
|
|
35
|
+
matches?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Answer a `prefers-reduced-motion` query, or null if it isn't one.
|
|
40
|
+
*
|
|
41
|
+
* The preference has two spellings and they are opposites, so a single
|
|
42
|
+
* `includes('prefers-reduced-motion')` would answer `no-preference` with the
|
|
43
|
+
* value meant for `reduce` — the same answer-without-reading mistake this
|
|
44
|
+
* helper exists to prevent. Core only queries `reduce` today (MobileNav,
|
|
45
|
+
* Carousel); handling both is what keeps that true if one changes.
|
|
46
|
+
*/
|
|
47
|
+
function answerReducedMotion(
|
|
48
|
+
query: string,
|
|
49
|
+
reduceMotion: boolean,
|
|
50
|
+
): boolean | null {
|
|
51
|
+
if (query.includes('prefers-reduced-motion: reduce')) {
|
|
52
|
+
return reduceMotion;
|
|
53
|
+
}
|
|
54
|
+
if (query.includes('prefers-reduced-motion: no-preference')) {
|
|
55
|
+
return !reduceMotion;
|
|
56
|
+
}
|
|
57
|
+
// Bare `(prefers-reduced-motion)` is true for any value other than
|
|
58
|
+
// no-preference, i.e. the same as `reduce`.
|
|
59
|
+
if (query.includes('prefers-reduced-motion')) {
|
|
60
|
+
return reduceMotion;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Install a `window.matchMedia` stub whose answer depends on the query.
|
|
67
|
+
*
|
|
68
|
+
* Call from `beforeEach` (or inside a test that needs different motion
|
|
69
|
+
* settings) and pair with `vi.unstubAllGlobals()` in `afterEach`.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```
|
|
73
|
+
* beforeEach(() => {
|
|
74
|
+
* stubMatchMedia({reduceMotion: false});
|
|
75
|
+
* });
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export function stubMatchMedia({
|
|
79
|
+
reduceMotion = false,
|
|
80
|
+
matches = true,
|
|
81
|
+
}: StubMatchMediaOptions = {}): void {
|
|
82
|
+
vi.stubGlobal(
|
|
83
|
+
'matchMedia',
|
|
84
|
+
vi.fn((query: string) => ({
|
|
85
|
+
matches: answerReducedMotion(query, reduceMotion) ?? matches,
|
|
86
|
+
media: query,
|
|
87
|
+
onchange: null,
|
|
88
|
+
addEventListener: vi.fn(),
|
|
89
|
+
removeEventListener: vi.fn(),
|
|
90
|
+
addListener: vi.fn(),
|
|
91
|
+
removeListener: vi.fn(),
|
|
92
|
+
dispatchEvent: vi.fn(),
|
|
93
|
+
})),
|
|
94
|
+
);
|
|
95
|
+
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -11,13 +11,17 @@
|
|
|
11
11
|
* SYNC: When modified, update this header
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
export {
|
|
15
|
-
hasActiveFocusTrapEscape,
|
|
16
|
-
isImeKeyEvent,
|
|
17
|
-
useFocusTrap,
|
|
18
|
-
} from './useFocusTrap';
|
|
14
|
+
export {hasActiveFocusTrapEscape, useFocusTrap} from './useFocusTrap';
|
|
19
15
|
export type {UseFocusTrapOptions, UseFocusTrapReturn} from './useFocusTrap';
|
|
20
16
|
|
|
17
|
+
/**
|
|
18
|
+
* @deprecated Import from `@astryxdesign/core/utils` instead — this is a pure
|
|
19
|
+
* predicate, not a hook, and the `hooks` barrel is a `'use client'` boundary.
|
|
20
|
+
* Re-exported here for one release so consumers can move; will be removed in
|
|
21
|
+
* an upcoming major.
|
|
22
|
+
*/
|
|
23
|
+
export {isImeKeyEvent} from '../utils/ime';
|
|
24
|
+
|
|
21
25
|
export {useAnnounce} from './useAnnounce';
|
|
22
26
|
export type {AnnounceFn, AnnouncePoliteness} from './useAnnounce';
|
|
23
27
|
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import {useCallback, useEffect, useRef} from 'react';
|
|
20
20
|
|
|
21
21
|
import {FOCUSABLE_SELECTOR} from './focusableSelector';
|
|
22
|
+
import {isImeKeyEvent} from '../utils/ime';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Module-level stack of active focus-trap Escape handlers.
|
|
@@ -94,20 +95,6 @@ export function hasActiveFocusTrapEscape(): boolean {
|
|
|
94
95
|
return escapeStack.length > 0;
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
/**
|
|
98
|
-
* Whether an Escape keydown should be ignored because it is cancelling an
|
|
99
|
-
* in-progress IME composition. CJK/IME users press Escape to cancel
|
|
100
|
-
* composition; that must not close the surrounding overlay. `keyCode === 229`
|
|
101
|
-
* covers browsers that fire keydown before `isComposing` is set. Exported so
|
|
102
|
-
* other overlays (Dialog, Drawer, CommandPalette) share one definition.
|
|
103
|
-
*/
|
|
104
|
-
export function isImeKeyEvent(event: {
|
|
105
|
-
isComposing?: boolean;
|
|
106
|
-
keyCode?: number;
|
|
107
|
-
}): boolean {
|
|
108
|
-
return event.isComposing === true || event.keyCode === 229;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
98
|
/**
|
|
112
99
|
* Whether an element is currently perceivable/focusable — excludes ones hidden
|
|
113
100
|
* via `display:none`/`visibility:hidden` or inside an `inert`/`hidden` subtree,
|
|
@@ -159,6 +159,25 @@ describe('useInputStatusIcon — no dangling aria-describedby (WCAG 1.3.1)', ()
|
|
|
159
159
|
).not.toBeInTheDocument();
|
|
160
160
|
expectNoDanglingDescribedBy(container);
|
|
161
161
|
});
|
|
162
|
+
|
|
163
|
+
it('keeps the tooltip status button interactive inside a non-interactive trailing slot', () => {
|
|
164
|
+
// TextArea positions its trailing slot as an absolute overlay with
|
|
165
|
+
// `pointer-events: none` (correct for the decorative spinner/icon it was
|
|
166
|
+
// built for). The focusable status button owns its own interactivity so
|
|
167
|
+
// hover still opens the tooltip there — keyboard focus always worked, the
|
|
168
|
+
// pointer path did not until the button set `pointer-events: auto`.
|
|
169
|
+
render(
|
|
170
|
+
<TextArea
|
|
171
|
+
label="Field"
|
|
172
|
+
value=""
|
|
173
|
+
onChange={() => {}}
|
|
174
|
+
status={{type: 'error', message: 'Message'}}
|
|
175
|
+
statusVariant="tooltip"
|
|
176
|
+
/>,
|
|
177
|
+
);
|
|
178
|
+
const statusButton = screen.getByRole('button', {name: 'Error details'});
|
|
179
|
+
expect(statusButton).toHaveStyle({pointerEvents: 'auto'});
|
|
180
|
+
});
|
|
162
181
|
});
|
|
163
182
|
|
|
164
183
|
describe('useInputStatusIcon — input-status-icon theme target', () => {
|
|
@@ -82,6 +82,13 @@ const styles = stylex.create({
|
|
|
82
82
|
background: 'none',
|
|
83
83
|
color: 'inherit',
|
|
84
84
|
cursor: 'pointer',
|
|
85
|
+
// Own the interactivity so the tooltip opens on hover even when the field
|
|
86
|
+
// renders this affordance inside a non-interactive trailing slot. TextArea
|
|
87
|
+
// positions its end slot as an absolute overlay with `pointer-events: none`
|
|
88
|
+
// (right for the decorative spinner/icon it was built for), which otherwise
|
|
89
|
+
// swallows this button's hover — keyboard focus still worked, pointer did
|
|
90
|
+
// not.
|
|
91
|
+
pointerEvents: 'auto',
|
|
85
92
|
borderRadius: radiusVars['--radius-full'],
|
|
86
93
|
// Not the shared offset: this button sits inside the field, and measured at
|
|
87
94
|
// the standard 3px its ring crosses the field border.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file useResolvedRequired.ts
|
|
7
|
+
* @input Uses React, FormLayoutContext
|
|
8
|
+
* @output Exports useResolvedRequired
|
|
9
|
+
* @position Resolves a field's effective aria-required against the form default
|
|
10
|
+
*
|
|
11
|
+
* SYNC: When modified, update these files to stay in sync:
|
|
12
|
+
* - /packages/core/src/FormLayout/FormLayoutContext.ts (defaultOptionality source)
|
|
13
|
+
* - /packages/core/src/FormLayout/FormLayout.tsx (prop doc: aria-required behavior)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {use} from 'react';
|
|
17
|
+
import {FormLayoutContext} from '../FormLayout/FormLayoutContext';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a field's effective *required* state for `aria-required`, honoring a
|
|
21
|
+
* surrounding `FormLayout`'s `defaultOptionality`.
|
|
22
|
+
*
|
|
23
|
+
* Under `defaultOptionality="required"` a field is required unless it opts out
|
|
24
|
+
* with `isOptional`. The visible indicator is suppressed for the unmarked
|
|
25
|
+
* majority, so those fields must still expose `aria-required` — otherwise a
|
|
26
|
+
* sighted user reads them as required (form-wide default, no indicator) while a
|
|
27
|
+
* screen reader hears "not required". This closes that mismatch.
|
|
28
|
+
*
|
|
29
|
+
* Semantics only: this drives `aria-required`, never the native `required`
|
|
30
|
+
* attribute, so a layout-level default can't silently switch on browser
|
|
31
|
+
* validation bubbles. `isOptional` takes precedence, matching `FieldLabel`.
|
|
32
|
+
*/
|
|
33
|
+
export function useResolvedRequired({
|
|
34
|
+
isRequired = false,
|
|
35
|
+
isOptional = false,
|
|
36
|
+
}: {
|
|
37
|
+
isRequired?: boolean;
|
|
38
|
+
isOptional?: boolean;
|
|
39
|
+
}): boolean {
|
|
40
|
+
const {defaultOptionality} = use(FormLayoutContext);
|
|
41
|
+
return !isOptional && (isRequired || defaultOptionality === 'required');
|
|
42
|
+
}
|
package/src/index.ts
CHANGED
package/src/theme/defineTheme.ts
CHANGED
|
@@ -248,20 +248,38 @@ export interface DefineThemeInput {
|
|
|
248
248
|
*/
|
|
249
249
|
radius?: RadiusScaleConfig;
|
|
250
250
|
/**
|
|
251
|
-
* Color scale configuration. Generates color token overrides from
|
|
252
|
-
*
|
|
251
|
+
* Color scale configuration. Generates color token overrides from an
|
|
252
|
+
* accent seed using the HCT perceptual color model.
|
|
253
253
|
*
|
|
254
254
|
* Only generates tokens derivable from the accent — status colors,
|
|
255
255
|
* categorical hues, and fixed tokens (on-dark/on-light) use defaults.
|
|
256
|
-
*
|
|
256
|
+
*
|
|
257
|
+
* `accent` accepts a single hex (same seed for both color schemes) or a
|
|
258
|
+
* `[light, dark]` tuple, matching `TokenValue`. With a tuple, the light
|
|
259
|
+
* scheme's full palette derives from the light seed and the dark
|
|
260
|
+
* scheme's from the dark seed.
|
|
257
261
|
*
|
|
258
262
|
* `accent` is optional — omit it for a neutral-only theme, which keeps
|
|
259
263
|
* the default accent tokens and only themes the neutrals.
|
|
260
264
|
*
|
|
265
|
+
* Precedence vs `tokens`: explicit `tokens` entries win over generated
|
|
266
|
+
* values, token by token. Because `--color-accent-muted`,
|
|
267
|
+
* `--color-text-accent` and `--color-icon-accent` are generated as
|
|
268
|
+
* `var(--color-accent)` references, a `tokens['--color-accent']`
|
|
269
|
+
* override re-points them at runtime. `--color-on-accent` does NOT
|
|
270
|
+
* follow: it is baked from the `color.accent` seed (a contrast
|
|
271
|
+
* computation CSS cannot express), so overriding the accent through
|
|
272
|
+
* `tokens` without also overriding `--color-on-accent` leaves the two
|
|
273
|
+
* out of sync. To re-seat the whole palette per scheme, prefer a tuple
|
|
274
|
+
* `color.accent` over the `tokens['--color-accent']` workaround.
|
|
275
|
+
*
|
|
261
276
|
* @example
|
|
262
|
-
* ```
|
|
277
|
+
* ```
|
|
263
278
|
* color: { accent: '#0064E0', neutralStyle: 'cool', contrast: 'standard' }
|
|
264
279
|
*
|
|
280
|
+
* // Per-scheme accents — light palette from the first seed, dark from the second
|
|
281
|
+
* color: { accent: ['#0064E0', '#48CAE4'] }
|
|
282
|
+
*
|
|
265
283
|
* // Neutral-only — accent tokens stay at their defaults
|
|
266
284
|
* color: { neutralStyle: 'warm' }
|
|
267
285
|
* ```
|
|
@@ -192,6 +192,144 @@ describe('expandColorScale — neutral-only themes (#2279)', () => {
|
|
|
192
192
|
});
|
|
193
193
|
});
|
|
194
194
|
|
|
195
|
+
describe('expandColorScale — tuple accent (#2279)', () => {
|
|
196
|
+
const LIGHT_SEED = '#B7410E';
|
|
197
|
+
const DARK_SEED = '#48CAE4';
|
|
198
|
+
|
|
199
|
+
it('accepts a [light, dark] tuple and generates the accent tokens', () => {
|
|
200
|
+
const tokens = expandColorScale({accent: [LIGHT_SEED, DARK_SEED]});
|
|
201
|
+
for (const key of ACCENT_TOKENS) {
|
|
202
|
+
expect(tokens).toHaveProperty(key);
|
|
203
|
+
}
|
|
204
|
+
expect(tokens['--color-accent']).toMatch(/^light-dark\(#/);
|
|
205
|
+
expect(tokens['--color-on-accent']).toMatch(/^light-dark\(#/);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('a same-halves tuple matches the single-string expansion, token for token', () => {
|
|
209
|
+
// Pins the string path as the tuple fast path: introducing tuples must
|
|
210
|
+
// not re-color any existing single-accent theme.
|
|
211
|
+
const fromString = expandColorScale({accent: LIGHT_SEED});
|
|
212
|
+
const fromTuple = expandColorScale({accent: [LIGHT_SEED, LIGHT_SEED]});
|
|
213
|
+
expect(fromTuple).toEqual(fromString);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('derives the light palette from the light seed and the dark palette from the dark seed', () => {
|
|
217
|
+
// Every generated light-dark() pair takes its light half from the light
|
|
218
|
+
// seed's palettes and its dark half from the dark seed's, so resolving
|
|
219
|
+
// each mode must agree with the matching single-accent expansion.
|
|
220
|
+
const tuple = defineTheme({
|
|
221
|
+
name: 'tuple',
|
|
222
|
+
color: {accent: [LIGHT_SEED, DARK_SEED]},
|
|
223
|
+
});
|
|
224
|
+
const fromLight = defineTheme({name: 'light', color: {accent: LIGHT_SEED}});
|
|
225
|
+
const fromDark = defineTheme({name: 'dark', color: {accent: DARK_SEED}});
|
|
226
|
+
|
|
227
|
+
const generatedKeys = Object.keys(
|
|
228
|
+
expandColorScale({accent: [LIGHT_SEED, DARK_SEED]}),
|
|
229
|
+
);
|
|
230
|
+
const tupleLight = resolveThemeTokens(tuple, {mode: 'light'});
|
|
231
|
+
const refLight = resolveThemeTokens(fromLight, {mode: 'light'});
|
|
232
|
+
const tupleDark = resolveThemeTokens(tuple, {mode: 'dark'});
|
|
233
|
+
const refDark = resolveThemeTokens(fromDark, {mode: 'dark'});
|
|
234
|
+
for (const key of generatedKeys) {
|
|
235
|
+
expect(tupleLight[key], `${key} (light)`).toBe(refLight[key]);
|
|
236
|
+
expect(tupleDark[key], `${key} (dark)`).toBe(refDark[key]);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('differing seeds produce a dark half the light seed would not', () => {
|
|
241
|
+
// Regression guard: the dark half must not silently reuse the light seed.
|
|
242
|
+
const tuple = defineTheme({
|
|
243
|
+
name: 'tuple-differs',
|
|
244
|
+
color: {accent: [LIGHT_SEED, DARK_SEED]},
|
|
245
|
+
});
|
|
246
|
+
const single = defineTheme({
|
|
247
|
+
name: 'single',
|
|
248
|
+
color: {accent: LIGHT_SEED},
|
|
249
|
+
});
|
|
250
|
+
expect(
|
|
251
|
+
resolveThemeTokens(tuple, {mode: 'dark'})['--color-accent'],
|
|
252
|
+
).not.toBe(resolveThemeTokens(single, {mode: 'dark'})['--color-accent']);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('keeps the derived accent tokens as references with a tuple accent', () => {
|
|
256
|
+
const tokens = expandColorScale({accent: [LIGHT_SEED, DARK_SEED]});
|
|
257
|
+
expect(tokens['--color-text-accent']).toBe('var(--color-accent)');
|
|
258
|
+
expect(tokens['--color-icon-accent']).toBe('var(--color-accent)');
|
|
259
|
+
expect(tokens['--color-accent-muted']).toContain('var(--color-accent)');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('honours neutralStyle with a tuple accent', () => {
|
|
263
|
+
const warm = expandColorScale({
|
|
264
|
+
accent: [LIGHT_SEED, DARK_SEED],
|
|
265
|
+
neutralStyle: 'warm',
|
|
266
|
+
});
|
|
267
|
+
const cool = expandColorScale({
|
|
268
|
+
accent: [LIGHT_SEED, DARK_SEED],
|
|
269
|
+
neutralStyle: 'cool',
|
|
270
|
+
});
|
|
271
|
+
expect(warm['--color-neutral']).not.toBe(cool['--color-neutral']);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('accent precedence — color config vs tokens overrides (#2279)', () => {
|
|
276
|
+
const CONFIG_ACCENT: [string, string] = ['#B7410E', '#48CAE4'];
|
|
277
|
+
const TOKEN_ACCENT: [string, string] = ['#AA0000', '#FF5555'];
|
|
278
|
+
|
|
279
|
+
it('tokens["--color-accent"] wins over the color-generated accent', () => {
|
|
280
|
+
const theme = defineTheme({
|
|
281
|
+
name: 'precedence-base',
|
|
282
|
+
color: {accent: CONFIG_ACCENT},
|
|
283
|
+
tokens: {'--color-accent': TOKEN_ACCENT},
|
|
284
|
+
});
|
|
285
|
+
expect(theme.tokens['--color-accent']).toBe(
|
|
286
|
+
`light-dark(${TOKEN_ACCENT[0]}, ${TOKEN_ACCENT[1]})`,
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('reference tokens follow the tokens override at runtime', () => {
|
|
291
|
+
const theme = defineTheme({
|
|
292
|
+
name: 'precedence-refs',
|
|
293
|
+
color: {accent: CONFIG_ACCENT},
|
|
294
|
+
tokens: {'--color-accent': TOKEN_ACCENT},
|
|
295
|
+
});
|
|
296
|
+
const light = resolveThemeTokens(theme, {mode: 'light'});
|
|
297
|
+
expect(light['--color-text-accent']).toBe(TOKEN_ACCENT[0]);
|
|
298
|
+
expect(light['--color-icon-accent']).toBe(TOKEN_ACCENT[0]);
|
|
299
|
+
const dark = resolveThemeTokens(theme, {mode: 'dark'});
|
|
300
|
+
expect(dark['--color-text-accent']).toBe(TOKEN_ACCENT[1]);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('baked --color-on-accent stays derived from the color.accent seed', () => {
|
|
304
|
+
// Documented divergence: a tokens override of --color-accent does not
|
|
305
|
+
// re-derive --color-on-accent, which is a baked contrast computation
|
|
306
|
+
// against the color.accent seed. Consumers who override the accent via
|
|
307
|
+
// tokens must override --color-on-accent too (or pass the tuple through
|
|
308
|
+
// color.accent so the whole palette re-derives).
|
|
309
|
+
const overridden = defineTheme({
|
|
310
|
+
name: 'precedence-on-accent',
|
|
311
|
+
color: {accent: CONFIG_ACCENT},
|
|
312
|
+
tokens: {'--color-accent': TOKEN_ACCENT},
|
|
313
|
+
});
|
|
314
|
+
const generated = defineTheme({
|
|
315
|
+
name: 'generated-on-accent',
|
|
316
|
+
color: {accent: CONFIG_ACCENT},
|
|
317
|
+
});
|
|
318
|
+
expect(overridden.tokens['--color-on-accent']).toBe(
|
|
319
|
+
generated.tokens['--color-on-accent'],
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('an explicit --color-on-accent token wins over the generated one', () => {
|
|
324
|
+
const theme = defineTheme({
|
|
325
|
+
name: 'precedence-on-accent-explicit',
|
|
326
|
+
color: {accent: CONFIG_ACCENT},
|
|
327
|
+
tokens: {'--color-on-accent': '#FFFFFF'},
|
|
328
|
+
});
|
|
329
|
+
expect(theme.tokens['--color-on-accent']).toBe('#FFFFFF');
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
195
333
|
describe('expandColorScale + defineTheme integration', () => {
|
|
196
334
|
it('explicit token overrides win over generated values', () => {
|
|
197
335
|
const theme = defineTheme({
|