@oxyhq/bloom 0.87.1 → 0.89.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/lib/commonjs/accordion/Accordion.js +7 -5
- package/lib/commonjs/accordion/Accordion.js.map +1 -1
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js +7 -12
- package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/commonjs/button/Button.js +8 -7
- package/lib/commonjs/button/Button.js.map +1 -1
- package/lib/commonjs/button/Button.web.js +2 -6
- package/lib/commonjs/button/Button.web.js.map +1 -1
- package/lib/commonjs/checkbox/Checkbox.js +10 -5
- package/lib/commonjs/checkbox/Checkbox.js.map +1 -1
- package/lib/commonjs/chip/Chip.js +10 -1
- package/lib/commonjs/chip/Chip.js.map +1 -1
- package/lib/commonjs/collapsible/Collapsible.js +5 -4
- package/lib/commonjs/collapsible/Collapsible.js.map +1 -1
- package/lib/commonjs/composition-bar/CompositionBar.js +6 -1
- package/lib/commonjs/composition-bar/CompositionBar.js.map +1 -1
- package/lib/commonjs/connection-dots/ConnectionDots.web.js +2 -6
- package/lib/commonjs/connection-dots/ConnectionDots.web.js.map +1 -1
- package/lib/commonjs/design-tokens/index.js +19 -0
- package/lib/commonjs/design-tokens/index.js.map +1 -1
- package/lib/commonjs/design-tokens/tokens-json.js +149 -0
- package/lib/commonjs/design-tokens/tokens-json.js.map +1 -0
- package/lib/commonjs/design-tokens/tokens.json +6681 -0
- package/lib/commonjs/dialog/Dialog.web.js +2 -6
- package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
- package/lib/commonjs/fab/Fab.web.js +2 -6
- package/lib/commonjs/fab/Fab.web.js.map +1 -1
- package/lib/commonjs/fonts/apply-font-faces.web.js +8 -10
- package/lib/commonjs/fonts/apply-font-faces.web.js.map +1 -1
- package/lib/commonjs/frosted-icon-button/FrostedIconButton.js +7 -1
- package/lib/commonjs/frosted-icon-button/FrostedIconButton.js.map +1 -1
- package/lib/commonjs/frosted-icon-button/FrostedIconButton.web.js +2 -6
- package/lib/commonjs/frosted-icon-button/FrostedIconButton.web.js.map +1 -1
- package/lib/commonjs/input-group/InputGroup.js +6 -4
- package/lib/commonjs/input-group/InputGroup.js.map +1 -1
- package/lib/commonjs/item/Item.js +23 -5
- package/lib/commonjs/item/Item.js.map +1 -1
- package/lib/commonjs/loading/SpinnerIcon.web.js +2 -6
- package/lib/commonjs/loading/SpinnerIcon.web.js.map +1 -1
- package/lib/commonjs/segmented-control/index.js +12 -5
- package/lib/commonjs/segmented-control/index.js.map +1 -1
- package/lib/commonjs/select/index.js +6 -4
- package/lib/commonjs/select/index.js.map +1 -1
- package/lib/commonjs/select/index.web.js +6 -4
- package/lib/commonjs/select/index.web.js.map +1 -1
- package/lib/commonjs/settings-list/SettingsList.js +6 -4
- package/lib/commonjs/settings-list/SettingsList.js.map +1 -1
- package/lib/commonjs/slider/Slider.js +12 -9
- package/lib/commonjs/slider/Slider.js.map +1 -1
- package/lib/commonjs/styles/adopt-style-sheet.js +126 -0
- package/lib/commonjs/styles/adopt-style-sheet.js.map +1 -0
- package/lib/commonjs/switch/Switch.js +9 -5
- package/lib/commonjs/switch/Switch.js.map +1 -1
- package/lib/commonjs/tab-bar/TabBarBase.js +6 -4
- package/lib/commonjs/tab-bar/TabBarBase.js.map +1 -1
- package/lib/module/accordion/Accordion.js +7 -5
- package/lib/module/accordion/Accordion.js.map +1 -1
- package/lib/module/bottom-sheet/BottomSheetBase.js +7 -12
- package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
- package/lib/module/button/Button.js +8 -7
- package/lib/module/button/Button.js.map +1 -1
- package/lib/module/button/Button.web.js +2 -6
- package/lib/module/button/Button.web.js.map +1 -1
- package/lib/module/checkbox/Checkbox.js +10 -5
- package/lib/module/checkbox/Checkbox.js.map +1 -1
- package/lib/module/chip/Chip.js +10 -1
- package/lib/module/chip/Chip.js.map +1 -1
- package/lib/module/collapsible/Collapsible.js +5 -4
- package/lib/module/collapsible/Collapsible.js.map +1 -1
- package/lib/module/composition-bar/CompositionBar.js +6 -1
- package/lib/module/composition-bar/CompositionBar.js.map +1 -1
- package/lib/module/connection-dots/ConnectionDots.web.js +2 -6
- package/lib/module/connection-dots/ConnectionDots.web.js.map +1 -1
- package/lib/module/design-tokens/index.js +5 -0
- package/lib/module/design-tokens/index.js.map +1 -1
- package/lib/module/design-tokens/tokens-json.js +144 -0
- package/lib/module/design-tokens/tokens-json.js.map +1 -0
- package/lib/module/design-tokens/tokens.json +6681 -0
- package/lib/module/dialog/Dialog.web.js +2 -6
- package/lib/module/dialog/Dialog.web.js.map +1 -1
- package/lib/module/fab/Fab.web.js +2 -6
- package/lib/module/fab/Fab.web.js.map +1 -1
- package/lib/module/fonts/apply-font-faces.web.js +8 -10
- package/lib/module/fonts/apply-font-faces.web.js.map +1 -1
- package/lib/module/frosted-icon-button/FrostedIconButton.js +7 -1
- package/lib/module/frosted-icon-button/FrostedIconButton.js.map +1 -1
- package/lib/module/frosted-icon-button/FrostedIconButton.web.js +2 -6
- package/lib/module/frosted-icon-button/FrostedIconButton.web.js.map +1 -1
- package/lib/module/input-group/InputGroup.js +6 -4
- package/lib/module/input-group/InputGroup.js.map +1 -1
- package/lib/module/item/Item.js +23 -5
- package/lib/module/item/Item.js.map +1 -1
- package/lib/module/loading/SpinnerIcon.web.js +2 -6
- package/lib/module/loading/SpinnerIcon.web.js.map +1 -1
- package/lib/module/segmented-control/index.js +12 -5
- package/lib/module/segmented-control/index.js.map +1 -1
- package/lib/module/select/index.js +6 -4
- package/lib/module/select/index.js.map +1 -1
- package/lib/module/select/index.web.js +6 -4
- package/lib/module/select/index.web.js.map +1 -1
- package/lib/module/settings-list/SettingsList.js +6 -4
- package/lib/module/settings-list/SettingsList.js.map +1 -1
- package/lib/module/slider/Slider.js +12 -9
- package/lib/module/slider/Slider.js.map +1 -1
- package/lib/module/styles/adopt-style-sheet.js +121 -0
- package/lib/module/styles/adopt-style-sheet.js.map +1 -0
- package/lib/module/switch/Switch.js +9 -5
- package/lib/module/switch/Switch.js.map +1 -1
- package/lib/module/tab-bar/TabBarBase.js +6 -4
- package/lib/module/tab-bar/TabBarBase.js.map +1 -1
- package/lib/typescript/commonjs/accordion/Accordion.d.ts.map +1 -1
- package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/button/Button.d.ts.map +1 -1
- package/lib/typescript/commonjs/button/Button.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/checkbox/Checkbox.d.ts.map +1 -1
- package/lib/typescript/commonjs/chip/Chip.d.ts.map +1 -1
- package/lib/typescript/commonjs/collapsible/Collapsible.d.ts.map +1 -1
- package/lib/typescript/commonjs/composition-bar/CompositionBar.d.ts.map +1 -1
- package/lib/typescript/commonjs/connection-dots/ConnectionDots.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/design-tokens/index.d.ts +2 -0
- package/lib/typescript/commonjs/design-tokens/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts +85 -0
- package/lib/typescript/commonjs/design-tokens/tokens-json.d.ts.map +1 -0
- package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/fab/Fab.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/fonts/apply-font-faces.web.d.ts +5 -3
- package/lib/typescript/commonjs/fonts/apply-font-faces.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/frosted-icon-button/FrostedIconButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/frosted-icon-button/FrostedIconButton.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/input-group/InputGroup.d.ts.map +1 -1
- package/lib/typescript/commonjs/item/Item.d.ts.map +1 -1
- package/lib/typescript/commonjs/loading/SpinnerIcon.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/segmented-control/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/select/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/select/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/settings-list/SettingsList.d.ts.map +1 -1
- package/lib/typescript/commonjs/slider/Slider.d.ts.map +1 -1
- package/lib/typescript/commonjs/styles/adopt-style-sheet.d.ts +22 -0
- package/lib/typescript/commonjs/styles/adopt-style-sheet.d.ts.map +1 -0
- package/lib/typescript/commonjs/switch/Switch.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/TabBarBase.d.ts.map +1 -1
- package/lib/typescript/module/accordion/Accordion.d.ts.map +1 -1
- package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
- package/lib/typescript/module/button/Button.d.ts.map +1 -1
- package/lib/typescript/module/button/Button.web.d.ts.map +1 -1
- package/lib/typescript/module/checkbox/Checkbox.d.ts.map +1 -1
- package/lib/typescript/module/chip/Chip.d.ts.map +1 -1
- package/lib/typescript/module/collapsible/Collapsible.d.ts.map +1 -1
- package/lib/typescript/module/composition-bar/CompositionBar.d.ts.map +1 -1
- package/lib/typescript/module/connection-dots/ConnectionDots.web.d.ts.map +1 -1
- package/lib/typescript/module/design-tokens/index.d.ts +2 -0
- package/lib/typescript/module/design-tokens/index.d.ts.map +1 -1
- package/lib/typescript/module/design-tokens/tokens-json.d.ts +85 -0
- package/lib/typescript/module/design-tokens/tokens-json.d.ts.map +1 -0
- package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
- package/lib/typescript/module/fab/Fab.web.d.ts.map +1 -1
- package/lib/typescript/module/fonts/apply-font-faces.web.d.ts +5 -3
- package/lib/typescript/module/fonts/apply-font-faces.web.d.ts.map +1 -1
- package/lib/typescript/module/frosted-icon-button/FrostedIconButton.d.ts.map +1 -1
- package/lib/typescript/module/frosted-icon-button/FrostedIconButton.web.d.ts.map +1 -1
- package/lib/typescript/module/input-group/InputGroup.d.ts.map +1 -1
- package/lib/typescript/module/item/Item.d.ts.map +1 -1
- package/lib/typescript/module/loading/SpinnerIcon.web.d.ts.map +1 -1
- package/lib/typescript/module/segmented-control/index.d.ts.map +1 -1
- package/lib/typescript/module/select/index.d.ts.map +1 -1
- package/lib/typescript/module/select/index.web.d.ts.map +1 -1
- package/lib/typescript/module/settings-list/SettingsList.d.ts.map +1 -1
- package/lib/typescript/module/slider/Slider.d.ts.map +1 -1
- package/lib/typescript/module/styles/adopt-style-sheet.d.ts +22 -0
- package/lib/typescript/module/styles/adopt-style-sheet.d.ts.map +1 -0
- package/lib/typescript/module/switch/Switch.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/TabBarBase.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/__tests__/Button.test.tsx +11 -3
- package/src/__tests__/InputGroup.test.tsx +4 -1
- package/src/__tests__/Item.test.tsx +5 -1
- package/src/__tests__/Slider.test.tsx +11 -2
- package/src/__tests__/TabBar.test.tsx +9 -1
- package/src/__tests__/TabBarNoSelection.test.tsx +3 -1
- package/src/__tests__/adopt-style-sheet.node.test.ts +21 -0
- package/src/__tests__/adopt-style-sheet.test.ts +165 -0
- package/src/__tests__/apply-font-faces.test.ts +32 -0
- package/src/__tests__/aria-state-web.test.tsx +466 -0
- package/src/__tests__/design-tokens-json.test.ts +112 -0
- package/src/__tests__/exports-map-contract.test.ts +5 -1
- package/src/__tests__/support/constructed-style-sheets.ts +68 -0
- package/src/accordion/Accordion.tsx +5 -1
- package/src/bottom-sheet/BottomSheetBase.tsx +10 -14
- package/src/button/Button.tsx +6 -1
- package/src/button/Button.web.tsx +2 -6
- package/src/checkbox/Checkbox.tsx +8 -1
- package/src/chip/Chip.tsx +8 -0
- package/src/collapsible/Collapsible.tsx +3 -1
- package/src/composition-bar/CompositionBar.tsx +4 -0
- package/src/connection-dots/ConnectionDots.web.tsx +2 -6
- package/src/design-tokens/index.ts +16 -0
- package/src/design-tokens/tokens-json.ts +214 -0
- package/src/design-tokens/tokens.json +6681 -0
- package/src/dialog/Dialog.web.tsx +2 -6
- package/src/fab/Fab.web.tsx +2 -6
- package/src/fonts/apply-font-faces.web.ts +11 -11
- package/src/frosted-icon-button/FrostedIconButton.tsx +5 -0
- package/src/frosted-icon-button/FrostedIconButton.web.tsx +2 -6
- package/src/input-group/InputGroup.tsx +4 -1
- package/src/item/Item.tsx +24 -2
- package/src/loading/SpinnerIcon.web.tsx +2 -6
- package/src/segmented-control/index.tsx +8 -1
- package/src/select/index.tsx +4 -1
- package/src/select/index.web.tsx +4 -1
- package/src/settings-list/SettingsList.tsx +4 -1
- package/src/slider/Slider.tsx +10 -2
- package/src/styles/adopt-style-sheet.ts +131 -0
- package/src/switch/Switch.tsx +7 -1
- package/src/tab-bar/TabBarBase.tsx +4 -1
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Every stateful Bloom control, checked against the DOM it actually produces.
|
|
6
|
+
//
|
|
7
|
+
// The bug this suite exists for: `Checkbox` announced no state on web. It set
|
|
8
|
+
// `accessibilityState={{checked}}` — the React Native answer, and the one every
|
|
9
|
+
// other control here used too — but react-native-web's `createDOMProps` never
|
|
10
|
+
// reads `accessibilityState`. It reads the `aria-*` props. So the box was drawn
|
|
11
|
+
// checked while assistive technology saw a `role="checkbox"` with no state at
|
|
12
|
+
// all, and the same was true of tab strips, radio options, accordions, sliders
|
|
13
|
+
// and toggles across the library.
|
|
14
|
+
//
|
|
15
|
+
// It survived because the existing suites assert on PROPS: `accessibilityState`
|
|
16
|
+
// was set, so they passed, and no prop-level assertion can tell you what
|
|
17
|
+
// react-native-web does with it afterwards. These tests render through the REAL
|
|
18
|
+
// react-native-web into jsdom and read the emitted ATTRIBUTES, which is the only
|
|
19
|
+
// place the difference is visible.
|
|
20
|
+
//
|
|
21
|
+
// The cross-platform contract, verified against both runtimes in node_modules:
|
|
22
|
+
// - react-native-web `createDOMProps` reads `aria-*`, ignores
|
|
23
|
+
// `accessibilityState` (it appears nowhere in that module).
|
|
24
|
+
// - React Native's `Pressable`/`View` fold `aria-busy|checked|disabled|
|
|
25
|
+
// expanded|selected` and `aria-value{min,max,now,text}` BACK into
|
|
26
|
+
// `accessibilityState`/`accessibilityValue`, with the `aria-*` value
|
|
27
|
+
// winning. So `aria-*` is the one spelling both platforms honour.
|
|
28
|
+
// - EXCEPT `aria-pressed`, which React Native has no concept of (its
|
|
29
|
+
// `AccessibilityState` is disabled/selected/checked/busy/expanded only).
|
|
30
|
+
// Toggles with `role="button"` therefore set BOTH: `accessibilityState`
|
|
31
|
+
// for native, `aria-pressed` for web. ARIA scopes these states by role —
|
|
32
|
+
// `aria-selected` would be invalid on a button, as `aria-pressed` would be
|
|
33
|
+
// on a tab — which is why the spelling differs per control rather than
|
|
34
|
+
// being one blanket substitution.
|
|
35
|
+
// - `aria-disabled` on a `Pressable` is a special case: react-native-web's
|
|
36
|
+
// `Pressable` appends its own from the `disabled` prop AFTER the caller's
|
|
37
|
+
// props, so a caller-supplied `aria-disabled` is overwritten there and the
|
|
38
|
+
// `disabled` PROP is the only spelling that works. On a plain `View` there
|
|
39
|
+
// is no such prop, so `aria-disabled` is the only one that works.
|
|
40
|
+
|
|
41
|
+
import React from 'react';
|
|
42
|
+
import { act } from 'react';
|
|
43
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
44
|
+
|
|
45
|
+
// The whole point is what react-native-web puts in the DOM, so this suite runs
|
|
46
|
+
// against the REAL RNW. Under the repo-wide `react-native` mock every assertion
|
|
47
|
+
// below would read a prop straight back off a stub element and pass whether or
|
|
48
|
+
// not the fix is present — the exact vacuity that let this ship.
|
|
49
|
+
jest.mock('react-native', () => jest.requireActual('react-native-web'));
|
|
50
|
+
|
|
51
|
+
import { Text } from 'react-native';
|
|
52
|
+
|
|
53
|
+
import { BloomThemeProvider } from '../theme/BloomThemeProvider';
|
|
54
|
+
import { Checkbox } from '../checkbox/Checkbox';
|
|
55
|
+
import { Switch } from '../switch/Switch';
|
|
56
|
+
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from '../accordion';
|
|
57
|
+
import { Collapsible } from '../collapsible';
|
|
58
|
+
import { Chip } from '../chip';
|
|
59
|
+
import { Item } from '../item';
|
|
60
|
+
import { SegmentedControl, SegmentedControlItem } from '../segmented-control';
|
|
61
|
+
import { Button } from '../button/Button';
|
|
62
|
+
import { Slider } from '../slider';
|
|
63
|
+
import { InputGroup } from '../input-group';
|
|
64
|
+
import { SettingsListItem } from '../settings-list/SettingsList';
|
|
65
|
+
import { FrostedIconButton } from '../frosted-icon-button';
|
|
66
|
+
import { CompositionBar } from '../composition-bar';
|
|
67
|
+
// Both Select forks by explicit filename: jest has no platform-extension
|
|
68
|
+
// resolution, so a bare `'../select'` would only ever exercise the native one
|
|
69
|
+
// and the web fork could drift back unnoticed.
|
|
70
|
+
import {
|
|
71
|
+
Select as WebSelect,
|
|
72
|
+
SelectItem as WebSelectItem,
|
|
73
|
+
SelectItemText as WebSelectItemText,
|
|
74
|
+
} from '../select/index.web';
|
|
75
|
+
import {
|
|
76
|
+
Select as NativeSelect,
|
|
77
|
+
SelectItem as NativeSelectItem,
|
|
78
|
+
SelectItemText as NativeSelectItemText,
|
|
79
|
+
} from '../select/index';
|
|
80
|
+
import { TabBar, TabBarButton } from '../tab-bar';
|
|
81
|
+
|
|
82
|
+
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
83
|
+
|
|
84
|
+
let container: HTMLDivElement;
|
|
85
|
+
let root: Root;
|
|
86
|
+
|
|
87
|
+
function mount(ui: React.ReactElement): HTMLElement {
|
|
88
|
+
act(() => {
|
|
89
|
+
root.render(
|
|
90
|
+
<BloomThemeProvider mode="light" colorPreset="teal">
|
|
91
|
+
{ui}
|
|
92
|
+
</BloomThemeProvider>,
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
return container;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
container = document.createElement('div');
|
|
100
|
+
document.body.appendChild(container);
|
|
101
|
+
root = createRoot(container);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
afterEach(() => {
|
|
105
|
+
act(() => root.unmount());
|
|
106
|
+
container.remove();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/** The DOM node react-native-web rendered for `testID`. */
|
|
110
|
+
function byTestId(c: HTMLElement, id: string): HTMLElement {
|
|
111
|
+
const el = c.querySelector(`[data-testid="${id}"]`);
|
|
112
|
+
if (!(el instanceof HTMLElement)) {
|
|
113
|
+
throw new Error(`No element rendered for testID "${id}"`);
|
|
114
|
+
}
|
|
115
|
+
return el;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The first node carrying `role`, which is what a screen reader would find. */
|
|
119
|
+
function byRole(c: HTMLElement, role: string): HTMLElement {
|
|
120
|
+
const el = c.querySelector(`[role="${role}"]`);
|
|
121
|
+
if (!(el instanceof HTMLElement)) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`No element with role="${role}" in DOM:\n${c.innerHTML.slice(0, 600)}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return el;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function allByRole(c: HTMLElement, role: string): HTMLElement[] {
|
|
130
|
+
return Array.from(c.querySelectorAll(`[role="${role}"]`)).filter(
|
|
131
|
+
(el): el is HTMLElement => el instanceof HTMLElement,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
describe('Checkbox (the reported defect)', () => {
|
|
138
|
+
it('emits aria-checked="true" when checked', () => {
|
|
139
|
+
const c = mount(<Checkbox checked onCheckedChange={() => {}} label="Sync" testID="cb" />);
|
|
140
|
+
const el = byTestId(c, 'cb');
|
|
141
|
+
expect(el.getAttribute('role')).toBe('checkbox');
|
|
142
|
+
expect(el.getAttribute('aria-checked')).toBe('true');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('emits aria-checked="false" when unchecked — absent is not the same as false', () => {
|
|
146
|
+
// An omitted attribute leaves the control stateless to assistive tech,
|
|
147
|
+
// which is what shipped. "false" is a positive statement of unchecked.
|
|
148
|
+
const c = mount(
|
|
149
|
+
<Checkbox checked={false} onCheckedChange={() => {}} label="Sync" testID="cb" />,
|
|
150
|
+
);
|
|
151
|
+
expect(byTestId(c, 'cb').getAttribute('aria-checked')).toBe('false');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('emits aria-checked="mixed" when indeterminate', () => {
|
|
155
|
+
const c = mount(
|
|
156
|
+
<Checkbox checked={false} indeterminate onCheckedChange={() => {}} label="S" testID="cb" />,
|
|
157
|
+
);
|
|
158
|
+
expect(byTestId(c, 'cb').getAttribute('aria-checked')).toBe('mixed');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('emits aria-disabled when disabled', () => {
|
|
162
|
+
const c = mount(
|
|
163
|
+
<Checkbox checked onCheckedChange={() => {}} label="Sync" disabled testID="cb" />,
|
|
164
|
+
);
|
|
165
|
+
expect(byTestId(c, 'cb').getAttribute('aria-disabled')).toBe('true');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('tracks a state change across a re-render', () => {
|
|
169
|
+
const c = mount(<Checkbox checked={false} onCheckedChange={() => {}} label="S" testID="cb" />);
|
|
170
|
+
expect(byTestId(c, 'cb').getAttribute('aria-checked')).toBe('false');
|
|
171
|
+
mount(<Checkbox checked onCheckedChange={() => {}} label="S" testID="cb" />);
|
|
172
|
+
expect(byTestId(c, 'cb').getAttribute('aria-checked')).toBe('true');
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('Switch', () => {
|
|
177
|
+
it('emits role="switch" with aria-checked', () => {
|
|
178
|
+
const c = mount(<Switch value onValueChange={() => {}} testID="sw" />);
|
|
179
|
+
const el = byTestId(c, 'sw');
|
|
180
|
+
expect(el.getAttribute('role')).toBe('switch');
|
|
181
|
+
expect(el.getAttribute('aria-checked')).toBe('true');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('emits aria-checked="false" when off', () => {
|
|
185
|
+
const c = mount(<Switch value={false} onValueChange={() => {}} testID="sw" />);
|
|
186
|
+
expect(byTestId(c, 'sw').getAttribute('aria-checked')).toBe('false');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('emits aria-disabled when disabled', () => {
|
|
190
|
+
// Regression: the switch guarded presses internally but never passed
|
|
191
|
+
// `disabled` to its Pressable, so web announced a disabled switch as
|
|
192
|
+
// operable. react-native-web overwrites a caller's `aria-disabled` on a
|
|
193
|
+
// Pressable, so the prop is the only route.
|
|
194
|
+
const c = mount(<Switch value onValueChange={() => {}} disabled testID="sw" />);
|
|
195
|
+
expect(byTestId(c, 'sw').getAttribute('aria-disabled')).toBe('true');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('Accordion / Collapsible', () => {
|
|
200
|
+
it('Accordion trigger emits aria-expanded reflecting the open item', () => {
|
|
201
|
+
const c = mount(
|
|
202
|
+
<Accordion type="single" value="a" onValueChange={() => {}}>
|
|
203
|
+
<AccordionItem value="a">
|
|
204
|
+
<AccordionTrigger>Open one</AccordionTrigger>
|
|
205
|
+
<AccordionContent>
|
|
206
|
+
<Text>body</Text>
|
|
207
|
+
</AccordionContent>
|
|
208
|
+
</AccordionItem>
|
|
209
|
+
<AccordionItem value="b">
|
|
210
|
+
<AccordionTrigger>Closed one</AccordionTrigger>
|
|
211
|
+
<AccordionContent>
|
|
212
|
+
<Text>body</Text>
|
|
213
|
+
</AccordionContent>
|
|
214
|
+
</AccordionItem>
|
|
215
|
+
</Accordion>,
|
|
216
|
+
);
|
|
217
|
+
const triggers = allByRole(c, 'button');
|
|
218
|
+
expect(triggers).toHaveLength(2);
|
|
219
|
+
expect(triggers[0]?.getAttribute('aria-expanded')).toBe('true');
|
|
220
|
+
expect(triggers[1]?.getAttribute('aria-expanded')).toBe('false');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('Collapsible header emits aria-expanded', () => {
|
|
224
|
+
const c = mount(
|
|
225
|
+
<Collapsible title="Details" defaultOpen>
|
|
226
|
+
<Text>body</Text>
|
|
227
|
+
</Collapsible>,
|
|
228
|
+
);
|
|
229
|
+
expect(byRole(c, 'button').getAttribute('aria-expanded')).toBe('true');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('Collapsible header emits aria-expanded="false" when closed', () => {
|
|
233
|
+
const c = mount(
|
|
234
|
+
<Collapsible title="Details">
|
|
235
|
+
<Text>body</Text>
|
|
236
|
+
</Collapsible>,
|
|
237
|
+
);
|
|
238
|
+
expect(byRole(c, 'button').getAttribute('aria-expanded')).toBe('false');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe('role="button" toggles use aria-pressed', () => {
|
|
243
|
+
// ARIA defines the toggle state of a button as `aria-pressed`; `aria-selected`
|
|
244
|
+
// is not valid on that role. React Native has no `aria-pressed`, so these
|
|
245
|
+
// components keep `accessibilityState={{selected}}` for native alongside it.
|
|
246
|
+
it('Chip emits aria-pressed when selected', () => {
|
|
247
|
+
const c = mount(
|
|
248
|
+
<Chip onPress={() => {}} selected testID="chip">
|
|
249
|
+
Filter
|
|
250
|
+
</Chip>,
|
|
251
|
+
);
|
|
252
|
+
expect(byTestId(c, 'chip').getAttribute('aria-pressed')).toBe('true');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('Chip emits aria-pressed="false" when unselected', () => {
|
|
256
|
+
const c = mount(
|
|
257
|
+
<Chip onPress={() => {}} selected={false} testID="chip">
|
|
258
|
+
Filter
|
|
259
|
+
</Chip>,
|
|
260
|
+
);
|
|
261
|
+
expect(byTestId(c, 'chip').getAttribute('aria-pressed')).toBe('false');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('FrostedIconButton emits aria-pressed, matching its .web fork', () => {
|
|
265
|
+
const c = mount(
|
|
266
|
+
<FrostedIconButton accessibilityLabel="Back" icon={<Text>x</Text>} active testID="fib" />,
|
|
267
|
+
);
|
|
268
|
+
expect(byTestId(c, 'fib').getAttribute('aria-pressed')).toBe('true');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('CompositionBar marks the selected segment with aria-pressed', () => {
|
|
272
|
+
const c = mount(
|
|
273
|
+
<CompositionBar
|
|
274
|
+
categories={[
|
|
275
|
+
{ key: 'a', name: 'Alpha', amount: 3, color: '#111111' },
|
|
276
|
+
{ key: 'b', name: 'Beta', amount: 1, color: '#222222' },
|
|
277
|
+
]}
|
|
278
|
+
selectedKey="b"
|
|
279
|
+
onSelect={() => {}}
|
|
280
|
+
hintLabel="Tap a segment"
|
|
281
|
+
/>,
|
|
282
|
+
);
|
|
283
|
+
const segments = allByRole(c, 'button');
|
|
284
|
+
expect(segments).toHaveLength(2);
|
|
285
|
+
expect(segments[0]?.getAttribute('aria-pressed')).toBe('false');
|
|
286
|
+
expect(segments[1]?.getAttribute('aria-pressed')).toBe('true');
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// `Item` puts `testID` on its inner content View, so these query the node that
|
|
291
|
+
// actually carries the role — which is the one a screen reader reads.
|
|
292
|
+
describe('Item selection is spelled per its role', () => {
|
|
293
|
+
it('a pressable row defaults to a button toggle: aria-pressed', () => {
|
|
294
|
+
const c = mount(<Item title="Row" onPress={() => {}} selected />);
|
|
295
|
+
expect(byRole(c, 'button').getAttribute('aria-pressed')).toBe('true');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('role="option" gets aria-selected, which is what ARIA allows there', () => {
|
|
299
|
+
const c = mount(<Item title="Row" role="option" onPress={() => {}} selected />);
|
|
300
|
+
const el = byRole(c, 'option');
|
|
301
|
+
expect(el.getAttribute('aria-selected')).toBe('true');
|
|
302
|
+
// `aria-pressed` is not valid on `option` and must not be emitted too.
|
|
303
|
+
expect(el.hasAttribute('aria-pressed')).toBe(false);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('a row with no selection state emits neither attribute', () => {
|
|
307
|
+
const c = mount(<Item title="Row" onPress={() => {}} />);
|
|
308
|
+
const el = byRole(c, 'button');
|
|
309
|
+
expect(el.hasAttribute('aria-pressed')).toBe(false);
|
|
310
|
+
expect(el.hasAttribute('aria-selected')).toBe(false);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('a non-pressable disabled row emits aria-disabled', () => {
|
|
314
|
+
// This branch renders a View, which has no `disabled` prop for
|
|
315
|
+
// react-native-web to derive the attribute from.
|
|
316
|
+
const c = mount(<Item title="Row" disabled />);
|
|
317
|
+
expect(c.querySelector('[aria-disabled="true"]')).not.toBeNull();
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('SegmentedControl spells the active state for its role', () => {
|
|
322
|
+
it('type="tabs" emits aria-selected on role="tab"', () => {
|
|
323
|
+
const c = mount(
|
|
324
|
+
<SegmentedControl label="View" type="tabs" value="one" onChange={() => {}}>
|
|
325
|
+
<SegmentedControlItem value="one" testID="seg-one">
|
|
326
|
+
<Text>One</Text>
|
|
327
|
+
</SegmentedControlItem>
|
|
328
|
+
<SegmentedControlItem value="two" testID="seg-two">
|
|
329
|
+
<Text>Two</Text>
|
|
330
|
+
</SegmentedControlItem>
|
|
331
|
+
</SegmentedControl>,
|
|
332
|
+
);
|
|
333
|
+
expect(byTestId(c, 'seg-one').getAttribute('role')).toBe('tab');
|
|
334
|
+
expect(byTestId(c, 'seg-one').getAttribute('aria-selected')).toBe('true');
|
|
335
|
+
expect(byTestId(c, 'seg-two').getAttribute('aria-selected')).toBe('false');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('type="radio" emits aria-checked on role="radio", not aria-selected', () => {
|
|
339
|
+
// ARIA gives `radio` a checked state; `aria-selected` is invalid there, so
|
|
340
|
+
// one blanket spelling for both modes would be wrong for this one.
|
|
341
|
+
const c = mount(
|
|
342
|
+
<SegmentedControl label="View" type="radio" value="two" onChange={() => {}}>
|
|
343
|
+
<SegmentedControlItem value="one" testID="seg-one">
|
|
344
|
+
<Text>One</Text>
|
|
345
|
+
</SegmentedControlItem>
|
|
346
|
+
<SegmentedControlItem value="two" testID="seg-two">
|
|
347
|
+
<Text>Two</Text>
|
|
348
|
+
</SegmentedControlItem>
|
|
349
|
+
</SegmentedControl>,
|
|
350
|
+
);
|
|
351
|
+
const two = byTestId(c, 'seg-two');
|
|
352
|
+
expect(two.getAttribute('role')).toBe('radio');
|
|
353
|
+
expect(two.getAttribute('aria-checked')).toBe('true');
|
|
354
|
+
expect(two.hasAttribute('aria-selected')).toBe(false);
|
|
355
|
+
expect(byTestId(c, 'seg-one').getAttribute('aria-checked')).toBe('false');
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('Button', () => {
|
|
360
|
+
it('emits aria-busy while loading, matching Button.web', () => {
|
|
361
|
+
const c = mount(
|
|
362
|
+
<Button loading testID="btn">
|
|
363
|
+
Saving
|
|
364
|
+
</Button>,
|
|
365
|
+
);
|
|
366
|
+
expect(byTestId(c, 'btn').getAttribute('aria-busy')).toBe('true');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('emits aria-disabled while loading, from the disabled prop', () => {
|
|
370
|
+
const c = mount(
|
|
371
|
+
<Button loading testID="btn">
|
|
372
|
+
Saving
|
|
373
|
+
</Button>,
|
|
374
|
+
);
|
|
375
|
+
expect(byTestId(c, 'btn').getAttribute('aria-disabled')).toBe('true');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('emits no aria-busy when idle', () => {
|
|
379
|
+
const c = mount(<Button testID="btn">Save</Button>);
|
|
380
|
+
expect(byTestId(c, 'btn').hasAttribute('aria-busy')).toBe(false);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe('Slider', () => {
|
|
385
|
+
it('emits the value as aria-value* — the object form reached web as nothing', () => {
|
|
386
|
+
// react-native-web has no handling for `accessibilityValue` at all, so the
|
|
387
|
+
// slider announced its role and no value whatsoever.
|
|
388
|
+
const c = mount(
|
|
389
|
+
<Slider value={40} min={0} max={100} onValueChange={() => {}} testID="sl" />,
|
|
390
|
+
);
|
|
391
|
+
const el = byTestId(c, 'sl');
|
|
392
|
+
expect(el.getAttribute('role')).toBe('slider');
|
|
393
|
+
expect(el.getAttribute('aria-valuenow')).toBe('40');
|
|
394
|
+
expect(el.getAttribute('aria-valuemin')).toBe('0');
|
|
395
|
+
expect(el.getAttribute('aria-valuemax')).toBe('100');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('emits aria-disabled when disabled', () => {
|
|
399
|
+
const c = mount(
|
|
400
|
+
<Slider value={0} min={0} max={10} onValueChange={() => {}} disabled testID="sl" />,
|
|
401
|
+
);
|
|
402
|
+
expect(byTestId(c, 'sl').getAttribute('aria-disabled')).toBe('true');
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// ARIA gives `role="radio"` a checked state, not a selected one. Both forks are
|
|
407
|
+
// asserted from one place so the platforms cannot drift apart again.
|
|
408
|
+
describe.each([
|
|
409
|
+
['web', WebSelect, WebSelectItem, WebSelectItemText] as const,
|
|
410
|
+
['native', NativeSelect, NativeSelectItem, NativeSelectItemText] as const,
|
|
411
|
+
])('SelectItem (%s fork) through react-native-web', (_p, Select, SelectItem, SelectItemText) => {
|
|
412
|
+
it('emits aria-checked reflecting the current value', () => {
|
|
413
|
+
const c = mount(
|
|
414
|
+
<Select value="a">
|
|
415
|
+
<SelectItem value="a" label="Option A">
|
|
416
|
+
<SelectItemText>Option A</SelectItemText>
|
|
417
|
+
</SelectItem>
|
|
418
|
+
<SelectItem value="b" label="Option B">
|
|
419
|
+
<SelectItemText>Option B</SelectItemText>
|
|
420
|
+
</SelectItem>
|
|
421
|
+
</Select>,
|
|
422
|
+
);
|
|
423
|
+
const options = allByRole(c, 'radio');
|
|
424
|
+
expect(options).toHaveLength(2);
|
|
425
|
+
expect(options[0]?.getAttribute('aria-checked')).toBe('true');
|
|
426
|
+
expect(options[1]?.getAttribute('aria-checked')).toBe('false');
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe('TabBar', () => {
|
|
431
|
+
it('emits aria-selected on the focused tab', () => {
|
|
432
|
+
const items = [
|
|
433
|
+
{ name: 'home', label: 'Home', icon: <Text>h</Text> },
|
|
434
|
+
{ name: 'search', label: 'Search', icon: <Text>s</Text> },
|
|
435
|
+
];
|
|
436
|
+
const c = mount(
|
|
437
|
+
<TabBar activeIndex={1}>
|
|
438
|
+
{items.map((item, index) => (
|
|
439
|
+
<TabBarButton key={item.name} item={item} index={index} />
|
|
440
|
+
))}
|
|
441
|
+
</TabBar>,
|
|
442
|
+
);
|
|
443
|
+
const tabs = allByRole(c, 'tab');
|
|
444
|
+
expect(tabs).toHaveLength(2);
|
|
445
|
+
expect(tabs[0]?.getAttribute('aria-selected')).toBe('false');
|
|
446
|
+
expect(tabs[1]?.getAttribute('aria-selected')).toBe('true');
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
describe('View-based containers emit aria-disabled', () => {
|
|
451
|
+
it('InputGroup', () => {
|
|
452
|
+
const c = mount(
|
|
453
|
+
<InputGroup disabled testID="ig">
|
|
454
|
+
<Text>field</Text>
|
|
455
|
+
</InputGroup>,
|
|
456
|
+
);
|
|
457
|
+
expect(byTestId(c, 'ig').getAttribute('aria-disabled')).toBe('true');
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('SettingsListItem without onPress', () => {
|
|
461
|
+
const c = mount(<SettingsListItem title="Wi-Fi" disabled />);
|
|
462
|
+
// The row renders a View wrapping the content; the disabled state belongs
|
|
463
|
+
// on whichever node carries the a11y role.
|
|
464
|
+
expect(c.querySelector('[aria-disabled="true"]')).not.toBeNull();
|
|
465
|
+
});
|
|
466
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
bloomDesignTokens,
|
|
6
|
+
renderBloomTokensJson,
|
|
7
|
+
resolvedColorToHex,
|
|
8
|
+
type PresetGroup,
|
|
9
|
+
} from '../design-tokens/tokens-json';
|
|
10
|
+
import { APP_COLOR_NAMES, APP_COLOR_PRESETS } from '../theme/color-presets';
|
|
11
|
+
import { CANONICAL_TOKENS, getResolvedTokens } from '../theme/token-registry';
|
|
12
|
+
|
|
13
|
+
const TOKENS_PATH = join(__dirname, '..', 'design-tokens', 'tokens.json');
|
|
14
|
+
|
|
15
|
+
describe('the shipped tokens.json is generated, not maintained', () => {
|
|
16
|
+
it('is byte-identical to a fresh render', () => {
|
|
17
|
+
// The whole guarantee: a token changed in the engine and not regenerated
|
|
18
|
+
// fails here rather than shipping a JSON that describes a palette Bloom no
|
|
19
|
+
// longer paints.
|
|
20
|
+
expect(readFileSync(TOKENS_PATH, 'utf8')).toBe(renderBloomTokensJson());
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('says so in its own $description', () => {
|
|
24
|
+
expect(bloomDesignTokens().$description).toContain('AUTO-GENERATED');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('tokens.json shape contract', () => {
|
|
29
|
+
/* Astro codegens C++ SkColor tables from these paths, so the shape is a
|
|
30
|
+
* published interface: new groups may be added, existing paths may not move. */
|
|
31
|
+
const tokens = bloomDesignTokens();
|
|
32
|
+
|
|
33
|
+
it('covers every preset Bloom ships, and nothing else', () => {
|
|
34
|
+
const presets = Object.keys(tokens.color).filter((key) => !key.startsWith('$'));
|
|
35
|
+
expect(presets.sort()).toEqual([...APP_COLOR_NAMES].sort());
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('carries both schemes and every canonical token, for every preset', () => {
|
|
39
|
+
for (const name of APP_COLOR_NAMES) {
|
|
40
|
+
const preset = tokens.color[name] as PresetGroup;
|
|
41
|
+
for (const scheme of ['light', 'dark'] as const) {
|
|
42
|
+
expect(Object.keys(preset[scheme]).sort()).toEqual([...CANONICAL_TOKENS].sort());
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('names tokens as the CSS custom property without the leading --', () => {
|
|
48
|
+
// What makes the mapping mechanical for a consumer. A token renamed on one
|
|
49
|
+
// side only would break it silently.
|
|
50
|
+
const preset = tokens.color.oxy as PresetGroup;
|
|
51
|
+
for (const token of Object.keys(preset.light)) {
|
|
52
|
+
expect(token.startsWith('--')).toBe(false);
|
|
53
|
+
expect(getResolvedTokens('oxy', 'light')[`--${token}`]).toBeDefined();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('resolves every colour to an sRGB hex string', () => {
|
|
58
|
+
for (const name of APP_COLOR_NAMES) {
|
|
59
|
+
const preset = tokens.color[name] as PresetGroup;
|
|
60
|
+
for (const scheme of ['light', 'dark'] as const) {
|
|
61
|
+
for (const [token, entry] of Object.entries(preset[scheme])) {
|
|
62
|
+
expect(`${name}.${scheme}.${token} = ${String(entry.$value)}`).toMatch(
|
|
63
|
+
/= #[0-9a-f]{6}([0-9a-f]{2})?$/,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('records each preset’s seed and variant', () => {
|
|
71
|
+
for (const name of APP_COLOR_NAMES) {
|
|
72
|
+
const preset = tokens.color[name] as PresetGroup;
|
|
73
|
+
expect(preset.$extensions['so.oxy.bloom']).toMatchObject({
|
|
74
|
+
seed: APP_COLOR_PRESETS[name].hex,
|
|
75
|
+
variant: APP_COLOR_PRESETS[name].variant,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('emits the numeric scales as px dimensions', () => {
|
|
81
|
+
expect(tokens.spacing['space-8']).toEqual({ $value: '8px' });
|
|
82
|
+
expect(tokens.radius['radius-max']).toEqual({ $value: '9999px' });
|
|
83
|
+
expect(tokens.borderWidth['hairline']).toEqual({ $value: '0.5px' });
|
|
84
|
+
expect(tokens.typography['body']).toMatchObject({
|
|
85
|
+
fontSize: { $value: '15px' },
|
|
86
|
+
fontWeight: { $value: 400 },
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('resolvedColorToHex', () => {
|
|
92
|
+
it('converts the engine’s two output forms', () => {
|
|
93
|
+
expect(resolvedColorToHex('rgb(255 239 253)')).toBe('#ffeffd');
|
|
94
|
+
expect(resolvedColorToHex('rgb(0 0 0)')).toBe('#000000');
|
|
95
|
+
// 0.13 × 255 = 33.15 → 33 → 0x21. An 8-bit alpha is exactly what an SkColor
|
|
96
|
+
// holds, so nothing is lost downstream.
|
|
97
|
+
expect(resolvedColorToHex('rgba(213, 86, 255, 0.13)')).toBe('#d556ff21');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('drops a fully opaque alpha rather than emitting #rrggbbff', () => {
|
|
101
|
+
expect(resolvedColorToHex('rgba(1, 2, 3, 1)')).toBe('#010203');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/* The engine emitting a third form — an oklch(), a bare hex, a named colour —
|
|
105
|
+
* would otherwise reach a C++ colour table as an unparseable string. Failing
|
|
106
|
+
* at generation time is the only place anyone would notice. */
|
|
107
|
+
it('throws on anything that is not an rgb()/rgba() colour', () => {
|
|
108
|
+
expect(() => resolvedColorToHex('oklch(0.7 0.1 300)')).toThrow(/not an rgb/);
|
|
109
|
+
expect(() => resolvedColorToHex('#ffeffd')).toThrow(/not an rgb/);
|
|
110
|
+
expect(() => resolvedColorToHex('0.5rem')).toThrow(/not an rgb/);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -37,7 +37,11 @@ const pkg = JSON.parse(readFileSync(PKG_PATH, 'utf8')) as {
|
|
|
37
37
|
const exportsMap = pkg.exports;
|
|
38
38
|
|
|
39
39
|
/** Non-subpath entries that are legitimately plain strings (static assets). */
|
|
40
|
-
const STRING_ENTRIES = [
|
|
40
|
+
const STRING_ENTRIES = [
|
|
41
|
+
'./design-tokens/theme.css',
|
|
42
|
+
'./design-tokens/tokens.json',
|
|
43
|
+
'./package.json',
|
|
44
|
+
];
|
|
41
45
|
|
|
42
46
|
/**
|
|
43
47
|
* Vacuity floor. `SUBPATHS` in the generator is 86 entries today; a traversal
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A spec-shaped stand-in for constructed stylesheets, for the jsdom suites.
|
|
3
|
+
*
|
|
4
|
+
* jsdom 26 ships the `CSSStyleSheet` CONSTRUCTOR but neither `replaceSync` nor
|
|
5
|
+
* `document.adoptedStyleSheets`, so every jsdom test takes Bloom's `<style>`
|
|
6
|
+
* fallback. That makes the whole CSP-safe path — the reason
|
|
7
|
+
* `styles/adopt-style-sheet.ts` exists — invisible to jest unless a test
|
|
8
|
+
* installs the API itself. Without this, a mutation that deleted the adoption
|
|
9
|
+
* branch entirely would leave the suite green.
|
|
10
|
+
*
|
|
11
|
+
* Not collected as a suite: jest's `testMatch` wants `*.test.ts` / `*.spec.ts`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface FakeStyleSheet {
|
|
15
|
+
/** The CSS most recently handed to `replaceSync`. */
|
|
16
|
+
cssText: string;
|
|
17
|
+
/** How many times the sheet has been re-parsed, so a test can prove it wasn't. */
|
|
18
|
+
replaceSyncCalls: number;
|
|
19
|
+
replaceSync(css: string): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ConstructedStyleSheetsHarness {
|
|
23
|
+
/** The document's adopted sheets, read live (the code reassigns the array). */
|
|
24
|
+
adopted(): readonly FakeStyleSheet[];
|
|
25
|
+
/** Drop the API again, restoring jsdom's own `CSSStyleSheet`. */
|
|
26
|
+
uninstall(): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function installConstructedStyleSheets(): ConstructedStyleSheetsHarness {
|
|
30
|
+
class FakeCSSStyleSheet implements FakeStyleSheet {
|
|
31
|
+
cssText = '';
|
|
32
|
+
replaceSyncCalls = 0;
|
|
33
|
+
|
|
34
|
+
replaceSync(css: string): void {
|
|
35
|
+
this.cssText = css;
|
|
36
|
+
this.replaceSyncCalls += 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const previousConstructor = Object.getOwnPropertyDescriptor(
|
|
41
|
+
globalThis,
|
|
42
|
+
'CSSStyleSheet',
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
Object.defineProperty(globalThis, 'CSSStyleSheet', {
|
|
46
|
+
value: FakeCSSStyleSheet,
|
|
47
|
+
writable: true,
|
|
48
|
+
configurable: true,
|
|
49
|
+
});
|
|
50
|
+
Object.defineProperty(document, 'adoptedStyleSheets', {
|
|
51
|
+
value: [],
|
|
52
|
+
writable: true,
|
|
53
|
+
configurable: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
adopted: () =>
|
|
58
|
+
document.adoptedStyleSheets as unknown as readonly FakeStyleSheet[],
|
|
59
|
+
uninstall: () => {
|
|
60
|
+
Reflect.deleteProperty(document, 'adoptedStyleSheets');
|
|
61
|
+
if (previousConstructor) {
|
|
62
|
+
Object.defineProperty(globalThis, 'CSSStyleSheet', previousConstructor);
|
|
63
|
+
} else {
|
|
64
|
+
Reflect.deleteProperty(globalThis, 'CSSStyleSheet');
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -176,7 +176,11 @@ const AccordionTriggerComponent: React.FC<AccordionTriggerProps> = ({
|
|
|
176
176
|
onPressOut={disabled ? undefined : onPressOut}
|
|
177
177
|
disabled={disabled}
|
|
178
178
|
accessibilityRole="button"
|
|
179
|
-
|
|
179
|
+
// `aria-expanded` rather than `accessibilityState`, which react-native-web
|
|
180
|
+
// drops entirely. React Native folds this back into `accessibilityState`,
|
|
181
|
+
// so it is the one spelling both platforms read. `disabled` travels on the
|
|
182
|
+
// `disabled` prop above.
|
|
183
|
+
aria-expanded={isExpanded}
|
|
180
184
|
>
|
|
181
185
|
{icon}
|
|
182
186
|
<View style={{ flex: 1 }}>
|