@multiplatform.one/forms 7.5.0 → 7.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/Button/index.cjs +6 -10
- package/dist/cjs/Button/index.native.js +6 -10
- package/dist/cjs/Button/index.native.js.map +1 -1
- package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.cjs +0 -1
- package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.native.js +0 -1
- package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
- package/dist/cjs/FieldDisplay/index.cjs +2 -1
- package/dist/cjs/FieldDisplay/index.native.js +9 -2
- package/dist/cjs/FieldDisplay/index.native.js.map +1 -1
- package/dist/cjs/FloatingPanel/index.cjs +8 -1
- package/dist/cjs/InputParts/index.cjs +64 -2
- package/dist/cjs/InputParts/index.native.js +70 -4
- package/dist/cjs/InputParts/index.native.js.map +1 -1
- package/dist/cjs/fields/Barcode/BarcodePreview.cjs +15 -9
- package/dist/cjs/fields/Barcode/BarcodePreview.native.js +15 -9
- package/dist/cjs/fields/Barcode/BarcodePreview.native.js.map +1 -1
- package/dist/cjs/fields/Combobox/index.cjs +14 -11
- package/dist/cjs/fields/Combobox/index.native.js +16 -11
- package/dist/cjs/fields/Combobox/index.native.js.map +1 -1
- package/dist/cjs/fields/FileUpload/index.cjs +66 -5
- package/dist/cjs/fields/FileUpload/index.native.js +85 -5
- package/dist/cjs/fields/FileUpload/index.native.js.map +1 -1
- package/dist/cjs/fields/Geolocation/geoValue.cjs +122 -0
- package/dist/cjs/fields/Geolocation/geoValue.native.js +151 -0
- package/dist/cjs/fields/Geolocation/geoValue.native.js.map +1 -0
- package/dist/cjs/fields/Geolocation/index.cjs +16 -29
- package/dist/cjs/fields/Geolocation/index.native.js +16 -29
- package/dist/cjs/fields/Geolocation/index.native.js.map +1 -1
- package/dist/cjs/fields/Icon/index.cjs +1 -0
- package/dist/cjs/fields/Icon/index.native.js +1 -0
- package/dist/cjs/fields/Icon/index.native.js.map +1 -1
- package/dist/cjs/fields/Input/index.cjs +175 -95
- package/dist/cjs/fields/Input/index.native.js +83 -4
- package/dist/cjs/fields/Input/index.native.js.map +1 -1
- package/dist/cjs/fields/MentionInput/index.cjs +6 -1
- package/dist/cjs/fields/MentionInput/index.native.js +6 -1
- package/dist/cjs/fields/MentionInput/index.native.js.map +1 -1
- package/dist/cjs/fields/OTPInput/index.cjs +7 -2
- package/dist/cjs/fields/OTPInput/index.native.js +7 -2
- package/dist/cjs/fields/OTPInput/index.native.js.map +1 -1
- package/dist/cjs/fields/PhoneInput/index.native.js +1 -0
- package/dist/cjs/fields/PhoneInput/index.native.js.map +1 -1
- package/dist/cjs/fields/SearchInput/index.native.js.map +1 -1
- package/dist/cjs/shared/utils.cjs +4 -14
- package/dist/cjs/shared/utils.native.js +6 -14
- package/dist/cjs/shared/utils.native.js.map +1 -1
- package/dist/esm/Button/index.mjs +6 -9
- package/dist/esm/Button/index.mjs.map +1 -1
- package/dist/esm/Button/index.native.js +6 -9
- package/dist/esm/Button/index.native.js.map +1 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.mjs +0 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.mjs.map +1 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.native.js +0 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
- package/dist/esm/FieldDisplay/index.mjs +2 -1
- package/dist/esm/FieldDisplay/index.mjs.map +1 -1
- package/dist/esm/FieldDisplay/index.native.js +9 -2
- package/dist/esm/FieldDisplay/index.native.js.map +1 -1
- package/dist/esm/FloatingPanel/index.mjs +8 -1
- package/dist/esm/FloatingPanel/index.mjs.map +1 -1
- package/dist/esm/InputParts/index.mjs +64 -2
- package/dist/esm/InputParts/index.mjs.map +1 -1
- package/dist/esm/InputParts/index.native.js +70 -4
- package/dist/esm/InputParts/index.native.js.map +1 -1
- package/dist/esm/fields/Barcode/BarcodePreview.mjs +15 -9
- package/dist/esm/fields/Barcode/BarcodePreview.mjs.map +1 -1
- package/dist/esm/fields/Barcode/BarcodePreview.native.js +15 -9
- package/dist/esm/fields/Barcode/BarcodePreview.native.js.map +1 -1
- package/dist/esm/fields/Combobox/index.mjs +14 -11
- package/dist/esm/fields/Combobox/index.mjs.map +1 -1
- package/dist/esm/fields/Combobox/index.native.js +16 -11
- package/dist/esm/fields/Combobox/index.native.js.map +1 -1
- package/dist/esm/fields/FileUpload/index.mjs +66 -5
- package/dist/esm/fields/FileUpload/index.mjs.map +1 -1
- package/dist/esm/fields/FileUpload/index.native.js +85 -5
- package/dist/esm/fields/FileUpload/index.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/geoValue.mjs +95 -0
- package/dist/esm/fields/Geolocation/geoValue.mjs.map +1 -0
- package/dist/esm/fields/Geolocation/geoValue.native.js +121 -0
- package/dist/esm/fields/Geolocation/geoValue.native.js.map +1 -0
- package/dist/esm/fields/Geolocation/index.mjs +15 -28
- package/dist/esm/fields/Geolocation/index.mjs.map +1 -1
- package/dist/esm/fields/Geolocation/index.native.js +15 -28
- package/dist/esm/fields/Geolocation/index.native.js.map +1 -1
- package/dist/esm/fields/Icon/index.mjs +1 -0
- package/dist/esm/fields/Icon/index.mjs.map +1 -1
- package/dist/esm/fields/Icon/index.native.js +1 -0
- package/dist/esm/fields/Icon/index.native.js.map +1 -1
- package/dist/esm/fields/Input/index.mjs +177 -98
- package/dist/esm/fields/Input/index.mjs.map +1 -1
- package/dist/esm/fields/Input/index.native.js +85 -7
- package/dist/esm/fields/Input/index.native.js.map +1 -1
- package/dist/esm/fields/MentionInput/index.mjs +6 -1
- package/dist/esm/fields/MentionInput/index.mjs.map +1 -1
- package/dist/esm/fields/MentionInput/index.native.js +6 -1
- package/dist/esm/fields/MentionInput/index.native.js.map +1 -1
- package/dist/esm/fields/OTPInput/index.mjs +7 -3
- package/dist/esm/fields/OTPInput/index.mjs.map +1 -1
- package/dist/esm/fields/OTPInput/index.native.js +7 -3
- package/dist/esm/fields/OTPInput/index.native.js.map +1 -1
- package/dist/esm/fields/PhoneInput/index.mjs.map +1 -1
- package/dist/esm/fields/PhoneInput/index.native.js +1 -0
- package/dist/esm/fields/PhoneInput/index.native.js.map +1 -1
- package/dist/esm/fields/SearchInput/index.mjs.map +1 -1
- package/dist/esm/fields/SearchInput/index.native.js.map +1 -1
- package/dist/esm/shared/utils.mjs +4 -14
- package/dist/esm/shared/utils.mjs.map +1 -1
- package/dist/esm/shared/utils.native.js +6 -14
- package/dist/esm/shared/utils.native.js.map +1 -1
- package/dist/jsx/Button/index.mjs +6 -9
- package/dist/jsx/Button/index.mjs.map +1 -1
- package/dist/jsx/Button/index.native.js +6 -10
- package/dist/jsx/Button/index.native.js.map +1 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.mjs +0 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.mjs.map +1 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.native.js +0 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
- package/dist/jsx/FieldDisplay/index.mjs +2 -1
- package/dist/jsx/FieldDisplay/index.mjs.map +1 -1
- package/dist/jsx/FieldDisplay/index.native.js +9 -2
- package/dist/jsx/FieldDisplay/index.native.js.map +1 -1
- package/dist/jsx/FloatingPanel/index.mjs +8 -1
- package/dist/jsx/FloatingPanel/index.mjs.map +1 -1
- package/dist/jsx/InputParts/index.mjs +64 -2
- package/dist/jsx/InputParts/index.mjs.map +1 -1
- package/dist/jsx/InputParts/index.native.js +70 -4
- package/dist/jsx/InputParts/index.native.js.map +1 -1
- package/dist/jsx/fields/Barcode/BarcodePreview.mjs +15 -9
- package/dist/jsx/fields/Barcode/BarcodePreview.mjs.map +1 -1
- package/dist/jsx/fields/Barcode/BarcodePreview.native.js +15 -9
- package/dist/jsx/fields/Barcode/BarcodePreview.native.js.map +1 -1
- package/dist/jsx/fields/Combobox/index.mjs +14 -11
- package/dist/jsx/fields/Combobox/index.mjs.map +1 -1
- package/dist/jsx/fields/Combobox/index.native.js +16 -11
- package/dist/jsx/fields/Combobox/index.native.js.map +1 -1
- package/dist/jsx/fields/FileUpload/index.mjs +66 -5
- package/dist/jsx/fields/FileUpload/index.mjs.map +1 -1
- package/dist/jsx/fields/FileUpload/index.native.js +85 -5
- package/dist/jsx/fields/FileUpload/index.native.js.map +1 -1
- package/dist/jsx/fields/Geolocation/geoValue.mjs +95 -0
- package/dist/jsx/fields/Geolocation/geoValue.mjs.map +1 -0
- package/dist/jsx/fields/Geolocation/geoValue.native.js +151 -0
- package/dist/jsx/fields/Geolocation/geoValue.native.js.map +1 -0
- package/dist/jsx/fields/Geolocation/index.mjs +15 -28
- package/dist/jsx/fields/Geolocation/index.mjs.map +1 -1
- package/dist/jsx/fields/Geolocation/index.native.js +16 -29
- package/dist/jsx/fields/Geolocation/index.native.js.map +1 -1
- package/dist/jsx/fields/Icon/index.mjs +1 -0
- package/dist/jsx/fields/Icon/index.mjs.map +1 -1
- package/dist/jsx/fields/Icon/index.native.js +1 -0
- package/dist/jsx/fields/Icon/index.native.js.map +1 -1
- package/dist/jsx/fields/Input/index.mjs +177 -98
- package/dist/jsx/fields/Input/index.mjs.map +1 -1
- package/dist/jsx/fields/Input/index.native.js +83 -4
- package/dist/jsx/fields/Input/index.native.js.map +1 -1
- package/dist/jsx/fields/MentionInput/index.mjs +6 -1
- package/dist/jsx/fields/MentionInput/index.mjs.map +1 -1
- package/dist/jsx/fields/MentionInput/index.native.js +6 -1
- package/dist/jsx/fields/MentionInput/index.native.js.map +1 -1
- package/dist/jsx/fields/OTPInput/index.mjs +7 -3
- package/dist/jsx/fields/OTPInput/index.mjs.map +1 -1
- package/dist/jsx/fields/OTPInput/index.native.js +7 -2
- package/dist/jsx/fields/OTPInput/index.native.js.map +1 -1
- package/dist/jsx/fields/PhoneInput/index.mjs.map +1 -1
- package/dist/jsx/fields/PhoneInput/index.native.js +1 -0
- package/dist/jsx/fields/PhoneInput/index.native.js.map +1 -1
- package/dist/jsx/fields/SearchInput/index.mjs.map +1 -1
- package/dist/jsx/fields/SearchInput/index.native.js.map +1 -1
- package/dist/jsx/shared/utils.mjs +4 -14
- package/dist/jsx/shared/utils.mjs.map +1 -1
- package/dist/jsx/shared/utils.native.js +6 -14
- package/dist/jsx/shared/utils.native.js.map +1 -1
- package/package.json +7 -7
- package/src/Button/Button.density.spec.tsx +118 -32
- package/src/Button/Button.labelInk.spec.tsx +72 -19
- package/src/Button/Button.lc71.spec.tsx +10 -3
- package/src/Button/Button.stories.tsx +97 -7
- package/src/Button/index.tsx +22 -12
- package/src/ContextualSaveBar/UnsavedChangesDialog.tsx +0 -1
- package/src/FieldDisplay/FieldDisplay.spec.tsx +11 -2
- package/src/FieldDisplay/index.tsx +5 -0
- package/src/FloatingPanel/index.tsx +22 -1
- package/src/FloatingPanel/panelWidthContract.spec.ts +1 -0
- package/src/InputParts/index.tsx +100 -3
- package/src/fields/Barcode/BarcodePreview.spec.tsx +153 -5
- package/src/fields/Barcode/BarcodePreview.stories.tsx +104 -0
- package/src/fields/Barcode/BarcodePreview.tsx +47 -15
- package/src/fields/ColorPicker/PresetSwatches.spec.tsx +71 -0
- package/src/fields/ColorPicker/PresetSwatches.stories.tsx +94 -0
- package/src/fields/Combobox/Combobox.spec.tsx +64 -1
- package/src/fields/Combobox/index.tsx +22 -11
- package/src/fields/FileUpload/FileUpload.spec.tsx +71 -0
- package/src/fields/FileUpload/index.tsx +100 -3
- package/src/fields/Geolocation/Geolocation.spec.tsx +126 -0
- package/src/fields/Geolocation/geoValue.spec.ts +169 -0
- package/src/fields/Geolocation/geoValue.ts +149 -0
- package/src/fields/Geolocation/index.tsx +15 -24
- package/src/fields/Icon/Icon.stories.tsx +1 -1
- package/src/fields/Icon/index.tsx +1 -0
- package/src/fields/Input/index.tsx +180 -81
- package/src/fields/MentionInput/index.tsx +9 -1
- package/src/fields/OTPInput/OtpInput.spec.tsx +36 -4
- package/src/fields/OTPInput/index.tsx +18 -3
- package/src/fields/OTPInput/otpCellHeightTouch.spec.ts +44 -0
- package/src/fields/PhoneInput/index.tsx +1 -0
- package/src/fields/SearchInput/index.tsx +5 -3
- package/src/shared/utils.spec.ts +46 -0
- package/src/shared/utils.ts +31 -16
- package/types/Button/index.d.ts +8 -3
- package/types/Button/index.d.ts.map +1 -1
- package/types/ContextualSaveBar/UnsavedChangesDialog.d.ts.map +1 -1
- package/types/FieldDisplay/index.d.ts.map +1 -1
- package/types/InputParts/index.d.ts +9 -5
- package/types/InputParts/index.d.ts.map +1 -1
- package/types/fields/Barcode/BarcodePreview.d.ts +20 -9
- package/types/fields/Barcode/BarcodePreview.d.ts.map +1 -1
- package/types/fields/Checkbox/index.d.ts +3 -3
- package/types/fields/Combobox/index.d.ts.map +1 -1
- package/types/fields/FileUpload/index.d.ts.map +1 -1
- package/types/fields/Geolocation/geoValue.d.ts +45 -0
- package/types/fields/Geolocation/geoValue.d.ts.map +1 -0
- package/types/fields/Geolocation/index.d.ts.map +1 -1
- package/types/fields/Icon/index.d.ts.map +1 -1
- package/types/fields/Input/index.d.ts +18 -6
- package/types/fields/Input/index.d.ts.map +1 -1
- package/types/fields/MentionInput/index.d.ts.map +1 -1
- package/types/fields/OTPInput/index.d.ts +12 -0
- package/types/fields/OTPInput/index.d.ts.map +1 -1
- package/types/fields/PhoneInput/index.d.ts.map +1 -1
- package/types/fields/RadioGroup/index.d.ts +2 -2
- package/types/fields/SearchInput/index.d.ts.map +1 -1
- package/types/fields/ToggleGroup/index.d.ts +1 -1
- package/types/shared/utils.d.ts.map +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PresetSwatches spec (MPO-117). The row is a radiogroup of swatches.
|
|
3
|
+
* ColorPicker owns the popover; this file only covers the exported grid.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { fireEvent, screen } from "@testing-library/react";
|
|
7
|
+
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
8
|
+
import { describe, expect, it, vi } from "vitest";
|
|
9
|
+
import { PresetSwatches } from "./parts";
|
|
10
|
+
|
|
11
|
+
vi.mock("@tamagui/lucide-icons-2", () => ({
|
|
12
|
+
Palette: () => null,
|
|
13
|
+
X: () => null,
|
|
14
|
+
Check: () => null,
|
|
15
|
+
Pipette: () => null,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const BRAND = ["#1E40AF", "#3B82F6", "#BFDBFE"];
|
|
19
|
+
|
|
20
|
+
describe("PresetSwatches", () => {
|
|
21
|
+
it("renders a radiogroup of radios for each preset", () => {
|
|
22
|
+
renderWithProviders(
|
|
23
|
+
<PresetSwatches presetColors={BRAND} selectedColor="#3B82F6" onSelect={vi.fn()} />,
|
|
24
|
+
);
|
|
25
|
+
expect(screen.getByRole("radiogroup", { name: "Color presets" })).toBeTruthy();
|
|
26
|
+
const radios = screen.getAllByRole("radio");
|
|
27
|
+
expect(radios).toHaveLength(BRAND.length);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("marks the selected hex aria-checked", () => {
|
|
31
|
+
renderWithProviders(
|
|
32
|
+
<PresetSwatches presetColors={BRAND} selectedColor="#3B82F6" onSelect={vi.fn()} />,
|
|
33
|
+
);
|
|
34
|
+
const selected = screen.getByRole("radio", { name: "Color #3B82F6" });
|
|
35
|
+
expect(selected.getAttribute("aria-checked")).toBe("true");
|
|
36
|
+
const other = screen.getByRole("radio", { name: "Color #1E40AF" });
|
|
37
|
+
expect(other.getAttribute("aria-checked")).toBe("false");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("onSelect fires with the hex of the pressed swatch", () => {
|
|
41
|
+
const onSelect = vi.fn();
|
|
42
|
+
renderWithProviders(
|
|
43
|
+
<PresetSwatches presetColors={BRAND} selectedColor={null} onSelect={onSelect} />,
|
|
44
|
+
);
|
|
45
|
+
fireEvent.click(screen.getByRole("radio", { name: "Color #1E40AF" }));
|
|
46
|
+
expect(onSelect).toHaveBeenCalledWith("#1E40AF");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("empty presetColors renders nothing", () => {
|
|
50
|
+
const { container } = renderWithProviders(
|
|
51
|
+
<PresetSwatches presetColors={[]} selectedColor={null} onSelect={vi.fn()} />,
|
|
52
|
+
);
|
|
53
|
+
expect(container.querySelector('[role="radiogroup"]')).toBeNull();
|
|
54
|
+
expect(container.textContent).toBe("");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("null entry is the no-color radio and reports null", () => {
|
|
58
|
+
const onSelect = vi.fn();
|
|
59
|
+
renderWithProviders(
|
|
60
|
+
<PresetSwatches
|
|
61
|
+
presetColors={[{ name: "None", value: null }, "#3B82F6"]}
|
|
62
|
+
selectedColor={null}
|
|
63
|
+
onSelect={onSelect}
|
|
64
|
+
/>,
|
|
65
|
+
);
|
|
66
|
+
const none = screen.getByRole("radio", { name: "None" });
|
|
67
|
+
expect(none.getAttribute("aria-checked")).toBe("true");
|
|
68
|
+
fireEvent.click(screen.getByRole("radio", { name: "Color #3B82F6" }));
|
|
69
|
+
expect(onSelect).toHaveBeenCalledWith("#3B82F6");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PresetSwatches — the color-preset row ColorPicker embeds. Exported from
|
|
3
|
+
* the forms barrel, previously invisible in Kitchen Sink because it had no
|
|
4
|
+
* story of its own.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { action } from "@multiplatform.one/storybook";
|
|
8
|
+
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
|
|
9
|
+
import { useState } from "react";
|
|
10
|
+
import { Text, YStack } from "tamagui";
|
|
11
|
+
import { PresetSwatches } from "./parts";
|
|
12
|
+
|
|
13
|
+
const BRAND = ["#1E40AF", "#1D4ED8", "#2563EB", "#3B82F6", "#60A5FA", "#93C5FD", "#BFDBFE"];
|
|
14
|
+
|
|
15
|
+
const meta: Meta<typeof PresetSwatches> = {
|
|
16
|
+
title: "Forms/PresetSwatches",
|
|
17
|
+
component: PresetSwatches,
|
|
18
|
+
parameters: {
|
|
19
|
+
status: { type: "stable" },
|
|
20
|
+
docs: {
|
|
21
|
+
description: {
|
|
22
|
+
component:
|
|
23
|
+
"Radiogroup of preset color swatches. Named entries, hex strings, and a null 'no color' chip.",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default meta;
|
|
30
|
+
type Story = StoryObj<typeof PresetSwatches>;
|
|
31
|
+
|
|
32
|
+
export const Main: Story = {
|
|
33
|
+
name: "Main",
|
|
34
|
+
render: () => {
|
|
35
|
+
const Example = () => {
|
|
36
|
+
const [selected, setSelected] = useState<string | null>(BRAND[3]);
|
|
37
|
+
return (
|
|
38
|
+
<YStack gap="$3" padding="$4" width={280}>
|
|
39
|
+
<PresetSwatches
|
|
40
|
+
presetColors={BRAND}
|
|
41
|
+
selectedColor={selected}
|
|
42
|
+
onSelect={(next) => {
|
|
43
|
+
setSelected(next);
|
|
44
|
+
action("onSelect")(next);
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
<Text fontSize="$2" color="$color10">
|
|
48
|
+
{selected ?? "none"}
|
|
49
|
+
</Text>
|
|
50
|
+
</YStack>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
return <Example />;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const Selected: Story = {
|
|
58
|
+
render: () => (
|
|
59
|
+
<YStack padding="$4" width={280}>
|
|
60
|
+
<PresetSwatches presetColors={BRAND} selectedColor="#3B82F6" onSelect={action("onSelect")} />
|
|
61
|
+
</YStack>
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Null entry is the "no color" chip with the diagonal strike. */
|
|
66
|
+
export const NoColor: Story = {
|
|
67
|
+
render: () => {
|
|
68
|
+
const Example = () => {
|
|
69
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
70
|
+
return (
|
|
71
|
+
<YStack gap="$3" padding="$4" width={280}>
|
|
72
|
+
<PresetSwatches
|
|
73
|
+
presetColors={[{ name: "None", value: null }, ...BRAND]}
|
|
74
|
+
selectedColor={selected}
|
|
75
|
+
onSelect={setSelected}
|
|
76
|
+
/>
|
|
77
|
+
</YStack>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
return <Example />;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** Empty list paints nothing; the host owns the empty marker. */
|
|
85
|
+
export const Empty: Story = {
|
|
86
|
+
render: () => (
|
|
87
|
+
<YStack padding="$4">
|
|
88
|
+
<Text fontSize="$2" color="$color10">
|
|
89
|
+
empty presetColors — row is null
|
|
90
|
+
</Text>
|
|
91
|
+
<PresetSwatches presetColors={[]} selectedColor={null} onSelect={action("onSelect")} />
|
|
92
|
+
</YStack>
|
|
93
|
+
),
|
|
94
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { act, fireEvent } from "@testing-library/react";
|
|
2
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
4
4
|
import { Form } from "../../Form";
|
|
5
5
|
import { Combobox } from "./index";
|
|
@@ -313,5 +313,68 @@ describe("Combobox", () => {
|
|
|
313
313
|
expect(activeOption(result.container)?.textContent).toContain("Carrot");
|
|
314
314
|
expect(activeOption(result.container)?.getAttribute("role")).toBe("option");
|
|
315
315
|
});
|
|
316
|
+
|
|
317
|
+
// The sheet core (<= OVERLAY_BREAKPOINT, and every native render) used to
|
|
318
|
+
// build headers itself from source order, so interleaved groups emitted a
|
|
319
|
+
// REPEAT header and the rows stayed unclustered — two different lists from
|
|
320
|
+
// one set of props. Both cores now go through groupComboboxOptions.
|
|
321
|
+
it("the sheet renders the same headers and row order as the desktop panel", async () => {
|
|
322
|
+
function shot(root: HTMLElement) {
|
|
323
|
+
return {
|
|
324
|
+
headers: [...root.querySelectorAll("[data-combobox-group-header]")].map(
|
|
325
|
+
(el) => el.textContent,
|
|
326
|
+
),
|
|
327
|
+
rows: [...root.querySelectorAll('[role="option"]')].map((el) => ({
|
|
328
|
+
label: el.textContent,
|
|
329
|
+
index: el.getAttribute("data-combobox-index"),
|
|
330
|
+
})),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
window.innerWidth = 1200;
|
|
335
|
+
const desktop = renderWithProviders(
|
|
336
|
+
<Combobox label="Produce" name="produce" options={groupedOptions} multiple open />,
|
|
337
|
+
);
|
|
338
|
+
await act(async () => {});
|
|
339
|
+
const onDesktop = shot(document.body);
|
|
340
|
+
desktop.unmount();
|
|
341
|
+
|
|
342
|
+
window.innerWidth = 500;
|
|
343
|
+
renderWithProviders(
|
|
344
|
+
<Combobox label="Produce" name="produce" options={groupedOptions} multiple open />,
|
|
345
|
+
);
|
|
346
|
+
await act(async () => {});
|
|
347
|
+
const onSheet = shot(document.body);
|
|
348
|
+
|
|
349
|
+
expect(onDesktop.headers).toEqual(["Fruit", "Vegetable"]);
|
|
350
|
+
expect(onSheet.headers).toEqual(onDesktop.headers);
|
|
351
|
+
expect(onSheet.rows).toEqual(onDesktop.rows);
|
|
352
|
+
// One header per group: a repeated label is the defect this guards.
|
|
353
|
+
expect(new Set(onSheet.headers).size).toBe(onSheet.headers.length);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// The sheet's activeIndex indexes the same grouped list the sheet renders,
|
|
357
|
+
// so arrows land on the row the user sees rather than the source-order row.
|
|
358
|
+
it("sheet ArrowDown follows the rendered row order, not source order", async () => {
|
|
359
|
+
window.innerWidth = 500;
|
|
360
|
+
renderWithProviders(
|
|
361
|
+
<Combobox label="Produce" name="produce" options={groupedOptions} multiple open />,
|
|
362
|
+
);
|
|
363
|
+
await act(async () => {});
|
|
364
|
+
|
|
365
|
+
const rows = [...document.body.querySelectorAll('[role="option"]')];
|
|
366
|
+
expect(rows.map((el) => el.textContent?.trim())).toEqual(["Apple", "Banana", "Carrot"]);
|
|
367
|
+
|
|
368
|
+
const search = document.body.querySelector("input");
|
|
369
|
+
expect(search).toBeTruthy();
|
|
370
|
+
// activeIndex starts at 0 (Apple); one ArrowDown must highlight the row
|
|
371
|
+
// rendered second (Banana), which is sortedFiltered[2], not [1].
|
|
372
|
+
await act(async () => {
|
|
373
|
+
fireEvent.keyDown(search!, { key: "ArrowDown" });
|
|
374
|
+
});
|
|
375
|
+
const active = document.body.querySelector('[data-active="true"]');
|
|
376
|
+
expect(active?.textContent).toContain("Banana");
|
|
377
|
+
expect(active?.getAttribute("data-combobox-index")).toBe("1");
|
|
378
|
+
});
|
|
316
379
|
});
|
|
317
380
|
});
|
|
@@ -1918,6 +1918,17 @@ function NativeComboboxCore({
|
|
|
1918
1918
|
return orderOptionsBySelected(filtered, current, selectedOrder);
|
|
1919
1919
|
}, [filtered, value, filter, selectedOrder]);
|
|
1920
1920
|
|
|
1921
|
+
// Same grouping the floating core uses (`groupComboboxOptions`, the mechanism
|
|
1922
|
+
// mpo-floating-measured.md §2.3 names as ours): rows cluster by group so each
|
|
1923
|
+
// group gets exactly ONE header. Rendering source order here emitted a repeat
|
|
1924
|
+
// header for interleaved groups and disagreed with the desktop panel for
|
|
1925
|
+
// identical props. `activeIndex` indexes `navigableOptions`, which IS the
|
|
1926
|
+
// render order, so arrows still land on the row the user sees (MPO-38 AC-6).
|
|
1927
|
+
const { flatOptions: navigableOptions } = useMemo(
|
|
1928
|
+
() => groupComboboxOptions(sortedFiltered),
|
|
1929
|
+
[sortedFiltered],
|
|
1930
|
+
);
|
|
1931
|
+
|
|
1921
1932
|
const searchCleanupRef = useRef<(() => void) | null>(null);
|
|
1922
1933
|
useEffect(() => {
|
|
1923
1934
|
if (!onSearch || !open) return;
|
|
@@ -1942,8 +1953,8 @@ function NativeComboboxCore({
|
|
|
1942
1953
|
}
|
|
1943
1954
|
}, [open]);
|
|
1944
1955
|
|
|
1945
|
-
const totalItems =
|
|
1946
|
-
const createOptionIndex = showCreateOption ?
|
|
1956
|
+
const totalItems = navigableOptions.length + (showCreateOption ? 1 : 0);
|
|
1957
|
+
const createOptionIndex = showCreateOption ? navigableOptions.length : -1;
|
|
1947
1958
|
const listboxId = comboboxListboxId(id);
|
|
1948
1959
|
const activeDescendant =
|
|
1949
1960
|
open && activeIndex != null && activeIndex >= 0 ? comboboxOptionId(id, activeIndex) : undefined;
|
|
@@ -2028,8 +2039,8 @@ function NativeComboboxCore({
|
|
|
2028
2039
|
e.preventDefault();
|
|
2029
2040
|
if (activeIndex === createOptionIndex && showCreateOption) {
|
|
2030
2041
|
handleCreate();
|
|
2031
|
-
} else if (activeIndex != null && activeIndex >= 0 &&
|
|
2032
|
-
handleSelect(
|
|
2042
|
+
} else if (activeIndex != null && activeIndex >= 0 && navigableOptions[activeIndex]) {
|
|
2043
|
+
handleSelect(navigableOptions[activeIndex]);
|
|
2033
2044
|
if (e.key === "Enter" || !multiple) setOpen(false);
|
|
2034
2045
|
}
|
|
2035
2046
|
}
|
|
@@ -2039,7 +2050,7 @@ function NativeComboboxCore({
|
|
|
2039
2050
|
createOptionIndex,
|
|
2040
2051
|
showCreateOption,
|
|
2041
2052
|
activeIndex,
|
|
2042
|
-
|
|
2053
|
+
navigableOptions,
|
|
2043
2054
|
handleCreate,
|
|
2044
2055
|
handleSelect,
|
|
2045
2056
|
multiple,
|
|
@@ -2066,8 +2077,8 @@ function NativeComboboxCore({
|
|
|
2066
2077
|
e.stopPropagation();
|
|
2067
2078
|
if (activeIndex === createOptionIndex && showCreateOption) {
|
|
2068
2079
|
handleCreate();
|
|
2069
|
-
} else if (activeIndex != null && activeIndex >= 0 &&
|
|
2070
|
-
handleSelect(
|
|
2080
|
+
} else if (activeIndex != null && activeIndex >= 0 && navigableOptions[activeIndex]) {
|
|
2081
|
+
handleSelect(navigableOptions[activeIndex]);
|
|
2071
2082
|
if (e.key === "Enter" || !multiple) setOpen(false);
|
|
2072
2083
|
} else {
|
|
2073
2084
|
setOpen(false);
|
|
@@ -2081,7 +2092,7 @@ function NativeComboboxCore({
|
|
|
2081
2092
|
activeIndex,
|
|
2082
2093
|
createOptionIndex,
|
|
2083
2094
|
showCreateOption,
|
|
2084
|
-
|
|
2095
|
+
navigableOptions,
|
|
2085
2096
|
handleCreate,
|
|
2086
2097
|
handleSelect,
|
|
2087
2098
|
multiple,
|
|
@@ -2138,12 +2149,12 @@ function NativeComboboxCore({
|
|
|
2138
2149
|
{searchError}
|
|
2139
2150
|
</Text>
|
|
2140
2151
|
</View>
|
|
2141
|
-
) :
|
|
2152
|
+
) : navigableOptions.length > 0 ? (
|
|
2142
2153
|
<>
|
|
2143
|
-
{
|
|
2154
|
+
{navigableOptions.map((option, i) => {
|
|
2144
2155
|
const selected = isSelectedFn(option.value);
|
|
2145
2156
|
const active = i === activeIndex;
|
|
2146
|
-
const prevGroup = i > 0 ?
|
|
2157
|
+
const prevGroup = i > 0 ? navigableOptions[i - 1].group : undefined;
|
|
2147
2158
|
const showGroupHeader = option.group && option.group !== prevGroup;
|
|
2148
2159
|
const optionRow = (
|
|
2149
2160
|
<View
|
|
@@ -134,6 +134,34 @@ describe("FileUpload", () => {
|
|
|
134
134
|
expect(dropzone?.getAttribute("data-kb-ring")).toBe("true");
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
+
// MPO-57 regression. `data-kb-ring="true"` proved the STATE, never the paint.
|
|
138
|
+
// The frame's LC-71 kill is a `:focus-visible` rule; a keyboard-origin focus
|
|
139
|
+
// matches `:focus-visible` too, so the base-state ring atom lost on
|
|
140
|
+
// specificity and the browser computed outline-width 0px while every
|
|
141
|
+
// assertion passed. Measured on the Storybook harness, not read off source.
|
|
142
|
+
// The ring must therefore also win INSIDE the focus-visible layer.
|
|
143
|
+
it("beats its own LC-71 focus-visible kill — the ring wins in the layer that suppresses it", () => {
|
|
144
|
+
const { container } = renderWithHouseThemes(
|
|
145
|
+
<FileUpload label="Attachment" name="attachment" />,
|
|
146
|
+
);
|
|
147
|
+
const zone = getZone(container);
|
|
148
|
+
fireEvent.keyDown(document.body, { key: "Tab", code: "Tab" });
|
|
149
|
+
fireEvent.focus(zone);
|
|
150
|
+
expect(zone.getAttribute("data-kb-ring")).toBe("true");
|
|
151
|
+
|
|
152
|
+
// Base-state ring (2px solid $outlineColor at offset 0).
|
|
153
|
+
expect(zone.className).toMatch(/_outlineWidth-2px/);
|
|
154
|
+
expect(zone.className).toMatch(/_outlineStyle-solid/);
|
|
155
|
+
expect(zone.className).toMatch(/_outlineOffset-0px/);
|
|
156
|
+
|
|
157
|
+
// …restated in the focus-visible layer, so the kill cannot outrank it.
|
|
158
|
+
expect(zone.className).toMatch(/_outlineWidth-0focus-visible-2px/);
|
|
159
|
+
expect(zone.className).toMatch(/_outlineStyle-0focus-visible-solid/);
|
|
160
|
+
// …and the zeroing atoms are gone from that layer while the ring is up.
|
|
161
|
+
expect(zone.className).not.toMatch(/_outlineWidth-0focus-visible-0px/);
|
|
162
|
+
expect(zone.className).not.toMatch(/_outlineStyle-0focus-visible-none/);
|
|
163
|
+
});
|
|
164
|
+
|
|
137
165
|
it("keeps Choose File out of the tab order so the dropzone is the one stop", () => {
|
|
138
166
|
const result = renderWithProviders(
|
|
139
167
|
<FileUpload label="Attachment" name="attachment" buttonText="Pick file" />,
|
|
@@ -319,6 +347,49 @@ describe("FileUpload bento-derived dropzone (MPO-57)", () => {
|
|
|
319
347
|
const thumb = flist.querySelector("img") as HTMLImageElement;
|
|
320
348
|
expect(thumb).toBeTruthy();
|
|
321
349
|
expect(thumb.getAttribute("alt")).toBe("diagram.png");
|
|
350
|
+
|
|
351
|
+
// AC-4 image clause (Bento Image Picker): an image pick is a TILE in the
|
|
352
|
+
// in-zone grid, not a row — the thumb IS the tile, and the remove disc
|
|
353
|
+
// rides its top-right corner (absolute, negative inset) instead of
|
|
354
|
+
// trailing on a row. Same 32/hitSlop-44 disc the rows carry.
|
|
355
|
+
const grid = flist.querySelector('[data-file-upload="tiles"]') as HTMLElement;
|
|
356
|
+
expect(grid).toBeTruthy();
|
|
357
|
+
const tile = grid.querySelector('[data-file-upload="tile"]') as HTMLElement;
|
|
358
|
+
expect(tile).toBeTruthy();
|
|
359
|
+
expect(tile.contains(thumb)).toBe(true);
|
|
360
|
+
const remove = tile.querySelector('[aria-label="Remove diagram.png"]') as HTMLElement;
|
|
361
|
+
expect(remove).toBeTruthy();
|
|
362
|
+
expect(remove.getAttribute("data-nested-px")).toBe("32");
|
|
363
|
+
expect(remove.getAttribute("data-mp-press-slop")).toBe("6");
|
|
364
|
+
const corner = tile.querySelector('[data-file-upload="tile-remove"]') as HTMLElement;
|
|
365
|
+
expect(corner).toBeTruthy();
|
|
366
|
+
expect(corner.contains(remove)).toBe(true);
|
|
367
|
+
expect(corner.className).toMatch(/_pos-absolute/);
|
|
368
|
+
// No size line inside a tile: Bento's populated Image Picker is thumbs only.
|
|
369
|
+
expect(tile.textContent ?? "").not.toMatch(/Bytes|KB|MB/);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("keeps non-image picks as rows while images tile — one list, two treatments", async () => {
|
|
373
|
+
if (typeof URL.createObjectURL !== "function") {
|
|
374
|
+
(URL as any).createObjectURL = () => "blob:mock";
|
|
375
|
+
(URL as any).revokeObjectURL = () => {};
|
|
376
|
+
}
|
|
377
|
+
const { container } = renderWithHouseThemes(
|
|
378
|
+
<FileUpload label="Attachment" name="attachment" multiple showPreview />,
|
|
379
|
+
);
|
|
380
|
+
await pickFile(container, new File(["fake-png-bytes"], "diagram.png", { type: "image/png" }));
|
|
381
|
+
await pickFile(container, new File(["hello"], "notes.txt", { type: "text/plain" }));
|
|
382
|
+
|
|
383
|
+
const zone = getZone(container);
|
|
384
|
+
const flist = zone.querySelector('[data-file-upload="files"]') as HTMLElement;
|
|
385
|
+
const tiles = flist.querySelectorAll('[data-file-upload="tile"]');
|
|
386
|
+
expect(tiles.length).toBe(1);
|
|
387
|
+
// The text file is a row with its bold size line and trailing disc, outside the grid.
|
|
388
|
+
const grid = flist.querySelector('[data-file-upload="tiles"]') as HTMLElement;
|
|
389
|
+
const rowRemove = flist.querySelector('[aria-label="Remove notes.txt"]') as HTMLElement;
|
|
390
|
+
expect(rowRemove).toBeTruthy();
|
|
391
|
+
expect(grid.contains(rowRemove)).toBe(false);
|
|
392
|
+
expect(flist.textContent).toContain("notes.txt");
|
|
322
393
|
});
|
|
323
394
|
});
|
|
324
395
|
|
|
@@ -133,6 +133,22 @@ const UploadAreaFrame = styled(YStack, {
|
|
|
133
133
|
focusVisibleStyle: { outlineWidth: 0, outlineStyle: "none" as any },
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* The house ring, applied so it survives the frame's own LC-71 kill.
|
|
138
|
+
*
|
|
139
|
+
* The kill above is what suppresses the 1px UA outline on a POINTER focus,
|
|
140
|
+
* and it has to stay. But it is a `:focus-visible` rule, and a keyboard-origin
|
|
141
|
+
* focus matches `:focus-visible` too — so spreading the ring as base props
|
|
142
|
+
* only loses on specificity and the zone paints no ring at all. Restating the
|
|
143
|
+
* same fragment in `focusStyle`/`focusVisibleStyle` beats the kill in exactly
|
|
144
|
+
* the state that should carry a ring, and leaves pointer focus alone (the
|
|
145
|
+
* caller only spreads this while `kbZoneFocus` is true).
|
|
146
|
+
*/
|
|
147
|
+
function withZoneFocusRing() {
|
|
148
|
+
const ring = ensureFocusVisibleRing({ outlineOffset: 0 });
|
|
149
|
+
return { ...ring, focusStyle: ring, focusVisibleStyle: ring };
|
|
150
|
+
}
|
|
151
|
+
|
|
136
152
|
const DropOverlayFrame = styled(YStack, {
|
|
137
153
|
position: "absolute",
|
|
138
154
|
top: 0,
|
|
@@ -158,6 +174,14 @@ const UploadFileListFrame = styled(YStack, {
|
|
|
158
174
|
borderTopColor: formDropzoneColors.border,
|
|
159
175
|
});
|
|
160
176
|
|
|
177
|
+
// BENTO-DERIVED (Image Picker): picked IMAGES populate a tile grid, not the
|
|
178
|
+
// file-row list — each tile is the thumb itself with the remove disc riding
|
|
179
|
+
// its top-right corner. Non-image picks and in-flight transfers keep the row.
|
|
180
|
+
const UploadTileGridFrame = styled(XStack, {
|
|
181
|
+
width: "100%",
|
|
182
|
+
flexWrap: "wrap",
|
|
183
|
+
});
|
|
184
|
+
|
|
161
185
|
export function FileUpload<
|
|
162
186
|
TParentData,
|
|
163
187
|
TName extends DeepKeys<TParentData>,
|
|
@@ -228,6 +252,13 @@ export function FileUpload<
|
|
|
228
252
|
// Intentional: extract the recipe icon step (16 at the 44 control) for the
|
|
229
253
|
// row file glyph — same held "icon 16" exception the CTA carries.
|
|
230
254
|
const rowGlyphSize = knobProps.controlIcon.width;
|
|
255
|
+
// Intentional: the image TILE edge is two control steps (88 at medium) —
|
|
256
|
+
// the same getFieldHeight ladder the 220 frame floor sits on, so tile and
|
|
257
|
+
// frame move together per size. The corner remove disc hangs a quarter of
|
|
258
|
+
// its own nestedControl step (8 at medium) outside the tile's top-right,
|
|
259
|
+
// which is the Bento Image Picker overlap read under the house tokens.
|
|
260
|
+
const tilePx = getFieldHeight(knobProps.sizeToken as SizeTokens, 2);
|
|
261
|
+
const tileRemoveInset = -Math.round(knobProps.nestedControl.px / 4);
|
|
231
262
|
|
|
232
263
|
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
|
233
264
|
const [transfers, setTransfers] = useState<TransferItem[]>([]);
|
|
@@ -722,6 +753,44 @@ export function FileUpload<
|
|
|
722
753
|
</XStack>
|
|
723
754
|
);
|
|
724
755
|
|
|
756
|
+
const renderImageTile = (
|
|
757
|
+
file: File,
|
|
758
|
+
preview: string,
|
|
759
|
+
onValueChange?: (nextValue: TData) => void,
|
|
760
|
+
) => (
|
|
761
|
+
<YStack
|
|
762
|
+
key={`${file.name}-${file.size}-${file.lastModified}`}
|
|
763
|
+
data-file-upload="tile"
|
|
764
|
+
position="relative"
|
|
765
|
+
width={tilePx}
|
|
766
|
+
height={tilePx}
|
|
767
|
+
>
|
|
768
|
+
<Image
|
|
769
|
+
borderRadius={cappedRadius}
|
|
770
|
+
// src (string), not source: the RN source object leaks
|
|
771
|
+
// onto the DOM <img> as source="[object Object]" on web
|
|
772
|
+
src={preview}
|
|
773
|
+
alt={file.name}
|
|
774
|
+
width={tilePx}
|
|
775
|
+
height={tilePx}
|
|
776
|
+
objectFit="cover"
|
|
777
|
+
/>
|
|
778
|
+
{canInteract ? (
|
|
779
|
+
<YStack
|
|
780
|
+
data-file-upload="tile-remove"
|
|
781
|
+
position="absolute"
|
|
782
|
+
top={tileRemoveInset}
|
|
783
|
+
right={tileRemoveInset}
|
|
784
|
+
zIndex={1}
|
|
785
|
+
>
|
|
786
|
+
{renderRemoveDisc(t("Remove {{name}}", { name: file.name }), () =>
|
|
787
|
+
handleFileRemove(file, onValueChange),
|
|
788
|
+
)}
|
|
789
|
+
</YStack>
|
|
790
|
+
) : null}
|
|
791
|
+
</YStack>
|
|
792
|
+
);
|
|
793
|
+
|
|
725
794
|
const renderUploadArea = (
|
|
726
795
|
errorProp?: string | boolean,
|
|
727
796
|
onValueChange?: (nextValue: TData) => void,
|
|
@@ -764,6 +833,15 @@ export function FileUpload<
|
|
|
764
833
|
: ({ role: "button", onPress: requestFiles } as any)
|
|
765
834
|
: { cursor: disabled ? "not-allowed" : undefined };
|
|
766
835
|
|
|
836
|
+
// Committed image picks become tiles; everything else stays a row.
|
|
837
|
+
const imageTiles: { file: File; preview: string }[] = [];
|
|
838
|
+
const fileRows: { file: File; preview: string | null; index: number }[] = [];
|
|
839
|
+
selectedFiles.forEach((file, index) => {
|
|
840
|
+
const preview = showPreview ? getFilePreview(file) : null;
|
|
841
|
+
if (preview) imageTiles.push({ file, preview });
|
|
842
|
+
else fileRows.push({ file, preview, index });
|
|
843
|
+
});
|
|
844
|
+
|
|
767
845
|
return (
|
|
768
846
|
<UploadAreaFrame
|
|
769
847
|
// The dropzone is this field's control: it carries the FieldLayout id
|
|
@@ -796,7 +874,16 @@ export function FileUpload<
|
|
|
796
874
|
// One family ring rule (boards media-01 dz-ring): 2px $outlineColor at
|
|
797
875
|
// offset 0. The old -2 inset was the F1 defect — an element's own
|
|
798
876
|
// outline is never clipped by its own overflow:hidden.
|
|
799
|
-
|
|
877
|
+
//
|
|
878
|
+
// The ring must ALSO be spread into focusStyle/focusVisibleStyle. The
|
|
879
|
+
// base props alone lose: the frame's LC-71 kill is a :focus-visible
|
|
880
|
+
// rule, the browser matches :focus-visible on a keyboard-origin focus,
|
|
881
|
+
// and a pseudo-class rule outranks the plain-class atom — the zone
|
|
882
|
+
// carried `_outlineWidth-2px` AND `_outlineWidth-0focus-visible-0px`
|
|
883
|
+
// and computed outline-width 0px. Measured on the harness, not read off
|
|
884
|
+
// the source: the pre-fix spec asserted the atom's PRESENCE, which was
|
|
885
|
+
// true while nothing painted.
|
|
886
|
+
{...(kbZoneFocus ? withZoneFocusRing() : undefined)}
|
|
800
887
|
hoverStyle={
|
|
801
888
|
canInteract
|
|
802
889
|
? {
|
|
@@ -903,8 +990,18 @@ export function FileUpload<
|
|
|
903
990
|
// The hairline tracks the frame edge's knob width — one dash voice.
|
|
904
991
|
borderTopWidth={knobProps.borderRadius.borderWidth ?? 1}
|
|
905
992
|
>
|
|
906
|
-
{
|
|
907
|
-
|
|
993
|
+
{imageTiles.length > 0 ? (
|
|
994
|
+
<UploadTileGridFrame
|
|
995
|
+
data-file-upload="tiles"
|
|
996
|
+
{...knobProps.gap}
|
|
997
|
+
{...knobProps.panelPadding}
|
|
998
|
+
>
|
|
999
|
+
{imageTiles.map(({ file, preview }) =>
|
|
1000
|
+
renderImageTile(file, preview, onValueChange),
|
|
1001
|
+
)}
|
|
1002
|
+
</UploadTileGridFrame>
|
|
1003
|
+
) : null}
|
|
1004
|
+
{fileRows.map(({ file, preview, index }) => {
|
|
908
1005
|
return (
|
|
909
1006
|
<XStack
|
|
910
1007
|
key={`${file.name}-${index}`}
|