@multiplatform.one/forms 7.8.0 → 7.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/Button/index.cjs +1 -1
- package/dist/cjs/Button/index.native.js +5 -3
- package/dist/cjs/Button/index.native.js.map +1 -1
- package/dist/cjs/InputParts/index.cjs +11 -2
- package/dist/cjs/InputParts/index.native.js +11 -2
- package/dist/cjs/InputParts/index.native.js.map +1 -1
- package/dist/cjs/fields/FileUpload/index.cjs +6 -0
- package/dist/cjs/fields/FileUpload/index.native.js +6 -0
- package/dist/cjs/fields/FileUpload/index.native.js.map +1 -1
- package/dist/cjs/fields/Icon/drawnSet.cjs +3 -1
- package/dist/cjs/fields/Icon/drawnSet.native.js +3 -1
- package/dist/cjs/fields/Icon/drawnSet.native.js.map +1 -1
- package/dist/cjs/fields/RadioGroup/index.cjs +34 -5
- package/dist/cjs/fields/RadioGroup/index.native.js +49 -7
- package/dist/cjs/fields/RadioGroup/index.native.js.map +1 -1
- package/dist/cjs/fields/ToggleGroup/index.cjs +35 -2
- package/dist/cjs/fields/ToggleGroup/index.native.js +42 -2
- package/dist/cjs/fields/ToggleGroup/index.native.js.map +1 -1
- package/dist/cjs/shared/nativeA11y.cjs +57 -0
- package/dist/cjs/shared/nativeA11y.native.js +61 -0
- package/dist/cjs/shared/nativeA11y.native.js.map +1 -0
- package/dist/esm/AdaptivePopup/index.native.js.map +1 -1
- package/dist/esm/Button/index.mjs +1 -1
- package/dist/esm/Button/index.mjs.map +1 -1
- package/dist/esm/Button/index.native.js +5 -3
- package/dist/esm/Button/index.native.js.map +1 -1
- package/dist/esm/Chip/index.native.js.map +1 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
- package/dist/esm/ContextualSaveBar/clientKnownIdentity.native.js.map +1 -1
- package/dist/esm/ContextualSaveBar/commitImmediateField.native.js.map +1 -1
- package/dist/esm/ContextualSaveBar/index.native.js.map +1 -1
- package/dist/esm/ContextualSaveBar/useUnsavedChangesGuard.native.js.map +1 -1
- package/dist/esm/ControlGroup/index.native.js.map +1 -1
- package/dist/esm/DescriptionList/index.native.js.map +1 -1
- package/dist/esm/ErrorSummary.native.js.map +1 -1
- package/dist/esm/FieldDisplay/editMode.native.js.map +1 -1
- package/dist/esm/FieldDisplay/formatters.native.js.map +1 -1
- package/dist/esm/FieldDisplay/index.native.js.map +1 -1
- package/dist/esm/FieldGroup.native.js.map +1 -1
- package/dist/esm/FloatingPanel/nestedAction.native.js.map +1 -1
- package/dist/esm/FloatingPanel/useFloatingPanel.native.js.map +1 -1
- package/dist/esm/Form/index.native.js.map +1 -1
- package/dist/esm/Form/validateOn.native.js.map +1 -1
- package/dist/esm/FormGrid.native.js.map +1 -1
- package/dist/esm/FormSection.native.js.map +1 -1
- package/dist/esm/InputParts/index.mjs +11 -2
- package/dist/esm/InputParts/index.mjs.map +1 -1
- package/dist/esm/InputParts/index.native.js +11 -2
- package/dist/esm/InputParts/index.native.js.map +1 -1
- package/dist/esm/Label/index.native.js.map +1 -1
- package/dist/esm/Skeleton/index.native.js.map +1 -1
- package/dist/esm/Spinner/SpinnerGlyph.native.js.map +1 -1
- package/dist/esm/Spinner/spinnerSize.native.js.map +1 -1
- package/dist/esm/Table/index.native.js.map +1 -1
- package/dist/esm/Wheel/haptic.native.js.map +1 -1
- package/dist/esm/Wheel/index.native.js.map +1 -1
- package/dist/esm/actionLabel.native.js.map +1 -1
- package/dist/esm/fieldDefaults.native.js.map +1 -1
- package/dist/esm/fieldLayout.native.js.map +1 -1
- package/dist/esm/fields/Barcode/BarcodePreview.native.js.map +1 -1
- package/dist/esm/fields/Barcode/code128.native.js.map +1 -1
- package/dist/esm/fields/Barcode/index.native.js.map +1 -1
- package/dist/esm/fields/Barcode/qr.native.js.map +1 -1
- package/dist/esm/fields/Barcode/scannerTypes.native.js.map +1 -1
- package/dist/esm/fields/Checkbox/Checkbox.native.js.map +1 -1
- package/dist/esm/fields/Checkbox/CheckboxBox.native.js.map +1 -1
- package/dist/esm/fields/Checkbox/CheckboxCard.native.js.map +1 -1
- package/dist/esm/fields/Checkbox/CheckboxFieldFrame.native.js.map +1 -1
- package/dist/esm/fields/Checkbox/CheckboxGroup.native.js.map +1 -1
- package/dist/esm/fields/Checkbox/checkboxGroupContext.native.js.map +1 -1
- package/dist/esm/fields/Checkbox/index.native.js.map +1 -1
- package/dist/esm/fields/ColorPicker/index.native.js.map +1 -1
- package/dist/esm/fields/ColorPicker/parts.native.js.map +1 -1
- package/dist/esm/fields/Combobox/authorOptions.native.js.map +1 -1
- package/dist/esm/fields/Combobox/index.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/Calendar.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/DateRangePicker.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/DatetimePicker.catalog.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/MonthPicker.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/MultiDatePicker.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/catalog.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/fieldValueSync.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/parts.native.js.map +1 -1
- package/dist/esm/fields/DatePicker/utils.native.js.map +1 -1
- package/dist/esm/fields/Duration/durationMath.native.js.map +1 -1
- package/dist/esm/fields/Duration/index.native.js.map +1 -1
- package/dist/esm/fields/FileUpload/filePickerTypes.native.js.map +1 -1
- package/dist/esm/fields/FileUpload/index.mjs +6 -0
- package/dist/esm/fields/FileUpload/index.mjs.map +1 -1
- package/dist/esm/fields/FileUpload/index.native.js +6 -0
- package/dist/esm/fields/FileUpload/index.native.js.map +1 -1
- package/dist/esm/fields/FileUpload/nativePickerLoader.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/expoMapsLoader.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/geoValue.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/geocoder.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/index.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/mapMath.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/mapTypes.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/nativeSearchFocus.native.js.map +1 -1
- package/dist/esm/fields/Icon/drawnSet.mjs +3 -1
- package/dist/esm/fields/Icon/drawnSet.mjs.map +1 -1
- package/dist/esm/fields/Icon/drawnSet.native.js +3 -1
- package/dist/esm/fields/Icon/drawnSet.native.js.map +1 -1
- package/dist/esm/fields/Icon/index.native.js.map +1 -1
- package/dist/esm/fields/ImageField/index.native.js.map +1 -1
- package/dist/esm/fields/Input/index.native.js.map +1 -1
- package/dist/esm/fields/MentionInput/index.native.js.map +1 -1
- package/dist/esm/fields/OTPInput/index.native.js.map +1 -1
- package/dist/esm/fields/OTPInput/otpCellCss.native.js.map +1 -1
- package/dist/esm/fields/PhoneInput/dialCodes.native.js.map +1 -1
- package/dist/esm/fields/PhoneInput/index.native.js.map +1 -1
- package/dist/esm/fields/PhoneInput/phone.native.js.map +1 -1
- package/dist/esm/fields/Progress/index.native.js.map +1 -1
- package/dist/esm/fields/RadioGroup/index.mjs +34 -5
- package/dist/esm/fields/RadioGroup/index.mjs.map +1 -1
- package/dist/esm/fields/RadioGroup/index.native.js +49 -7
- package/dist/esm/fields/RadioGroup/index.native.js.map +1 -1
- package/dist/esm/fields/Rating/index.native.js.map +1 -1
- package/dist/esm/fields/Rating/ratingMath.native.js.map +1 -1
- package/dist/esm/fields/Rating/types.native.js.map +1 -1
- package/dist/esm/fields/SearchInput/index.native.js.map +1 -1
- package/dist/esm/fields/Select/index.native.js.map +1 -1
- package/dist/esm/fields/Signature/index.native.js.map +1 -1
- package/dist/esm/fields/Slider/index.native.js.map +1 -1
- package/dist/esm/fields/Slider/jumpMotion.native.js.map +1 -1
- package/dist/esm/fields/Stepper/index.native.js.map +1 -1
- package/dist/esm/fields/Stepper/resolveStepperPlacement.native.js.map +1 -1
- package/dist/esm/fields/Switch/index.native.js.map +1 -1
- package/dist/esm/fields/TextArea/index.native.js.map +1 -1
- package/dist/esm/fields/TimePicker/catalog.native.js.map +1 -1
- package/dist/esm/fields/TimePicker/parts.native.js.map +1 -1
- package/dist/esm/fields/TimeRangeScrubber/geometry.native.js.map +1 -1
- package/dist/esm/fields/TimeRangeScrubber/index.native.js.map +1 -1
- package/dist/esm/fields/ToggleGroup/index.mjs +35 -2
- package/dist/esm/fields/ToggleGroup/index.mjs.map +1 -1
- package/dist/esm/fields/ToggleGroup/index.native.js +42 -2
- package/dist/esm/fields/ToggleGroup/index.native.js.map +1 -1
- package/dist/esm/fields/nativeDateTime/OsDateTimeSheet.native.js.map +1 -1
- package/dist/esm/fields/nativeDateTime/osDateTimeIosDisplay.native.js.map +1 -1
- package/dist/esm/fields/nativeSelect.native.js.map +1 -1
- package/dist/esm/fields/selectedOrder.native.js.map +1 -1
- package/dist/esm/formSpacing.native.js.map +1 -1
- package/dist/esm/hooks/useFocusManagement.native.js.map +1 -1
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/requiredMarking.native.js.map +1 -1
- package/dist/esm/shared/TimeColumn.native.js.map +1 -1
- package/dist/esm/shared/capabilityPreflight.native.js.map +1 -1
- package/dist/esm/shared/colorRamps.native.js.map +1 -1
- package/dist/esm/shared/devWarn.native.js.map +1 -1
- package/dist/esm/shared/floatingList.native.js.map +1 -1
- package/dist/esm/shared/focusManagement.native.js.map +1 -1
- package/dist/esm/shared/groupContext.native.js.map +1 -1
- package/dist/esm/shared/i18n.native.js.map +1 -1
- package/dist/esm/shared/nativeA11y.mjs +31 -0
- package/dist/esm/shared/nativeA11y.mjs.map +1 -0
- package/dist/esm/shared/nativeA11y.native.js +32 -0
- package/dist/esm/shared/nativeA11y.native.js.map +1 -0
- package/dist/esm/shared/pickerTriggerContract.native.js.map +1 -1
- package/dist/esm/shared/pressSlopProps.native.js.map +1 -1
- package/dist/esm/shared/t.native.js.map +1 -1
- package/dist/esm/shared/tableCellContext.native.js.map +1 -1
- package/dist/esm/shared/useDragScroll.native.js.map +1 -1
- package/dist/esm/shared/useKeyboardInset.native.js.map +1 -1
- package/dist/esm/shared/useSheetEscapeDismiss.native.js.map +1 -1
- package/dist/esm/shared/useWheelIncrement.native.js.map +1 -1
- package/dist/esm/shared/utils.native.js.map +1 -1
- package/dist/esm/shared/zIndex.native.js.map +1 -1
- package/dist/esm/themedPrimitives.native.js.map +1 -1
- package/dist/esm/types.native.js.map +1 -1
- package/dist/esm/utils/LinkCell.native.js.map +1 -1
- package/dist/jsx/Button/index.mjs +1 -1
- package/dist/jsx/Button/index.mjs.map +1 -1
- package/dist/jsx/Button/index.native.js +5 -3
- package/dist/jsx/Button/index.native.js.map +1 -1
- package/dist/jsx/InputParts/index.mjs +11 -2
- package/dist/jsx/InputParts/index.mjs.map +1 -1
- package/dist/jsx/InputParts/index.native.js +11 -2
- package/dist/jsx/InputParts/index.native.js.map +1 -1
- package/dist/jsx/fields/FileUpload/index.mjs +6 -0
- package/dist/jsx/fields/FileUpload/index.mjs.map +1 -1
- package/dist/jsx/fields/FileUpload/index.native.js +6 -0
- package/dist/jsx/fields/FileUpload/index.native.js.map +1 -1
- package/dist/jsx/fields/Icon/drawnSet.mjs +3 -1
- package/dist/jsx/fields/Icon/drawnSet.mjs.map +1 -1
- package/dist/jsx/fields/Icon/drawnSet.native.js +3 -1
- package/dist/jsx/fields/Icon/drawnSet.native.js.map +1 -1
- package/dist/jsx/fields/RadioGroup/index.mjs +34 -5
- package/dist/jsx/fields/RadioGroup/index.mjs.map +1 -1
- package/dist/jsx/fields/RadioGroup/index.native.js +49 -7
- package/dist/jsx/fields/RadioGroup/index.native.js.map +1 -1
- package/dist/jsx/fields/ToggleGroup/index.mjs +35 -2
- package/dist/jsx/fields/ToggleGroup/index.mjs.map +1 -1
- package/dist/jsx/fields/ToggleGroup/index.native.js +42 -2
- package/dist/jsx/fields/ToggleGroup/index.native.js.map +1 -1
- package/dist/jsx/shared/nativeA11y.mjs +31 -0
- package/dist/jsx/shared/nativeA11y.mjs.map +1 -0
- package/dist/jsx/shared/nativeA11y.native.js +61 -0
- package/dist/jsx/shared/nativeA11y.native.js.map +1 -0
- package/package.json +9 -8
- package/src/Button/Button.fontWeight.spec.tsx +75 -0
- package/src/Button/index.tsx +5 -3
- package/src/FieldGroup.stories.tsx +3 -7
- package/src/FieldLayout.stories.tsx +34 -12
- package/src/Fieldset.stories.tsx +3 -7
- package/src/FormGrid.stories.tsx +5 -11
- package/src/InputParts/InputButton.spec.tsx +16 -0
- package/src/InputParts/index.tsx +19 -2
- package/src/fieldFamilyStories.radius.spec.tsx +118 -0
- package/src/fields/Barcode/Barcode.spec.tsx +2 -0
- package/src/fields/FileUpload/FileUpload.spec.tsx +29 -0
- package/src/fields/FileUpload/index.tsx +6 -0
- package/src/fields/Icon/drawnSet.tsx +7 -2
- package/src/fields/RadioGroup/RadioGroup.native.spec.tsx +67 -0
- package/src/fields/RadioGroup/index.tsx +82 -8
- package/src/fields/ToggleGroup/ToggleGroup.native.spec.tsx +73 -0
- package/src/fields/ToggleGroup/ToggleGroup.spec.tsx +49 -0
- package/src/fields/ToggleGroup/index.tsx +76 -1
- package/src/shared/nativeA11y.spec.ts +64 -0
- package/src/shared/nativeA11y.ts +80 -0
- package/types/Button/index.d.ts.map +1 -1
- package/types/InputParts/index.d.ts.map +1 -1
- package/types/fields/FileUpload/index.d.ts.map +1 -1
- package/types/fields/Icon/drawnSet.d.ts.map +1 -1
- package/types/fields/RadioGroup/index.d.ts +28 -1
- package/types/fields/RadioGroup/index.d.ts.map +1 -1
- package/types/fields/ToggleGroup/index.d.ts +30 -1
- package/types/fields/ToggleGroup/index.d.ts.map +1 -1
- package/types/shared/nativeA11y.d.ts +61 -0
- package/types/shared/nativeA11y.d.ts.map +1 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four form LAYOUT stories must mount house field chrome, not a raw
|
|
3
|
+
* tamagui primitive.
|
|
4
|
+
*
|
|
5
|
+
* FieldLayout, FieldGroup, Fieldset and FormGrid paint no radius of their own —
|
|
6
|
+
* they are stacks, a grid and a legend. Whatever radius a probe reads off one
|
|
7
|
+
* of their stories belongs to the CONTROL the story chose to mount. A raw
|
|
8
|
+
* `Input` from "tamagui" consumes no knobs, so it pins one radius at every
|
|
9
|
+
* stop and the axis reads dead against a component that never had a say.
|
|
10
|
+
*
|
|
11
|
+
* The measurement here is therefore the STORY's, not the component's: walk the
|
|
12
|
+
* borderRadius knob and assert the control inside each story tracks it.
|
|
13
|
+
*
|
|
14
|
+
* Under jsdom the providers inject no CSS and the radius atom keeps its token
|
|
15
|
+
* reference (`_btlr-t-radius-4`) rather than resolving to `9px`, so this file
|
|
16
|
+
* gates the token ladder. The pixel ladder (0/5/9/16/50) and the dark scheme
|
|
17
|
+
* are measured for real by scripts/knob-axis-probe.mjs against a browser.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { cleanup } from "@testing-library/react";
|
|
21
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
22
|
+
import { Preset, type BorderRadius } from "@multiplatform.one/theme";
|
|
23
|
+
import type { ReactElement } from "react";
|
|
24
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
25
|
+
|
|
26
|
+
import * as fieldGroupStories from "./FieldGroup.stories";
|
|
27
|
+
import * as fieldLayoutStories from "./FieldLayout.stories";
|
|
28
|
+
import * as fieldsetStories from "./Fieldset.stories";
|
|
29
|
+
import * as formGridStories from "./FormGrid.stories";
|
|
30
|
+
|
|
31
|
+
afterEach(cleanup);
|
|
32
|
+
|
|
33
|
+
/** Knob stop -> radius token index, the `$N` the resolver hands the frame. */
|
|
34
|
+
const ladder: Record<BorderRadius, string> = {
|
|
35
|
+
none: "0",
|
|
36
|
+
small: "2",
|
|
37
|
+
medium: "4",
|
|
38
|
+
large: "6",
|
|
39
|
+
full: "12",
|
|
40
|
+
};
|
|
41
|
+
const stops = Object.keys(ladder) as BorderRadius[];
|
|
42
|
+
|
|
43
|
+
type StoryLike = { render?: (args: Record<string, unknown>) => ReactElement };
|
|
44
|
+
|
|
45
|
+
/** Every radius token present in the subtree, deduped — the probe's reading. */
|
|
46
|
+
function radiusTokens(container: HTMLElement): string[] {
|
|
47
|
+
const found = new Set<string>();
|
|
48
|
+
for (const el of container.querySelectorAll<HTMLElement>("*")) {
|
|
49
|
+
for (const cls of String(el.className || "").split(/\s+/)) {
|
|
50
|
+
const match = /^_btlr-t-radius-(\d+)$/.exec(cls);
|
|
51
|
+
if (match) found.add(match[1]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [...found].sort();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function mountAt(story: StoryLike, meta: { args?: unknown }, stop: BorderRadius) {
|
|
58
|
+
if (!story.render) throw new Error("story has no render");
|
|
59
|
+
return renderWithProviders(
|
|
60
|
+
<Preset overrides={{ borderRadius: stop, elevation: "none" }}>
|
|
61
|
+
{story.render({ ...((meta.args ?? {}) as Record<string, unknown>) })}
|
|
62
|
+
</Preset>,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const family = [
|
|
67
|
+
{
|
|
68
|
+
name: "Forms/FieldLayout",
|
|
69
|
+
story: fieldLayoutStories.Basic as StoryLike,
|
|
70
|
+
meta: fieldLayoutStories.default,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "Forms/FieldGroup",
|
|
74
|
+
story: fieldGroupStories.Basic as StoryLike,
|
|
75
|
+
meta: fieldGroupStories.default,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: "Forms/Fieldset",
|
|
79
|
+
story: fieldsetStories.Default as StoryLike,
|
|
80
|
+
meta: fieldsetStories.default,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "Forms/FormGrid",
|
|
84
|
+
story: formGridStories.Basic as StoryLike,
|
|
85
|
+
meta: formGridStories.default,
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
describe("form layout stories mount knob-consuming field chrome", () => {
|
|
90
|
+
for (const { name, story, meta } of family) {
|
|
91
|
+
describe(name, () => {
|
|
92
|
+
// A story that fails to render measures flat on every axis, which reads
|
|
93
|
+
// exactly like a dead knob. Prove there is a control before believing
|
|
94
|
+
// anything measured off it.
|
|
95
|
+
it("renders a text control at all", () => {
|
|
96
|
+
const { container } = mountAt(story, meta as { args?: unknown }, "medium");
|
|
97
|
+
expect(container.querySelectorAll("input").length).toBeGreaterThan(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
for (const stop of stops) {
|
|
101
|
+
it(`takes radius token $${ladder[stop]} at borderRadius:${stop}`, () => {
|
|
102
|
+
const { container } = mountAt(story, meta as { args?: unknown }, stop);
|
|
103
|
+
expect(radiusTokens(container)).toContain(ladder[stop]);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
it("moves across all five stops instead of pinning one radius", () => {
|
|
108
|
+
const signatures = stops.map((stop) => {
|
|
109
|
+
const result = mountAt(story, meta as { args?: unknown }, stop);
|
|
110
|
+
const tokens = JSON.stringify(radiusTokens(result.container));
|
|
111
|
+
cleanup();
|
|
112
|
+
return tokens;
|
|
113
|
+
});
|
|
114
|
+
expect(new Set(signatures).size).toBe(stops.length);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
@@ -102,6 +102,8 @@ describe("Barcode", () => {
|
|
|
102
102
|
const button = result.container.querySelector("[aria-label='Scan barcode']");
|
|
103
103
|
expect(button).not.toBeNull();
|
|
104
104
|
expect(button?.textContent).toContain("Scan");
|
|
105
|
+
expect(button?.querySelector("scan")).toBeNull();
|
|
106
|
+
expect(result.container.querySelector("scan")).toBeNull();
|
|
105
107
|
});
|
|
106
108
|
|
|
107
109
|
it("renders readOnly — hides scan button", () => {
|
|
@@ -45,6 +45,14 @@ function getZone(container: Element): HTMLElement {
|
|
|
45
45
|
return zone as HTMLElement;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function borderWidthAtomPx(el: HTMLElement, prefix: string): number {
|
|
49
|
+
const atom = el.className.split(/\s+/).find((cls) => cls.startsWith(prefix));
|
|
50
|
+
// No atom means the side declares no width and falls through to the UA
|
|
51
|
+
// initial, which is exactly the defect — report it as such, not as 0.
|
|
52
|
+
if (!atom) return Number.NaN;
|
|
53
|
+
return Number.parseFloat(atom.slice(prefix.length));
|
|
54
|
+
}
|
|
55
|
+
|
|
48
56
|
async function pickFile(container: Element, file: File) {
|
|
49
57
|
const input = container.querySelector("input[type='file']") as HTMLInputElement | null;
|
|
50
58
|
expect(input).toBeTruthy();
|
|
@@ -317,6 +325,27 @@ describe("FileUpload bento-derived dropzone (MPO-57)", () => {
|
|
|
317
325
|
expect(zone.textContent).toContain("Choose File");
|
|
318
326
|
});
|
|
319
327
|
|
|
328
|
+
it("hangs the file list off ONE top hairline — bottom/left/right are 0, not the UA 3px initial (MPO-124)", async () => {
|
|
329
|
+
const { container } = renderWithHouseThemes(
|
|
330
|
+
<FileUpload label="Attachment" name="attachment" />,
|
|
331
|
+
);
|
|
332
|
+
await pickFile(container, new File(["hello"], "hello.txt", { type: "text/plain" }));
|
|
333
|
+
|
|
334
|
+
const zone = getZone(container);
|
|
335
|
+
const flist = zone.querySelector('[data-file-upload="files"]') as HTMLElement;
|
|
336
|
+
expect(flist).toBeTruthy();
|
|
337
|
+
|
|
338
|
+
// The frame declares `borderStyle: dashed` for all four sides. A side that
|
|
339
|
+
// declares no width therefore does not render nothing — it renders the CSS
|
|
340
|
+
// initial `medium` (3px) in `currentColor` ($color12), which is a heavy
|
|
341
|
+
// dashed BOX around the picked files instead of the one hairline the
|
|
342
|
+
// divider contract asks for.
|
|
343
|
+
expect(borderWidthAtomPx(flist, "_btw-")).toBe(1);
|
|
344
|
+
for (const prefix of ["_bbw-", "_blw-", "_brw-"] as const) {
|
|
345
|
+
expect(borderWidthAtomPx(flist, prefix)).toBe(0);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
320
349
|
it("keeps compact a space-only dial — CTA stays 44 and remove stays nested 32 (LC-76)", async () => {
|
|
321
350
|
const { container } = renderWithHouseThemes(
|
|
322
351
|
<FileUpload label="Attachment" name="attachment" compact />,
|
|
@@ -170,6 +170,12 @@ const DropOverlayFrame = styled(YStack, {
|
|
|
170
170
|
const UploadFileListFrame = styled(YStack, {
|
|
171
171
|
width: "100%",
|
|
172
172
|
borderTopWidth: 1,
|
|
173
|
+
// A side with a declared style and no declared width is NOT a side with no
|
|
174
|
+
// border: it takes the CSS initial `medium` (3px) in `currentColor`, which
|
|
175
|
+
// boxed the picked files in a heavy $color12 dash (MPO-124).
|
|
176
|
+
borderBottomWidth: 0,
|
|
177
|
+
borderLeftWidth: 0,
|
|
178
|
+
borderRightWidth: 0,
|
|
173
179
|
borderStyle: "dashed",
|
|
174
180
|
borderTopColor: formDropzoneColors.border,
|
|
175
181
|
});
|
|
@@ -213,13 +213,18 @@ export function filterDrawnIcons(
|
|
|
213
213
|
*/
|
|
214
214
|
const drawnIconInkToken = "color11";
|
|
215
215
|
|
|
216
|
-
function useDrawnInk(color?: string): string
|
|
216
|
+
function useDrawnInk(color?: string): string {
|
|
217
217
|
const theme = useTheme() as unknown as Record<string, { val?: unknown } | undefined>;
|
|
218
218
|
const raw = !color || color === "currentColor" ? `$${drawnIconInkToken}` : color;
|
|
219
219
|
const val = raw.startsWith("$") ? theme[raw.slice(1)]?.val : raw;
|
|
220
220
|
// An unknown consumer token falls back to the active theme's readable ink.
|
|
221
221
|
const fallback = theme[drawnIconInkToken]?.val ?? theme.color?.val;
|
|
222
|
-
|
|
222
|
+
if (typeof val === "string") return val;
|
|
223
|
+
if (typeof fallback === "string") return fallback;
|
|
224
|
+
// Outside a theme both lookups miss. Returning undefined is what MPO-72 was:
|
|
225
|
+
// react-native-svg takes it as nil and Core Graphics paints with whatever
|
|
226
|
+
// stroke was last in the context. A mid neutral is visible on both schemes.
|
|
227
|
+
return "#808080";
|
|
223
228
|
}
|
|
224
229
|
|
|
225
230
|
export function DrawnIcon({
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { RadioGroup } from "./index";
|
|
4
|
+
|
|
5
|
+
// MPO-283. Mock tamagui's isWeb so the native branch runs on the test host
|
|
6
|
+
// renderer, then read the React Native prop names back off the DOM. The state
|
|
7
|
+
// object itself is pinned in shared/nativeA11y.spec.ts — the host stringifies
|
|
8
|
+
// it — so what this file proves is that the component reaches the helper with
|
|
9
|
+
// the right role and the right name.
|
|
10
|
+
vi.mock("tamagui", async (importOriginal) => ({
|
|
11
|
+
...(await importOriginal<typeof import("tamagui")>()),
|
|
12
|
+
isWeb: false,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const phase = [
|
|
16
|
+
{ label: "CREDITS", value: "CREDITS" },
|
|
17
|
+
{ label: "DEBITS", value: "DEBITS" },
|
|
18
|
+
{ label: "DISCRETIONARY", value: "DISCRETIONARY", disabled: true },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const names = (container: HTMLElement) =>
|
|
22
|
+
Array.from(container.querySelectorAll("[accessibilityrole]")).map((el) => [
|
|
23
|
+
el.getAttribute("accessibilityrole"),
|
|
24
|
+
el.getAttribute("accessibilitylabel"),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
describe("native RadioGroup accessibility contract", () => {
|
|
28
|
+
it("makes every option a named control instead of bare StaticText", () => {
|
|
29
|
+
const { container } = renderWithProviders(
|
|
30
|
+
<RadioGroup label="Phase" options={phase} value="DEBITS" />,
|
|
31
|
+
);
|
|
32
|
+
expect(names(container)).toEqual([
|
|
33
|
+
["button", "CREDITS"],
|
|
34
|
+
["button", "DEBITS"],
|
|
35
|
+
["button", "DISCRETIONARY"],
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("prefixes a group accessibilityLabel onto each option, because iOS never reads a container", () => {
|
|
40
|
+
const { container } = renderWithProviders(
|
|
41
|
+
<RadioGroup accessibilityLabel="Phase" options={phase} value="DEBITS" />,
|
|
42
|
+
);
|
|
43
|
+
expect(names(container)).toContainEqual(["button", "Phase, DEBITS"]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("takes aria-label as an alias, since tamagui never maps it onto native", () => {
|
|
47
|
+
const { container } = renderWithProviders(
|
|
48
|
+
<RadioGroup aria-label="Phase" options={phase} value="DEBITS" />,
|
|
49
|
+
);
|
|
50
|
+
expect(names(container)).toContainEqual(["button", "Phase, DEBITS"]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("lets an option override its own name", () => {
|
|
54
|
+
const { container } = renderWithProviders(
|
|
55
|
+
<RadioGroup
|
|
56
|
+
options={[{ label: "▲", value: "up", accessibilityLabel: "Increasing" }]}
|
|
57
|
+
value="up"
|
|
58
|
+
/>,
|
|
59
|
+
);
|
|
60
|
+
expect(names(container)).toEqual([["button", "Increasing"]]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// The sibling Label is also told to stay out of the native tree so an option
|
|
64
|
+
// is not read twice. It cannot be asserted here — the web renderer drops the
|
|
65
|
+
// React Native accessibility props off Label entirely — so that one is
|
|
66
|
+
// measured on device instead.
|
|
67
|
+
});
|
|
@@ -28,6 +28,7 @@ import { getSize } from "@tamagui/get-token";
|
|
|
28
28
|
import { Skeleton } from "../../Skeleton";
|
|
29
29
|
import { Field, FieldLayout } from "../../fieldLayout";
|
|
30
30
|
import { formControlColors, formSelectedColors } from "../../shared/colorRamps";
|
|
31
|
+
import { choiceItemA11y, composeA11yName } from "../../shared/nativeA11y";
|
|
31
32
|
import {
|
|
32
33
|
getElevationWrapperProps,
|
|
33
34
|
getFieldError,
|
|
@@ -131,6 +132,14 @@ export interface RadioGroupOption {
|
|
|
131
132
|
disabled?: boolean;
|
|
132
133
|
/** Muted secondary line under the label; rendered in `card` mode. */
|
|
133
134
|
description?: string;
|
|
135
|
+
/**
|
|
136
|
+
* MPO-283 — accessible name for THIS option, overriding `label`. Use it
|
|
137
|
+
* when the option carries meaning the label text does not: a glyph, or a
|
|
138
|
+
* state encoded in fill/weight/position rather than words.
|
|
139
|
+
*/
|
|
140
|
+
accessibilityLabel?: string;
|
|
141
|
+
/** Extra spoken context for this option (native `accessibilityHint`). */
|
|
142
|
+
accessibilityHint?: string;
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
export interface RadioGroupProps {
|
|
@@ -179,11 +188,33 @@ export interface RadioGroupProps {
|
|
|
179
188
|
* primitives (same anatomy as `CheckboxGroup card`).
|
|
180
189
|
*/
|
|
181
190
|
card?: boolean;
|
|
191
|
+
/**
|
|
192
|
+
* MPO-283 — accessible name for the whole group.
|
|
193
|
+
*
|
|
194
|
+
* tamagui does not map `aria-label` onto React Native's
|
|
195
|
+
* `accessibilityLabel`, so the web attribute is inert on native; this prop
|
|
196
|
+
* is the native channel and `aria-label` is accepted as an alias.
|
|
197
|
+
*
|
|
198
|
+
* iOS speaks elements, never non-element containers, so a name set here is
|
|
199
|
+
* PREFIXED onto each option's accessible name ("Phase, CREDITS") instead of
|
|
200
|
+
* sitting on the frame where nothing reads it. Options stay individually
|
|
201
|
+
* reachable and keep their own checked state.
|
|
202
|
+
*/
|
|
203
|
+
accessibilityLabel?: string;
|
|
204
|
+
/** Alias for `accessibilityLabel`; also emitted as the web attribute. */
|
|
205
|
+
"aria-label"?: string;
|
|
206
|
+
/** Web-only name reference. Native has no id-based equivalent. */
|
|
207
|
+
"aria-labelledby"?: string;
|
|
208
|
+
/** Extra spoken context for the group (native `accessibilityHint`). */
|
|
209
|
+
accessibilityHint?: string;
|
|
182
210
|
}
|
|
183
211
|
|
|
184
212
|
interface RadioOptionItemProps {
|
|
185
213
|
option: RadioGroupOption;
|
|
186
214
|
itemId: string;
|
|
215
|
+
/** MPO-283 — the resolved native accessible name for this option. */
|
|
216
|
+
nativeLabel: string;
|
|
217
|
+
nativeHint?: string;
|
|
187
218
|
a11y?: { "aria-labelledby"?: string; "aria-describedby"?: string };
|
|
188
219
|
selected: boolean;
|
|
189
220
|
isTabStop?: boolean;
|
|
@@ -203,6 +234,8 @@ interface RadioOptionItemProps {
|
|
|
203
234
|
function RadioOptionItem({
|
|
204
235
|
option,
|
|
205
236
|
itemId,
|
|
237
|
+
nativeLabel,
|
|
238
|
+
nativeHint,
|
|
206
239
|
a11y,
|
|
207
240
|
selected,
|
|
208
241
|
isTabStop,
|
|
@@ -277,15 +310,22 @@ function RadioOptionItem({
|
|
|
277
310
|
{...(groupDisabled || option.disabled ? disabledChrome : undefined)}
|
|
278
311
|
pointerEvents={readOnly ? "none" : undefined}
|
|
279
312
|
{...a11y}
|
|
280
|
-
// RN only exposes the radio
|
|
281
|
-
//
|
|
313
|
+
// MPO-283. RN only exposes the radio when `accessible` is set, and the
|
|
314
|
+
// aria-* spellings this block used to carry never reached iOS at all:
|
|
315
|
+
// tamagui does not map them onto React Native's accessibility props, so
|
|
316
|
+
// options came back as bare StaticText with no role and no checked
|
|
317
|
+
// state. The RN prop names are the ones iOS actually reads.
|
|
282
318
|
{...(!isWeb
|
|
283
|
-
? {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
319
|
+
? choiceItemA11y({
|
|
320
|
+
kind: "radio",
|
|
321
|
+
name: nativeLabel,
|
|
322
|
+
selected,
|
|
323
|
+
disabled: Boolean(groupDisabled || option.disabled),
|
|
324
|
+
hint: nativeHint,
|
|
325
|
+
})
|
|
326
|
+
: option.accessibilityLabel
|
|
327
|
+
? { "aria-label": option.accessibilityLabel }
|
|
328
|
+
: {})}
|
|
289
329
|
{...(hoverProps && {
|
|
290
330
|
hoverStyle: {
|
|
291
331
|
...hoverProps,
|
|
@@ -344,6 +384,10 @@ export function RadioGroup({
|
|
|
344
384
|
skeleton,
|
|
345
385
|
compact,
|
|
346
386
|
card,
|
|
387
|
+
accessibilityLabel,
|
|
388
|
+
accessibilityHint,
|
|
389
|
+
"aria-label": ariaLabel,
|
|
390
|
+
"aria-labelledby": ariaLabelledBy,
|
|
347
391
|
}: RadioGroupProps) {
|
|
348
392
|
// Canonical `onChange` and the alias `onValueChange` both fire from the
|
|
349
393
|
// single emit site below (same reconciliation as Switch).
|
|
@@ -351,6 +395,17 @@ export function RadioGroup({
|
|
|
351
395
|
onChange?.(val);
|
|
352
396
|
onValueChange?.(val);
|
|
353
397
|
};
|
|
398
|
+
// MPO-283: one name, two channels — tamagui drops `aria-label` on native, so
|
|
399
|
+
// the prop and the attribute are reconciled here.
|
|
400
|
+
const groupA11yName = accessibilityLabel ?? ariaLabel;
|
|
401
|
+
// iOS reads elements, not containers, so an explicit group name rides in
|
|
402
|
+
// front of each option's own name rather than sitting on the frame.
|
|
403
|
+
const optionA11yName = (option: RadioGroupOption) =>
|
|
404
|
+
composeA11yName(
|
|
405
|
+
groupA11yName,
|
|
406
|
+
option.accessibilityLabel ??
|
|
407
|
+
(typeof option.label === "string" ? option.label : String(option.value)),
|
|
408
|
+
);
|
|
354
409
|
const { resolvedForm, knobProps, control, text, elevation, disabledState, id } = useFormField({
|
|
355
410
|
form: formProp,
|
|
356
411
|
id: idProp,
|
|
@@ -440,6 +495,8 @@ export function RadioGroup({
|
|
|
440
495
|
<RadioOptionItem
|
|
441
496
|
option={option}
|
|
442
497
|
itemId={itemId}
|
|
498
|
+
nativeLabel={optionA11yName(option)}
|
|
499
|
+
nativeHint={option.accessibilityHint ?? accessibilityHint}
|
|
443
500
|
a11y={a11y}
|
|
444
501
|
selected={!!selected}
|
|
445
502
|
isTabStop={isTabStop}
|
|
@@ -541,6 +598,15 @@ export function RadioGroup({
|
|
|
541
598
|
)}
|
|
542
599
|
<Label
|
|
543
600
|
htmlFor={itemId}
|
|
601
|
+
// MPO-283: on native the radio itself now carries this text as
|
|
602
|
+
// its accessible name, so leaving the Label in the tree reads
|
|
603
|
+
// every option twice. Web keeps the label/control association.
|
|
604
|
+
{...(!isWeb
|
|
605
|
+
? {
|
|
606
|
+
accessibilityElementsHidden: true,
|
|
607
|
+
importantForAccessibility: "no-hide-descendants" as const,
|
|
608
|
+
}
|
|
609
|
+
: undefined)}
|
|
544
610
|
size={(size || knobProps.sizeToken) as FontSizeTokens}
|
|
545
611
|
{...knobProps.body}
|
|
546
612
|
color={knobProps.textAccentColor}
|
|
@@ -615,6 +681,14 @@ export function RadioGroup({
|
|
|
615
681
|
aria-required={required || undefined}
|
|
616
682
|
aria-invalid={hasError || undefined}
|
|
617
683
|
aria-readonly={readOnly || undefined}
|
|
684
|
+
// MPO-283: web reads the attribute, Android reads the RN prop off the
|
|
685
|
+
// container. iOS reads neither, which is why the name is also prefixed
|
|
686
|
+
// onto every option above.
|
|
687
|
+
{...(groupA11yName ? { "aria-label": groupA11yName } : undefined)}
|
|
688
|
+
{...(ariaLabelledBy ? { "aria-labelledby": ariaLabelledBy } : undefined)}
|
|
689
|
+
{...(!isWeb && groupA11yName
|
|
690
|
+
? { accessibilityLabel: groupA11yName, accessibilityRole: "radiogroup" as const }
|
|
691
|
+
: undefined)}
|
|
618
692
|
>
|
|
619
693
|
{renderItems(radioValue, emit)}
|
|
620
694
|
</TamaguiRadioGroup>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { ToggleGroup } from "./index";
|
|
4
|
+
|
|
5
|
+
// MPO-283. Before this the whole segmented control was absent from the iOS
|
|
6
|
+
// tree. See RadioGroup.native.spec.tsx for why the state object is pinned in
|
|
7
|
+
// shared/nativeA11y.spec.ts rather than here.
|
|
8
|
+
vi.mock("tamagui", async (importOriginal) => ({
|
|
9
|
+
...(await importOriginal<typeof import("tamagui")>()),
|
|
10
|
+
isWeb: false,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const severity = [
|
|
14
|
+
{ label: "error", value: "error" },
|
|
15
|
+
{ label: "warning", value: "warning" },
|
|
16
|
+
{ label: "info", value: "info", disabled: true },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const names = (container: HTMLElement) =>
|
|
20
|
+
Array.from(container.querySelectorAll("[accessibilityrole]")).map((el) => [
|
|
21
|
+
el.getAttribute("accessibilityrole"),
|
|
22
|
+
el.getAttribute("accessibilitylabel"),
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
describe("native ToggleGroup accessibility contract", () => {
|
|
26
|
+
it("puts every segment in the tree as a named control", () => {
|
|
27
|
+
const { container } = renderWithProviders(
|
|
28
|
+
<ToggleGroup label="Severity" options={severity} value="warning" />,
|
|
29
|
+
);
|
|
30
|
+
expect(names(container)).toEqual([
|
|
31
|
+
["button", "error"],
|
|
32
|
+
["button", "warning"],
|
|
33
|
+
["button", "info"],
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// The platform role is `button` for both kinds, measured: a radio/checkbox
|
|
38
|
+
// accessibilityRole takes the element out of the iOS tree entirely.
|
|
39
|
+
it("keeps every segment in the tree when the group takes several values", () => {
|
|
40
|
+
const { container } = renderWithProviders(
|
|
41
|
+
<ToggleGroup type="multiple" options={severity} value={["error"]} />,
|
|
42
|
+
);
|
|
43
|
+
expect(names(container)).toEqual([
|
|
44
|
+
["button", "error"],
|
|
45
|
+
["button", "warning"],
|
|
46
|
+
["button", "info"],
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("prefixes a group accessibilityLabel onto each segment, because iOS never reads a container", () => {
|
|
51
|
+
const { container } = renderWithProviders(
|
|
52
|
+
<ToggleGroup accessibilityLabel="Severity" options={severity} value="warning" />,
|
|
53
|
+
);
|
|
54
|
+
expect(names(container)).toContainEqual(["button", "Severity, warning"]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("takes aria-label as an alias, since tamagui never maps it onto native", () => {
|
|
58
|
+
const { container } = renderWithProviders(
|
|
59
|
+
<ToggleGroup aria-label="Severity" options={severity} value="warning" />,
|
|
60
|
+
);
|
|
61
|
+
expect(names(container)).toContainEqual(["button", "Severity, warning"]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("lets a segment override its own name", () => {
|
|
65
|
+
const { container } = renderWithProviders(
|
|
66
|
+
<ToggleGroup
|
|
67
|
+
options={[{ label: "●", value: "on", accessibilityLabel: "Filled" }]}
|
|
68
|
+
value="on"
|
|
69
|
+
/>,
|
|
70
|
+
);
|
|
71
|
+
expect(names(container)).toEqual([["button", "Filled"]]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -294,6 +294,55 @@ describe("ToggleGroup", () => {
|
|
|
294
294
|
});
|
|
295
295
|
});
|
|
296
296
|
|
|
297
|
+
describe("per-segment elevation wrapper (MPO-133)", () => {
|
|
298
|
+
function elevationWrapperFor(segment: HTMLElement): HTMLElement {
|
|
299
|
+
const wrapper = segment.parentElement as HTMLElement;
|
|
300
|
+
expect(wrapper?.getAttribute("style") ?? "").toMatch(/box-shadow/);
|
|
301
|
+
return wrapper;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
it("lays the wrapper out as a row so the segment's zero flex-basis sizes width, not height", () => {
|
|
305
|
+
const { container } = renderWithHouseThemes(
|
|
306
|
+
<ToggleGroup label="Alignment" options={options} defaultValue="left" />,
|
|
307
|
+
);
|
|
308
|
+
const segments = [...container.querySelectorAll("[data-state]")] as HTMLElement[];
|
|
309
|
+
expect(segments).toHaveLength(3);
|
|
310
|
+
for (const segment of segments) {
|
|
311
|
+
expect(segment.className).toContain("_fb-0px");
|
|
312
|
+
const wrapper = elevationWrapperFor(segment);
|
|
313
|
+
expect(wrapper.className).toContain("_fd-row");
|
|
314
|
+
expect(wrapper.className).toContain("_fg-1");
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("keeps the row wrapper in the vertical orientation", () => {
|
|
319
|
+
const { container } = renderWithHouseThemes(
|
|
320
|
+
<ToggleGroup
|
|
321
|
+
label="Alignment"
|
|
322
|
+
options={options}
|
|
323
|
+
defaultValue="left"
|
|
324
|
+
orientation="vertical"
|
|
325
|
+
/>,
|
|
326
|
+
);
|
|
327
|
+
const segments = [...container.querySelectorAll("[data-state]")] as HTMLElement[];
|
|
328
|
+
expect(segments).toHaveLength(3);
|
|
329
|
+
for (const segment of segments) {
|
|
330
|
+
expect(segment.className).toContain("_fb-0px");
|
|
331
|
+
expect(elevationWrapperFor(segment).className).toContain("_fd-row");
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("leaves the group frame flat while the segments carry the shadow (E-FLAT)", () => {
|
|
336
|
+
const { container } = renderWithHouseThemes(
|
|
337
|
+
<ToggleGroup label="Alignment" options={options} defaultValue="left" />,
|
|
338
|
+
);
|
|
339
|
+
const group = container.querySelector('[role="group"]') as HTMLElement;
|
|
340
|
+
expect(group).toBeTruthy();
|
|
341
|
+
expect(group.className).not.toMatch(/_bxsh-/);
|
|
342
|
+
expect(group.getAttribute("style") ?? "").not.toMatch(/box-shadow/);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
297
346
|
describe("one tab stop (LC-49 / SB-M-217)", () => {
|
|
298
347
|
it("exposes exactly one tab stop among the segments", () => {
|
|
299
348
|
const { container } = renderWithHouseThemes(
|