@multiplatform.one/forms 7.57.0 → 7.59.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/AdaptivePopup/index.cjs +5 -1
- package/dist/cjs/AdaptivePopup/index.native.js +5 -1
- package/dist/cjs/AdaptivePopup/index.native.js.map +1 -1
- package/dist/cjs/Button/index.cjs +2 -1
- package/dist/cjs/Button/index.native.js +2 -1
- package/dist/cjs/Button/index.native.js.map +1 -1
- package/dist/cjs/FloatingPanel/compactFace.cjs +38 -0
- package/dist/cjs/FloatingPanel/compactFace.native.js +42 -0
- package/dist/cjs/FloatingPanel/compactFace.native.js.map +1 -0
- package/dist/cjs/FloatingPanel/index.cjs +38 -9
- package/dist/cjs/FloatingPanel/index.native.js +160 -6
- package/dist/cjs/FloatingPanel/index.native.js.map +1 -1
- package/dist/cjs/FloatingPanel/pointAnchor.cjs +22 -0
- package/dist/cjs/FloatingPanel/pointAnchor.native.js +23 -0
- package/dist/cjs/FloatingPanel/pointAnchor.native.js.map +1 -1
- package/dist/cjs/InputParts/index.cjs +13 -7
- package/dist/cjs/InputParts/index.native.js +13 -7
- package/dist/cjs/InputParts/index.native.js.map +1 -1
- package/dist/cjs/fields/Checkbox/Checkbox.cjs +24 -14
- package/dist/cjs/fields/Checkbox/Checkbox.native.js +38 -22
- package/dist/cjs/fields/Checkbox/Checkbox.native.js.map +1 -1
- package/dist/cjs/fields/RadioGroup/index.cjs +6 -1
- package/dist/cjs/fields/RadioGroup/index.native.js +6 -1
- package/dist/cjs/fields/RadioGroup/index.native.js.map +1 -1
- package/dist/cjs/fields/Select/InlineChoices.cjs +148 -0
- package/dist/cjs/fields/Select/InlineChoices.native.js +163 -0
- package/dist/cjs/fields/Select/InlineChoices.native.js.map +1 -0
- package/dist/cjs/fields/Select/index.cjs +79 -17
- package/dist/cjs/fields/Select/index.native.js +92 -17
- package/dist/cjs/fields/Select/index.native.js.map +1 -1
- package/dist/cjs/fields/Select/shouldInlineChoices.cjs +37 -0
- package/dist/cjs/fields/Select/shouldInlineChoices.native.js +43 -0
- package/dist/cjs/fields/Select/shouldInlineChoices.native.js.map +1 -0
- package/dist/cjs/fields/Switch/index.cjs +22 -10
- package/dist/cjs/fields/Switch/index.native.js +28 -14
- package/dist/cjs/fields/Switch/index.native.js.map +1 -1
- package/dist/cjs/shared/choiceRowPress.cjs +46 -0
- package/dist/cjs/shared/choiceRowPress.native.js +50 -0
- package/dist/cjs/shared/choiceRowPress.native.js.map +1 -0
- package/dist/cjs/shared/overlayStack.cjs +4 -0
- package/dist/cjs/shared/overlayStack.native.js +4 -0
- package/dist/cjs/shared/overlayStack.native.js.map +1 -1
- package/dist/esm/AdaptivePopup/index.mjs +6 -2
- package/dist/esm/AdaptivePopup/index.mjs.map +1 -1
- package/dist/esm/AdaptivePopup/index.native.js +6 -2
- package/dist/esm/AdaptivePopup/index.native.js.map +1 -1
- package/dist/esm/Button/index.mjs +3 -2
- package/dist/esm/Button/index.mjs.map +1 -1
- package/dist/esm/Button/index.native.js +3 -2
- package/dist/esm/Button/index.native.js.map +1 -1
- package/dist/esm/FloatingPanel/compactFace.mjs +11 -0
- package/dist/esm/FloatingPanel/compactFace.mjs.map +1 -0
- package/dist/esm/FloatingPanel/compactFace.native.js +12 -0
- package/dist/esm/FloatingPanel/compactFace.native.js.map +1 -0
- package/dist/esm/FloatingPanel/index.mjs +40 -11
- package/dist/esm/FloatingPanel/index.mjs.map +1 -1
- package/dist/esm/FloatingPanel/index.native.js +162 -8
- package/dist/esm/FloatingPanel/index.native.js.map +1 -1
- package/dist/esm/FloatingPanel/pointAnchor.mjs +22 -1
- package/dist/esm/FloatingPanel/pointAnchor.mjs.map +1 -1
- package/dist/esm/FloatingPanel/pointAnchor.native.js +23 -1
- package/dist/esm/FloatingPanel/pointAnchor.native.js.map +1 -1
- package/dist/esm/InputParts/index.mjs +13 -7
- package/dist/esm/InputParts/index.mjs.map +1 -1
- package/dist/esm/InputParts/index.native.js +13 -7
- package/dist/esm/InputParts/index.native.js.map +1 -1
- package/dist/esm/fields/Checkbox/Checkbox.mjs +24 -14
- package/dist/esm/fields/Checkbox/Checkbox.mjs.map +1 -1
- package/dist/esm/fields/Checkbox/Checkbox.native.js +38 -22
- package/dist/esm/fields/Checkbox/Checkbox.native.js.map +1 -1
- package/dist/esm/fields/RadioGroup/index.mjs +6 -1
- package/dist/esm/fields/RadioGroup/index.mjs.map +1 -1
- package/dist/esm/fields/RadioGroup/index.native.js +6 -1
- package/dist/esm/fields/RadioGroup/index.native.js.map +1 -1
- package/dist/esm/fields/Select/InlineChoices.mjs +123 -0
- package/dist/esm/fields/Select/InlineChoices.mjs.map +1 -0
- package/dist/esm/fields/Select/InlineChoices.native.js +135 -0
- package/dist/esm/fields/Select/InlineChoices.native.js.map +1 -0
- package/dist/esm/fields/Select/index.mjs +82 -20
- package/dist/esm/fields/Select/index.mjs.map +1 -1
- package/dist/esm/fields/Select/index.native.js +95 -20
- package/dist/esm/fields/Select/index.native.js.map +1 -1
- package/dist/esm/fields/Select/shouldInlineChoices.mjs +11 -0
- package/dist/esm/fields/Select/shouldInlineChoices.mjs.map +1 -0
- package/dist/esm/fields/Select/shouldInlineChoices.native.js +14 -0
- package/dist/esm/fields/Select/shouldInlineChoices.native.js.map +1 -0
- package/dist/esm/fields/Switch/index.mjs +22 -10
- package/dist/esm/fields/Switch/index.mjs.map +1 -1
- package/dist/esm/fields/Switch/index.native.js +28 -14
- package/dist/esm/fields/Switch/index.native.js.map +1 -1
- package/dist/esm/shared/choiceRowPress.mjs +20 -0
- package/dist/esm/shared/choiceRowPress.mjs.map +1 -0
- package/dist/esm/shared/choiceRowPress.native.js +21 -0
- package/dist/esm/shared/choiceRowPress.native.js.map +1 -0
- package/dist/esm/shared/overlayStack.mjs +4 -1
- package/dist/esm/shared/overlayStack.mjs.map +1 -1
- package/dist/esm/shared/overlayStack.native.js +4 -1
- package/dist/esm/shared/overlayStack.native.js.map +1 -1
- package/dist/jsx/AdaptivePopup/index.mjs +6 -2
- package/dist/jsx/AdaptivePopup/index.mjs.map +1 -1
- package/dist/jsx/AdaptivePopup/index.native.js +5 -1
- package/dist/jsx/AdaptivePopup/index.native.js.map +1 -1
- package/dist/jsx/Button/index.mjs +3 -2
- package/dist/jsx/Button/index.mjs.map +1 -1
- package/dist/jsx/Button/index.native.js +2 -1
- package/dist/jsx/Button/index.native.js.map +1 -1
- package/dist/jsx/FloatingPanel/compactFace.mjs +11 -0
- package/dist/jsx/FloatingPanel/compactFace.mjs.map +1 -0
- package/dist/jsx/FloatingPanel/compactFace.native.js +42 -0
- package/dist/jsx/FloatingPanel/compactFace.native.js.map +1 -0
- package/dist/jsx/FloatingPanel/index.mjs +40 -11
- package/dist/jsx/FloatingPanel/index.mjs.map +1 -1
- package/dist/jsx/FloatingPanel/index.native.js +160 -6
- package/dist/jsx/FloatingPanel/index.native.js.map +1 -1
- package/dist/jsx/FloatingPanel/pointAnchor.mjs +22 -1
- package/dist/jsx/FloatingPanel/pointAnchor.mjs.map +1 -1
- package/dist/jsx/FloatingPanel/pointAnchor.native.js +23 -0
- package/dist/jsx/FloatingPanel/pointAnchor.native.js.map +1 -1
- package/dist/jsx/InputParts/index.mjs +13 -7
- package/dist/jsx/InputParts/index.mjs.map +1 -1
- package/dist/jsx/InputParts/index.native.js +13 -7
- package/dist/jsx/InputParts/index.native.js.map +1 -1
- package/dist/jsx/fields/Checkbox/Checkbox.mjs +24 -14
- package/dist/jsx/fields/Checkbox/Checkbox.mjs.map +1 -1
- package/dist/jsx/fields/Checkbox/Checkbox.native.js +38 -22
- package/dist/jsx/fields/Checkbox/Checkbox.native.js.map +1 -1
- package/dist/jsx/fields/RadioGroup/index.mjs +6 -1
- package/dist/jsx/fields/RadioGroup/index.mjs.map +1 -1
- package/dist/jsx/fields/RadioGroup/index.native.js +6 -1
- package/dist/jsx/fields/RadioGroup/index.native.js.map +1 -1
- package/dist/jsx/fields/Select/InlineChoices.mjs +123 -0
- package/dist/jsx/fields/Select/InlineChoices.mjs.map +1 -0
- package/dist/jsx/fields/Select/InlineChoices.native.js +163 -0
- package/dist/jsx/fields/Select/InlineChoices.native.js.map +1 -0
- package/dist/jsx/fields/Select/index.mjs +82 -20
- package/dist/jsx/fields/Select/index.mjs.map +1 -1
- package/dist/jsx/fields/Select/index.native.js +92 -17
- package/dist/jsx/fields/Select/index.native.js.map +1 -1
- package/dist/jsx/fields/Select/shouldInlineChoices.mjs +11 -0
- package/dist/jsx/fields/Select/shouldInlineChoices.mjs.map +1 -0
- package/dist/jsx/fields/Select/shouldInlineChoices.native.js +43 -0
- package/dist/jsx/fields/Select/shouldInlineChoices.native.js.map +1 -0
- package/dist/jsx/fields/Switch/index.mjs +22 -10
- package/dist/jsx/fields/Switch/index.mjs.map +1 -1
- package/dist/jsx/fields/Switch/index.native.js +28 -14
- package/dist/jsx/fields/Switch/index.native.js.map +1 -1
- package/dist/jsx/shared/choiceRowPress.mjs +20 -0
- package/dist/jsx/shared/choiceRowPress.mjs.map +1 -0
- package/dist/jsx/shared/choiceRowPress.native.js +50 -0
- package/dist/jsx/shared/choiceRowPress.native.js.map +1 -0
- package/dist/jsx/shared/overlayStack.mjs +4 -1
- package/dist/jsx/shared/overlayStack.mjs.map +1 -1
- package/dist/jsx/shared/overlayStack.native.js +4 -0
- package/dist/jsx/shared/overlayStack.native.js.map +1 -1
- package/package.json +8 -8
- package/src/AdaptivePopup/index.tsx +6 -1
- package/src/Button/Button.controlTiers.spec.tsx +29 -0
- package/src/Button/index.tsx +3 -1
- package/src/FloatingPanel/FloatingPanel.compactFace.spec.tsx +187 -0
- package/src/FloatingPanel/FloatingPanel.menuFace.native.spec.tsx +195 -0
- package/src/FloatingPanel/compactFace.spec.ts +40 -0
- package/src/FloatingPanel/compactFace.ts +32 -0
- package/src/FloatingPanel/index.native.tsx +178 -4
- package/src/FloatingPanel/index.tsx +55 -11
- package/src/FloatingPanel/pointAnchor.spec.ts +80 -0
- package/src/FloatingPanel/pointAnchor.ts +52 -0
- package/src/InputParts/index.tsx +18 -7
- package/src/fields/Checkbox/Checkbox.native.spec.tsx +44 -0
- package/src/fields/Checkbox/Checkbox.tsx +31 -18
- package/src/fields/DatePicker/columnFit.spec.tsx +85 -0
- package/src/fields/RadioGroup/RadioGroup.native.spec.tsx +65 -0
- package/src/fields/RadioGroup/RadioGroup.stories.tsx +49 -0
- package/src/fields/RadioGroup/index.tsx +14 -2
- package/src/fields/Select/InlineChoices.tsx +156 -0
- package/src/fields/Select/index.tsx +100 -45
- package/src/fields/Select/shouldInlineChoices.spec.ts +25 -0
- package/src/fields/Select/shouldInlineChoices.ts +22 -0
- package/src/fields/Switch/Switch.rowPress.native.spec.tsx +69 -0
- package/src/fields/Switch/index.tsx +29 -11
- package/src/fields/pickerRows.spec.tsx +2 -0
- package/src/overlayGround.spec.ts +181 -0
- package/src/shared/choiceRowPress.ts +40 -0
- package/src/shared/overlayStack.tsx +5 -0
- package/types/AdaptivePopup/index.d.ts.map +1 -1
- package/types/Button/index.d.ts.map +1 -1
- package/types/FloatingPanel/compactFace.d.ts +21 -0
- package/types/FloatingPanel/compactFace.d.ts.map +1 -0
- package/types/FloatingPanel/index.d.ts +10 -0
- package/types/FloatingPanel/index.d.ts.map +1 -1
- package/types/FloatingPanel/index.native.d.ts +8 -1
- package/types/FloatingPanel/index.native.d.ts.map +1 -1
- package/types/FloatingPanel/pointAnchor.d.ts +29 -0
- package/types/FloatingPanel/pointAnchor.d.ts.map +1 -1
- package/types/InputParts/index.d.ts.map +1 -1
- package/types/fields/Checkbox/Checkbox.d.ts.map +1 -1
- package/types/fields/Checkbox/index.d.ts +3 -3
- package/types/fields/RadioGroup/index.d.ts.map +1 -1
- package/types/fields/Select/InlineChoices.d.ts +29 -0
- package/types/fields/Select/InlineChoices.d.ts.map +1 -0
- package/types/fields/Select/index.d.ts +6 -0
- package/types/fields/Select/index.d.ts.map +1 -1
- package/types/fields/Select/shouldInlineChoices.d.ts +16 -0
- package/types/fields/Select/shouldInlineChoices.d.ts.map +1 -0
- package/types/fields/Switch/index.d.ts.map +1 -1
- package/types/shared/choiceRowPress.d.ts +40 -0
- package/types/shared/choiceRowPress.d.ts.map +1 -0
- package/types/shared/overlayStack.d.ts +2 -0
- package/types/shared/overlayStack.d.ts.map +1 -1
|
@@ -6,8 +6,11 @@ import { useState } from "react";
|
|
|
6
6
|
import { RadioGroup as TamaguiRadioGroup, XStack, YStack } from "tamagui";
|
|
7
7
|
import { Label } from "../../Label";
|
|
8
8
|
import { Preset, Text, Paragraph } from "@multiplatform.one/theme";
|
|
9
|
+
import { AdaptivePopup } from "../../AdaptivePopup";
|
|
9
10
|
import { Button } from "../../Button";
|
|
10
11
|
import { Form } from "../../Form";
|
|
12
|
+
import { Checkbox } from "../Checkbox";
|
|
13
|
+
import { Switch } from "../Switch";
|
|
11
14
|
import { RadioGroup } from "./index";
|
|
12
15
|
|
|
13
16
|
const meta: Meta<typeof RadioGroup> = {
|
|
@@ -374,3 +377,49 @@ export const InTable: Story = {
|
|
|
374
377
|
);
|
|
375
378
|
},
|
|
376
379
|
};
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* FZ-7849: on a phone the whole row of every labelled choice selects, the label
|
|
383
|
+
* included, inside a sheet as on the page. The SHC console's Catalog filters
|
|
384
|
+
* found the label dead in a sheet.
|
|
385
|
+
*/
|
|
386
|
+
export const RowPressInSheet: Story = {
|
|
387
|
+
name: "Row press in a sheet",
|
|
388
|
+
render: () => {
|
|
389
|
+
const RowPressExample = () => {
|
|
390
|
+
const [open, setOpen] = useState(false);
|
|
391
|
+
const [kind, setKind] = useState("app");
|
|
392
|
+
const [installed, setInstalled] = useState(false);
|
|
393
|
+
const [updates, setUpdates] = useState(false);
|
|
394
|
+
const summary = `Kind: ${kind} · Installed: ${installed ? "yes" : "no"} · Updates: ${updates ? "on" : "off"}`;
|
|
395
|
+
return (
|
|
396
|
+
<YStack gap="$3">
|
|
397
|
+
<Text>{summary}</Text>
|
|
398
|
+
<Button onPress={() => setOpen(true)}>Filter</Button>
|
|
399
|
+
<AdaptivePopup open={open} onOpenChange={setOpen} title="Filter">
|
|
400
|
+
<YStack gap="$4">
|
|
401
|
+
<RadioGroup
|
|
402
|
+
label="Kind"
|
|
403
|
+
options={[
|
|
404
|
+
{ value: "app", label: "app" },
|
|
405
|
+
{ value: "vapp", label: "vapp" },
|
|
406
|
+
{ value: "addon", label: "addon" },
|
|
407
|
+
]}
|
|
408
|
+
value={kind}
|
|
409
|
+
onChange={setKind}
|
|
410
|
+
/>
|
|
411
|
+
<Checkbox
|
|
412
|
+
label="Installed only"
|
|
413
|
+
checked={installed}
|
|
414
|
+
onChange={(next) => setInstalled(next === true)}
|
|
415
|
+
/>
|
|
416
|
+
<Switch label="Auto updates" checked={updates} onChange={setUpdates} />
|
|
417
|
+
<Text>{summary}</Text>
|
|
418
|
+
</YStack>
|
|
419
|
+
</AdaptivePopup>
|
|
420
|
+
</YStack>
|
|
421
|
+
);
|
|
422
|
+
};
|
|
423
|
+
return <RowPressExample />;
|
|
424
|
+
},
|
|
425
|
+
};
|
|
@@ -31,6 +31,7 @@ import { Skeleton } from "../../Skeleton";
|
|
|
31
31
|
import { Field, FieldLayout } from "../../fieldLayout";
|
|
32
32
|
import { formControlColors, formSelectedColors } from "../../shared/colorRamps";
|
|
33
33
|
import { choiceItemA11y, composeA11yName } from "../../shared/nativeA11y";
|
|
34
|
+
import { choiceLabelPressProps, choiceRowPressProps } from "../../shared/choiceRowPress";
|
|
34
35
|
import {
|
|
35
36
|
getElevationWrapperProps,
|
|
36
37
|
getFieldError,
|
|
@@ -582,8 +583,19 @@ export function RadioGroup({
|
|
|
582
583
|
<Container gap="$2">
|
|
583
584
|
{options.map((option, index) => {
|
|
584
585
|
const itemId = `${id}-${index}-${option.value}`;
|
|
586
|
+
const select =
|
|
587
|
+
disabled || option.disabled || readOnly
|
|
588
|
+
? undefined
|
|
589
|
+
: () => {
|
|
590
|
+
if (currentValue !== option.value) emit(option.value);
|
|
591
|
+
};
|
|
585
592
|
return (
|
|
586
|
-
<XStack
|
|
593
|
+
<XStack
|
|
594
|
+
key={`${index}-${option.value}`}
|
|
595
|
+
gap="$2"
|
|
596
|
+
alignItems="center"
|
|
597
|
+
{...choiceRowPressProps(select)}
|
|
598
|
+
>
|
|
587
599
|
{renderRadioItem(
|
|
588
600
|
option,
|
|
589
601
|
itemId,
|
|
@@ -592,7 +604,7 @@ export function RadioGroup({
|
|
|
592
604
|
index === tabStopIndex,
|
|
593
605
|
)}
|
|
594
606
|
<Label
|
|
595
|
-
|
|
607
|
+
{...choiceLabelPressProps(itemId, select)}
|
|
596
608
|
// MPO-283: on native the radio itself now carries this text as
|
|
597
609
|
// its accessible name, so leaving the Label in the tree reads
|
|
598
610
|
// every option twice. Web keeps the label/control association.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { hairline, IconSlot, keyboardFocusRingProps } from "@multiplatform.one/theme";
|
|
2
|
+
import { CheckIcon } from "@phosphor-icons/react";
|
|
3
|
+
import { useRef, type KeyboardEvent, type ReactNode } from "react";
|
|
4
|
+
import { Pressable } from "react-native";
|
|
5
|
+
import { isWeb, Text, View, XStack, YStack } from "tamagui";
|
|
6
|
+
import { useFieldDescribedBy } from "../../fieldLayout";
|
|
7
|
+
import { formCommonColors, formInputColors } from "../../shared/colorRamps";
|
|
8
|
+
import { choiceItemA11y } from "../../shared/nativeA11y";
|
|
9
|
+
|
|
10
|
+
export interface InlineChoice {
|
|
11
|
+
value: string;
|
|
12
|
+
label: string;
|
|
13
|
+
icon?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface InlineChoicesProps {
|
|
17
|
+
options: InlineChoice[];
|
|
18
|
+
value?: string;
|
|
19
|
+
onValueChange: (value: string) => void;
|
|
20
|
+
onBlur?: (...args: any[]) => void;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
/** The field id: FieldLayout's label points at it. */
|
|
23
|
+
id?: string;
|
|
24
|
+
labelledBy?: string;
|
|
25
|
+
"aria-label"?: string;
|
|
26
|
+
knobProps: any;
|
|
27
|
+
/** The LC-05 selected mark, already resolved to a paint. */
|
|
28
|
+
markColor: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const nextKeys = new Set(["ArrowDown", "ArrowRight"]);
|
|
32
|
+
const previousKeys = new Set(["ArrowUp", "ArrowLeft"]);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A short Select inside a sheet laid out in place (LC-262 DIRECT-ACTIONS): one
|
|
36
|
+
* grouped list of radio rows, each with its glyph and label, the chosen row
|
|
37
|
+
* marked by the accent check alone. No trigger and no popover. On the web it
|
|
38
|
+
* is one tab stop, arrows move and choose, Space and Enter choose (APG radio
|
|
39
|
+
* group).
|
|
40
|
+
*/
|
|
41
|
+
export function InlineChoices({
|
|
42
|
+
options,
|
|
43
|
+
value,
|
|
44
|
+
onValueChange,
|
|
45
|
+
onBlur,
|
|
46
|
+
disabled,
|
|
47
|
+
id,
|
|
48
|
+
labelledBy,
|
|
49
|
+
"aria-label": ariaLabel,
|
|
50
|
+
knobProps,
|
|
51
|
+
markColor,
|
|
52
|
+
}: InlineChoicesProps) {
|
|
53
|
+
const describedBy = useFieldDescribedBy();
|
|
54
|
+
const rowsRef = useRef<Array<HTMLElement | null>>([]);
|
|
55
|
+
const textColor = knobProps.textAccentColor;
|
|
56
|
+
const selectedIndex = options.findIndex((option) => option.value === value);
|
|
57
|
+
const tabStop = selectedIndex >= 0 ? selectedIndex : 0;
|
|
58
|
+
|
|
59
|
+
const choose = (index: number) => {
|
|
60
|
+
const option = options[index];
|
|
61
|
+
if (disabled || !option || option.value === value) return;
|
|
62
|
+
onValueChange(option.value);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const onKeyDown = (index: number) => (event: KeyboardEvent) => {
|
|
66
|
+
const step = nextKeys.has(event.key) ? 1 : previousKeys.has(event.key) ? -1 : 0;
|
|
67
|
+
if (step) {
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
const next = (index + step + options.length) % options.length;
|
|
70
|
+
rowsRef.current[next]?.focus();
|
|
71
|
+
choose(next);
|
|
72
|
+
} else if (event.key === " " || event.key === "Enter") {
|
|
73
|
+
event.preventDefault();
|
|
74
|
+
choose(index);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const rowContent = (option: InlineChoice, index: number, selected: boolean) => (
|
|
79
|
+
<XStack
|
|
80
|
+
alignItems="center"
|
|
81
|
+
height={knobProps.menuRow.minHeight}
|
|
82
|
+
paddingHorizontal={knobProps.panelPadding.padding}
|
|
83
|
+
{...knobProps.gap}
|
|
84
|
+
{...(index === 0 ? undefined : hairline.top)}
|
|
85
|
+
borderColor={formCommonColors.divider}
|
|
86
|
+
pointerEvents="none"
|
|
87
|
+
>
|
|
88
|
+
<View flexShrink={0} alignItems="center" justifyContent="center" minWidth={18}>
|
|
89
|
+
<IconSlot icon={option.icon} color={textColor} />
|
|
90
|
+
</View>
|
|
91
|
+
<Text
|
|
92
|
+
fontFamily={knobProps.body.fontFamily}
|
|
93
|
+
fontWeight={knobProps.body.fontWeight}
|
|
94
|
+
color={textColor}
|
|
95
|
+
numberOfLines={1}
|
|
96
|
+
flex={1}
|
|
97
|
+
>
|
|
98
|
+
{option.label}
|
|
99
|
+
</Text>
|
|
100
|
+
{selected ? <CheckIcon size={14} weight="bold" color={markColor} /> : null}
|
|
101
|
+
</XStack>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<YStack
|
|
106
|
+
id={id}
|
|
107
|
+
role="radiogroup"
|
|
108
|
+
aria-labelledby={labelledBy}
|
|
109
|
+
aria-label={labelledBy ? undefined : ariaLabel}
|
|
110
|
+
aria-describedby={describedBy}
|
|
111
|
+
aria-disabled={disabled || undefined}
|
|
112
|
+
data-testid="select-inline-choices"
|
|
113
|
+
{...knobProps.borderRadius}
|
|
114
|
+
borderColor={formInputColors.border.base}
|
|
115
|
+
backgroundColor={formInputColors.background.base}
|
|
116
|
+
overflow="hidden"
|
|
117
|
+
width="100%"
|
|
118
|
+
>
|
|
119
|
+
{options.map((option, index) => {
|
|
120
|
+
const selected = option.value === value;
|
|
121
|
+
if (isWeb) {
|
|
122
|
+
return (
|
|
123
|
+
<View
|
|
124
|
+
key={option.value}
|
|
125
|
+
ref={(node: any) => {
|
|
126
|
+
rowsRef.current[index] = node;
|
|
127
|
+
}}
|
|
128
|
+
role="radio"
|
|
129
|
+
aria-checked={selected}
|
|
130
|
+
aria-disabled={disabled || undefined}
|
|
131
|
+
tabIndex={!disabled && index === tabStop ? 0 : -1}
|
|
132
|
+
cursor={disabled ? "default" : "pointer"}
|
|
133
|
+
onPress={() => choose(index)}
|
|
134
|
+
onKeyDown={onKeyDown(index)}
|
|
135
|
+
onBlur={onBlur}
|
|
136
|
+
outlineWidth={0}
|
|
137
|
+
focusVisibleStyle={keyboardFocusRingProps}
|
|
138
|
+
>
|
|
139
|
+
{rowContent(option, index, selected)}
|
|
140
|
+
</View>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return (
|
|
144
|
+
<Pressable
|
|
145
|
+
key={option.value}
|
|
146
|
+
{...choiceItemA11y({ name: option.label, selected, disabled: !!disabled })}
|
|
147
|
+
disabled={disabled}
|
|
148
|
+
onPress={() => choose(index)}
|
|
149
|
+
>
|
|
150
|
+
{rowContent(option, index, selected)}
|
|
151
|
+
</Pressable>
|
|
152
|
+
);
|
|
153
|
+
})}
|
|
154
|
+
</YStack>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import { useTouchSurface } from "@multiplatform.one/theme";
|
|
1
|
+
import { IconSlot, useTouchSurface } from "@multiplatform.one/theme";
|
|
2
2
|
import type { KeyboardEvent, MouseEvent, ReactNode, RefObject, UIEvent } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
useSyncExternalStore,
|
|
11
|
+
memo,
|
|
12
|
+
} from "react";
|
|
4
13
|
import { CaretDownIcon, CheckIcon } from "@phosphor-icons/react";
|
|
5
14
|
import {
|
|
6
15
|
useClick,
|
|
@@ -54,13 +63,16 @@ import {
|
|
|
54
63
|
panelViewportPadding,
|
|
55
64
|
useViewportGtSm,
|
|
56
65
|
} from "../../FloatingPanel";
|
|
66
|
+
import { pickerCompactFace, resolveCompactFace } from "../../FloatingPanel/compactFace";
|
|
57
67
|
import { PanelPortal } from "../../FloatingPanel/PanelPortal";
|
|
58
|
-
import { useOverlayZIndex } from "../../shared/overlayStack";
|
|
68
|
+
import { useInsideOverlay, useOverlayZIndex } from "../../shared/overlayStack";
|
|
59
69
|
import { useDragScroll } from "../../shared/useDragScroll";
|
|
60
70
|
import { useIsInTableCell } from "../../shared/tableCellContext";
|
|
61
71
|
import { t } from "../../shared/t";
|
|
62
72
|
import { orderOptionsBySelected, type SelectedOrder } from "../selectedOrder";
|
|
63
73
|
import { shouldUseIosNativeSelect } from "../nativeSelect";
|
|
74
|
+
import { InlineChoices } from "./InlineChoices";
|
|
75
|
+
import { shouldInlineChoices } from "./shouldInlineChoices";
|
|
64
76
|
|
|
65
77
|
/** Undefined on web (RNW ships no ActionSheetIOS); real on iOS. */
|
|
66
78
|
const actionSheetIOS = (ReactNative as { ActionSheetIOS?: typeof ReactNative.ActionSheetIOS })
|
|
@@ -73,6 +85,12 @@ export interface SelectOption {
|
|
|
73
85
|
label: string;
|
|
74
86
|
/** Optional group label for grouping options */
|
|
75
87
|
group?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Leading glyph on the option's row (LC-112: every menu row carries one when
|
|
90
|
+
* the options have them). Inside a sheet, five or fewer options that all
|
|
91
|
+
* carry one render inline as an icon choice list (LC-262).
|
|
92
|
+
*/
|
|
93
|
+
icon?: ReactNode;
|
|
76
94
|
}
|
|
77
95
|
|
|
78
96
|
export interface SelectProps {
|
|
@@ -150,8 +168,9 @@ const SelectRow = styled(View, {
|
|
|
150
168
|
// Do not put outlineWidth here — Tamagui maps these onto mouse :focus and
|
|
151
169
|
// they beat instance ring props. Rows paint the ring via selectItemRingProps.
|
|
152
170
|
variants: {
|
|
171
|
+
// LC-262: the accent check alone marks the chosen row, never a grey fill.
|
|
153
172
|
selected: {
|
|
154
|
-
true: { backgroundColor:
|
|
173
|
+
true: { backgroundColor: "transparent", borderRadius: 0 },
|
|
155
174
|
false: { backgroundColor: "transparent" },
|
|
156
175
|
},
|
|
157
176
|
active: {
|
|
@@ -164,6 +183,15 @@ const SelectRow = styled(View, {
|
|
|
164
183
|
} as const,
|
|
165
184
|
});
|
|
166
185
|
|
|
186
|
+
/** An option's leading glyph, in the row's ink. */
|
|
187
|
+
function OptionGlyph({ icon, color }: { icon: ReactNode; color?: string }) {
|
|
188
|
+
return (
|
|
189
|
+
<View flexShrink={0} alignItems="center" justifyContent="center" minWidth={18}>
|
|
190
|
+
<IconSlot icon={icon} color={color} />
|
|
191
|
+
</View>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
167
195
|
function GroupHeader(props: HouseTextProps) {
|
|
168
196
|
return (
|
|
169
197
|
<HouseText
|
|
@@ -432,7 +460,9 @@ const SelectItem = memo(function SelectItem({
|
|
|
432
460
|
role="option"
|
|
433
461
|
tabIndex={-1}
|
|
434
462
|
selected={isSelected}
|
|
435
|
-
|
|
463
|
+
// The open-time cursor sits on the chosen row; it only paints while the
|
|
464
|
+
// keyboard drives it, so the chosen row never opens grey (LC-262).
|
|
465
|
+
active={Boolean(isActive) && ctx.kbNav}
|
|
436
466
|
aria-selected={isSelected}
|
|
437
467
|
data-state={isSelected ? "active" : "inactive"}
|
|
438
468
|
height={ctx.knobProps.menuRow.minHeight}
|
|
@@ -446,22 +476,15 @@ const SelectItem = memo(function SelectItem({
|
|
|
446
476
|
{...(suppressHover
|
|
447
477
|
? {
|
|
448
478
|
hoverStyle: {
|
|
449
|
-
backgroundColor: isActive
|
|
450
|
-
? formInputColors.background.focus
|
|
451
|
-
: isSelected
|
|
452
|
-
? formInputColors.background.base
|
|
453
|
-
: "transparent",
|
|
479
|
+
backgroundColor: isActive ? formInputColors.background.focus : "transparent",
|
|
454
480
|
},
|
|
455
481
|
}
|
|
456
482
|
: {
|
|
457
|
-
hoverStyle: {
|
|
458
|
-
backgroundColor: isSelected
|
|
459
|
-
? formInputColors.background.base
|
|
460
|
-
: formInputColors.background.focus,
|
|
461
|
-
},
|
|
483
|
+
hoverStyle: { backgroundColor: formInputColors.background.focus },
|
|
462
484
|
})}
|
|
463
485
|
{...transitionProps(ctx.knobProps.motion.control)}
|
|
464
486
|
>
|
|
487
|
+
{option.icon ? <OptionGlyph icon={option.icon} color={ctx.textColor} /> : null}
|
|
465
488
|
<Text
|
|
466
489
|
fontFamily={ctx.knobProps.body.fontFamily}
|
|
467
490
|
fontWeight={ctx.knobProps.body.fontWeight}
|
|
@@ -1247,6 +1270,9 @@ function NativeSelectCore({
|
|
|
1247
1270
|
dismissible,
|
|
1248
1271
|
native,
|
|
1249
1272
|
}: FloatingSelectCoreProps) {
|
|
1273
|
+
const insideOverlay = useInsideOverlay();
|
|
1274
|
+
const compactFace = pickerCompactFace(options.length);
|
|
1275
|
+
const asMenu = resolveCompactFace(compactFace, insideOverlay) === "menu";
|
|
1250
1276
|
const hydrationTouch = useTouchSurface();
|
|
1251
1277
|
const describedBy = useFieldDescribedBy();
|
|
1252
1278
|
const fieldA11y = useFieldA11y();
|
|
@@ -1422,30 +1448,16 @@ function NativeSelectCore({
|
|
|
1422
1448
|
{...(isWeb ? selectItemRingProps(active, kbNav) : undefined)}
|
|
1423
1449
|
hoverStyle={
|
|
1424
1450
|
activeIndex != null
|
|
1425
|
-
? {
|
|
1426
|
-
|
|
1427
|
-
? formInputColors.background.focus
|
|
1428
|
-
: selected
|
|
1429
|
-
? formInputColors.background.base
|
|
1430
|
-
: "transparent",
|
|
1431
|
-
}
|
|
1432
|
-
: {
|
|
1433
|
-
backgroundColor: selected
|
|
1434
|
-
? formInputColors.background.base
|
|
1435
|
-
: formInputColors.background.focus,
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
backgroundColor={
|
|
1439
|
-
active
|
|
1440
|
-
? formInputColors.background.focus
|
|
1441
|
-
: selected
|
|
1442
|
-
? formInputColors.background.base
|
|
1443
|
-
: "transparent"
|
|
1451
|
+
? { backgroundColor: active ? formInputColors.background.focus : "transparent" }
|
|
1452
|
+
: { backgroundColor: formInputColors.background.focus }
|
|
1444
1453
|
}
|
|
1454
|
+
// LC-262: the accent check alone marks the chosen row.
|
|
1455
|
+
backgroundColor={active && kbNav ? formInputColors.background.focus : "transparent"}
|
|
1445
1456
|
pressStyle={{ backgroundColor: formInputColors.background.focus }}
|
|
1446
1457
|
// Native: parent RNPressable owns the hit; disable child stealing.
|
|
1447
1458
|
pointerEvents={isWeb ? undefined : "none"}
|
|
1448
1459
|
>
|
|
1460
|
+
{option.icon ? <OptionGlyph icon={option.icon} color={textColor} /> : null}
|
|
1449
1461
|
<Text
|
|
1450
1462
|
fontFamily={knobProps.body.fontFamily}
|
|
1451
1463
|
fontWeight={knobProps.body.fontWeight}
|
|
@@ -1481,7 +1493,9 @@ function NativeSelectCore({
|
|
|
1481
1493
|
: placeholder
|
|
1482
1494
|
: (selectedOption?.label ?? placeholder);
|
|
1483
1495
|
|
|
1484
|
-
|
|
1496
|
+
// The iOS action sheet is a sheet too: a short list, or any list inside an
|
|
1497
|
+
// overlay, opens as a menu over the trigger instead (FZ-7830).
|
|
1498
|
+
if (!asMenu && shouldUseIosNativeSelect({ native, multiple, isWeb, os: Platform.OS })) {
|
|
1485
1499
|
const openNative = () => {
|
|
1486
1500
|
if (disabled) return;
|
|
1487
1501
|
const labels = options.map((o) => o.label);
|
|
@@ -1556,6 +1570,9 @@ function NativeSelectCore({
|
|
|
1556
1570
|
if (!val) onBlur?.();
|
|
1557
1571
|
}}
|
|
1558
1572
|
sheet
|
|
1573
|
+
// A short list opens as a menu over its trigger; a long one keeps its
|
|
1574
|
+
// sheet at page level and opens as a menu inside another overlay (FZ-7830).
|
|
1575
|
+
compactFace={compactFace}
|
|
1559
1576
|
triggerA11y={{
|
|
1560
1577
|
label: ariaLabel ?? fieldA11y?.label,
|
|
1561
1578
|
value: triggerValueText,
|
|
@@ -1676,6 +1693,10 @@ function NativeSelectCore({
|
|
|
1676
1693
|
|
|
1677
1694
|
// ── Select (public API) ──────────────────────────────────────
|
|
1678
1695
|
|
|
1696
|
+
const subscribeNothing = () => () => {};
|
|
1697
|
+
const clientSnapshot = () => true;
|
|
1698
|
+
const serverSnapshot = () => false;
|
|
1699
|
+
|
|
1679
1700
|
export function Select({
|
|
1680
1701
|
name,
|
|
1681
1702
|
form: formProp,
|
|
@@ -1725,18 +1746,32 @@ export function Select({
|
|
|
1725
1746
|
const isControlled = value !== undefined;
|
|
1726
1747
|
const hasLabel = Boolean(label);
|
|
1727
1748
|
|
|
1728
|
-
// DG-CTRL-01 / DG-CTRL-05 — short single-select lists prefer RadioGroup.
|
|
1729
|
-
warnSelectTooFewOptions({
|
|
1730
|
-
count: options?.length ?? 0,
|
|
1731
|
-
multiple,
|
|
1732
|
-
component: "Select",
|
|
1733
|
-
id,
|
|
1734
|
-
});
|
|
1735
1749
|
const viewportGtSm = useViewportGtSm();
|
|
1736
|
-
|
|
1737
|
-
//
|
|
1750
|
+
const insideOverlay = useInsideOverlay();
|
|
1751
|
+
// Sheet core on native and below `$sm` (viewport < 640, OVERLAY_BREAKPOINT).
|
|
1752
|
+
// Floating from `$sm` up, and on a phone-width web for a short list or
|
|
1753
|
+
// inside another overlay, where a sheet would stack on a sheet (FZ-7830).
|
|
1754
|
+
// Native keeps the sheet core, whose FloatingPanel opens the same menu.
|
|
1755
|
+
// SSR and the hydration render keep the sheet core, as the viewport does.
|
|
1738
1756
|
const effectiveGtSm = typeof window !== "undefined" ? viewportGtSm : false;
|
|
1739
|
-
const
|
|
1757
|
+
const hydrated = useSyncExternalStore(subscribeNothing, clientSnapshot, serverSnapshot);
|
|
1758
|
+
const compactMenu =
|
|
1759
|
+
hydrated &&
|
|
1760
|
+
resolveCompactFace(pickerCompactFace(options?.length ?? 0), insideOverlay) === "menu";
|
|
1761
|
+
const shouldUseSheet = !isWeb || (!effectiveGtSm && !compactMenu);
|
|
1762
|
+
const inTableCell = useIsInTableCell();
|
|
1763
|
+
const inline = !inTableCell && shouldInlineChoices({ insideOverlay, multiple, options });
|
|
1764
|
+
// DG-CTRL-01 / DG-CTRL-05 — short single-select lists prefer RadioGroup,
|
|
1765
|
+
// which the inline face already is.
|
|
1766
|
+
if (!inline) {
|
|
1767
|
+
warnSelectTooFewOptions({
|
|
1768
|
+
count: options?.length ?? 0,
|
|
1769
|
+
multiple,
|
|
1770
|
+
component: "Select",
|
|
1771
|
+
id,
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
const markColor = useSelectedMarkColor();
|
|
1740
1775
|
|
|
1741
1776
|
// Render skeleton placeholder
|
|
1742
1777
|
if (skeleton) {
|
|
@@ -1801,6 +1836,26 @@ export function Select({
|
|
|
1801
1836
|
handleBlur?: (...args: any[]) => void,
|
|
1802
1837
|
) => {
|
|
1803
1838
|
if (readOnly) return renderReadOnly(selectValue);
|
|
1839
|
+
if (inline) {
|
|
1840
|
+
return (
|
|
1841
|
+
<InlineChoices
|
|
1842
|
+
options={options}
|
|
1843
|
+
value={typeof selectValue === "string" ? selectValue : undefined}
|
|
1844
|
+
onValueChange={(val) => {
|
|
1845
|
+
if (disabled) return;
|
|
1846
|
+
handleValueChange?.(val);
|
|
1847
|
+
emitValueChange(val);
|
|
1848
|
+
}}
|
|
1849
|
+
onBlur={handleBlur}
|
|
1850
|
+
disabled={disabled}
|
|
1851
|
+
id={id}
|
|
1852
|
+
labelledBy={hasLabel ? `${id}-label` : undefined}
|
|
1853
|
+
aria-label={ariaLabel}
|
|
1854
|
+
knobProps={knobProps}
|
|
1855
|
+
markColor={markColor}
|
|
1856
|
+
/>
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1804
1859
|
const SelectCore = shouldUseSheet ? NativeSelectCore : FloatingSelectCore;
|
|
1805
1860
|
return (
|
|
1806
1861
|
<SelectCore
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { INLINE_CHOICES_MAX, shouldInlineChoices } from "./shouldInlineChoices";
|
|
3
|
+
|
|
4
|
+
const glyph = "•";
|
|
5
|
+
const withIcons = (n: number) => Array.from({ length: n }, () => ({ icon: glyph }));
|
|
6
|
+
|
|
7
|
+
describe("shouldInlineChoices (LC-262, FZ-7830)", () => {
|
|
8
|
+
it("lays out five or fewer glyphed options inline inside an overlay", () => {
|
|
9
|
+
expect(INLINE_CHOICES_MAX).toBe(5);
|
|
10
|
+
expect(shouldInlineChoices({ insideOverlay: true, options: withIcons(2) })).toBe(true);
|
|
11
|
+
expect(shouldInlineChoices({ insideOverlay: true, options: withIcons(5) })).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("keeps the trigger past five, without a glyph on every option, at page level, or for multiple", () => {
|
|
15
|
+
expect(shouldInlineChoices({ insideOverlay: true, options: withIcons(6) })).toBe(false);
|
|
16
|
+
expect(shouldInlineChoices({ insideOverlay: true, options: [...withIcons(3), {}] })).toBe(
|
|
17
|
+
false,
|
|
18
|
+
);
|
|
19
|
+
expect(shouldInlineChoices({ insideOverlay: false, options: withIcons(3) })).toBe(false);
|
|
20
|
+
expect(
|
|
21
|
+
shouldInlineChoices({ insideOverlay: true, multiple: true, options: withIcons(3) }),
|
|
22
|
+
).toBe(false);
|
|
23
|
+
expect(shouldInlineChoices({ insideOverlay: true, options: [] })).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/** The most options a Select inside a sheet lays out inline (LC-262). */
|
|
4
|
+
export const INLINE_CHOICES_MAX = 5;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A single-choice Select inside a sheet, dialog or popover with five or fewer
|
|
8
|
+
* options that all carry a glyph renders inline as an icon choice list: no
|
|
9
|
+
* trigger and no popover, the choices sit in the sheet (LC-262, FZ-7830).
|
|
10
|
+
*/
|
|
11
|
+
export function shouldInlineChoices({
|
|
12
|
+
insideOverlay,
|
|
13
|
+
multiple,
|
|
14
|
+
options,
|
|
15
|
+
}: {
|
|
16
|
+
insideOverlay: boolean;
|
|
17
|
+
multiple?: boolean;
|
|
18
|
+
options: ReadonlyArray<{ icon?: ReactNode }> | undefined;
|
|
19
|
+
}): boolean {
|
|
20
|
+
if (!insideOverlay || multiple || !options?.length) return false;
|
|
21
|
+
return options.length <= INLINE_CHOICES_MAX && options.every((option) => option.icon != null);
|
|
22
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
2
|
+
import { useForm } from "@tanstack/react-form";
|
|
3
|
+
import { cleanup, fireEvent } from "@testing-library/react";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { Form } from "../../Form";
|
|
6
|
+
import { Switch } from "./index";
|
|
7
|
+
import { muteBubbleInputs } from "../../../tests/muteBubbleInputs";
|
|
8
|
+
|
|
9
|
+
vi.mock("tamagui", async (importOriginal) => ({
|
|
10
|
+
...(await importOriginal<typeof import("tamagui")>()),
|
|
11
|
+
isWeb: false,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
afterEach(cleanup);
|
|
15
|
+
|
|
16
|
+
function FormSwitch({ onChange }: { onChange: (checked: boolean) => void }) {
|
|
17
|
+
const form = useForm({ defaultValues: { alerts: false } });
|
|
18
|
+
return (
|
|
19
|
+
<Form form={form}>
|
|
20
|
+
<Switch name="alerts" label="Alerts" onChange={onChange} />
|
|
21
|
+
</Form>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// FZ-7849. Same contract as RadioGroup's row press: the row and the label flip
|
|
26
|
+
// the switch directly on native instead of through tamagui's focusable
|
|
27
|
+
// registry, and each press flips it exactly once.
|
|
28
|
+
describe("native Switch row press", () => {
|
|
29
|
+
muteBubbleInputs();
|
|
30
|
+
|
|
31
|
+
const rowOf = (label: HTMLElement) => label.parentElement as HTMLElement;
|
|
32
|
+
|
|
33
|
+
it("flips once from the row and once from the label, standalone", () => {
|
|
34
|
+
const onChange = vi.fn();
|
|
35
|
+
const result = renderWithProviders(<Switch label="Dark mode" onChange={onChange} />);
|
|
36
|
+
const label = result.getByText("Dark mode");
|
|
37
|
+
expect(label.hasAttribute("for")).toBe(false);
|
|
38
|
+
fireEvent.click(rowOf(label));
|
|
39
|
+
expect(onChange).toHaveBeenCalledExactlyOnceWith(true);
|
|
40
|
+
fireEvent.click(label);
|
|
41
|
+
expect(onChange).toHaveBeenCalledTimes(2);
|
|
42
|
+
expect(onChange).toHaveBeenLastCalledWith(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("flips a form field once from its label", () => {
|
|
46
|
+
const onChange = vi.fn();
|
|
47
|
+
const result = renderWithProviders(<FormSwitch onChange={onChange} />);
|
|
48
|
+
fireEvent.click(result.getByText("Alerts"));
|
|
49
|
+
expect(onChange).toHaveBeenCalledExactlyOnceWith(true);
|
|
50
|
+
fireEvent.click(rowOf(result.getByText("Alerts")));
|
|
51
|
+
expect(onChange).toHaveBeenLastCalledWith(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("leaves a disabled or read-only switch's row and label dead", () => {
|
|
55
|
+
const onChange = vi.fn();
|
|
56
|
+
const result = renderWithProviders(
|
|
57
|
+
<>
|
|
58
|
+
<Switch label="Off limits" onChange={onChange} disabled />
|
|
59
|
+
<Switch label="Look only" onChange={onChange} readOnly />
|
|
60
|
+
</>,
|
|
61
|
+
);
|
|
62
|
+
for (const text of ["Off limits", "Look only"]) {
|
|
63
|
+
const label = result.getByText(text);
|
|
64
|
+
fireEvent.click(rowOf(label));
|
|
65
|
+
fireEvent.click(label);
|
|
66
|
+
}
|
|
67
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
});
|