@seed-design/lynx-react 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/Accordion/Accordion.jsx +2 -2
- package/lib/components/ActionButton/ActionButton.jsx +1 -1
- package/lib/components/AlertDialog/AlertDialog.d.ts +64 -0
- package/lib/components/AlertDialog/AlertDialog.d.ts.map +1 -0
- package/lib/components/AlertDialog/AlertDialog.jsx +138 -0
- package/lib/components/AlertDialog/AlertDialog.namespace.d.ts +2 -0
- package/lib/components/AlertDialog/AlertDialog.namespace.d.ts.map +1 -0
- package/lib/components/AlertDialog/AlertDialog.namespace.js +17 -0
- package/lib/components/AlertDialog/index.d.ts +3 -0
- package/lib/components/AlertDialog/index.d.ts.map +1 -0
- package/lib/components/AppBar/AppBar.d.ts +14 -1
- package/lib/components/AppBar/AppBar.d.ts.map +1 -1
- package/lib/components/AppBar/AppBar.jsx +46 -10
- package/lib/components/AppBar/AppBar.namespace.d.ts +1 -1
- package/lib/components/AppBar/AppBar.namespace.d.ts.map +1 -1
- package/lib/components/AppBar/index.d.ts +1 -1
- package/lib/components/AppBar/index.d.ts.map +1 -1
- package/lib/components/AttachmentDisplay/AttachmentDisplay.d.ts +182 -0
- package/lib/components/AttachmentDisplay/AttachmentDisplay.d.ts.map +1 -0
- package/lib/components/AttachmentDisplay/AttachmentDisplay.jsx +444 -0
- package/lib/components/AttachmentDisplay/index.d.ts +2 -0
- package/lib/components/AttachmentDisplay/index.d.ts.map +1 -0
- package/lib/components/AttachmentInput/AttachmentInput.d.ts +179 -0
- package/lib/components/AttachmentInput/AttachmentInput.d.ts.map +1 -0
- package/lib/components/AttachmentInput/AttachmentInput.jsx +520 -0
- package/lib/components/AttachmentInput/index.d.ts +2 -0
- package/lib/components/AttachmentInput/index.d.ts.map +1 -0
- package/lib/components/BottomSheet/BottomSheet.d.ts +1 -1
- package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/lib/components/BottomSheet/BottomSheet.jsx +1 -7
- package/lib/components/Callout/Callout.jsx +2 -2
- package/lib/components/Checkbox/Checkbox.jsx +2 -2
- package/lib/components/Chip/Chip.jsx +2 -2
- package/lib/components/ChipTabs/ChipTabs.d.ts +41 -0
- package/lib/components/ChipTabs/ChipTabs.d.ts.map +1 -0
- package/lib/components/ChipTabs/ChipTabs.jsx +39 -0
- package/lib/components/ChipTabs/ChipTabs.namespace.d.ts +2 -0
- package/lib/components/ChipTabs/ChipTabs.namespace.d.ts.map +1 -0
- package/lib/components/ChipTabs/ChipTabs.namespace.js +13 -0
- package/lib/components/ChipTabs/index.d.ts +3 -0
- package/lib/components/ChipTabs/index.d.ts.map +1 -0
- package/lib/components/ContextualFloatingButton/ContextualFloatingButton.d.ts +18 -0
- package/lib/components/ContextualFloatingButton/ContextualFloatingButton.d.ts.map +1 -0
- package/lib/components/ContextualFloatingButton/ContextualFloatingButton.jsx +113 -0
- package/lib/components/ContextualFloatingButton/index.d.ts +2 -0
- package/lib/components/ContextualFloatingButton/index.d.ts.map +1 -0
- package/lib/components/Dialog/Dialog.d.ts +87 -0
- package/lib/components/Dialog/Dialog.d.ts.map +1 -0
- package/lib/components/Dialog/Dialog.jsx +147 -0
- package/lib/components/Dialog/Dialog.namespace.d.ts +2 -0
- package/lib/components/Dialog/Dialog.namespace.d.ts.map +1 -0
- package/lib/components/Dialog/Dialog.namespace.js +18 -0
- package/lib/components/Dialog/index.d.ts +3 -0
- package/lib/components/Dialog/index.d.ts.map +1 -0
- package/lib/components/FloatingActionButton/FloatingActionButton.d.ts +23 -0
- package/lib/components/FloatingActionButton/FloatingActionButton.d.ts.map +1 -0
- package/lib/components/FloatingActionButton/FloatingActionButton.jsx +76 -0
- package/lib/components/FloatingActionButton/FloatingActionButton.namespace.d.ts +2 -0
- package/lib/components/FloatingActionButton/FloatingActionButton.namespace.d.ts.map +1 -0
- package/lib/components/FloatingActionButton/FloatingActionButton.namespace.js +10 -0
- package/lib/components/FloatingActionButton/index.d.ts +3 -0
- package/lib/components/FloatingActionButton/index.d.ts.map +1 -0
- package/lib/components/HelpBubble/HelpBubble.d.ts +67 -0
- package/lib/components/HelpBubble/HelpBubble.d.ts.map +1 -0
- package/lib/components/HelpBubble/HelpBubble.jsx +600 -0
- package/lib/components/HelpBubble/HelpBubble.namespace.d.ts +2 -0
- package/lib/components/HelpBubble/HelpBubble.namespace.d.ts.map +1 -0
- package/lib/components/HelpBubble/HelpBubble.namespace.js +18 -0
- package/lib/components/HelpBubble/index.d.ts +3 -0
- package/lib/components/HelpBubble/index.d.ts.map +1 -0
- package/lib/components/Icon/Icon.d.ts +1 -0
- package/lib/components/Icon/Icon.d.ts.map +1 -1
- package/lib/components/Icon/Icon.jsx +3 -2
- package/lib/components/IdentityPlaceholder/IdentityPlaceholder.d.ts +28 -0
- package/lib/components/IdentityPlaceholder/IdentityPlaceholder.d.ts.map +1 -0
- package/lib/components/IdentityPlaceholder/IdentityPlaceholder.jsx +37 -0
- package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.d.ts +2 -0
- package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.d.ts.map +1 -0
- package/lib/components/IdentityPlaceholder/IdentityPlaceholder.namespace.js +9 -0
- package/lib/components/IdentityPlaceholder/identity-placeholder-business.js +4 -0
- package/lib/components/IdentityPlaceholder/identity-placeholder-person.js +4 -0
- package/lib/components/IdentityPlaceholder/index.d.ts +3 -0
- package/lib/components/IdentityPlaceholder/index.d.ts.map +1 -0
- package/lib/components/InputButton/InputButton.d.ts +2 -0
- package/lib/components/InputButton/InputButton.d.ts.map +1 -1
- package/lib/components/InputButton/InputButton.jsx +40 -8
- package/lib/components/List/List.d.ts +1 -1
- package/lib/components/List/List.d.ts.map +1 -1
- package/lib/components/List/List.jsx +26 -12
- package/lib/components/Menu/Menu.d.ts.map +1 -1
- package/lib/components/Menu/Menu.jsx +15 -5
- package/lib/components/PageBanner/PageBanner.d.ts +3 -0
- package/lib/components/PageBanner/PageBanner.d.ts.map +1 -1
- package/lib/components/PageBanner/PageBanner.jsx +49 -11
- package/lib/components/QuantityPicker/QuantityPicker.d.ts +53 -0
- package/lib/components/QuantityPicker/QuantityPicker.d.ts.map +1 -0
- package/lib/components/QuantityPicker/QuantityPicker.jsx +282 -0
- package/lib/components/QuantityPicker/QuantityPicker.namespace.d.ts +2 -0
- package/lib/components/QuantityPicker/QuantityPicker.namespace.d.ts.map +1 -0
- package/lib/components/QuantityPicker/QuantityPicker.namespace.js +11 -0
- package/lib/components/QuantityPicker/index.d.ts +3 -0
- package/lib/components/QuantityPicker/index.d.ts.map +1 -0
- package/lib/components/RadioGroup/RadioGroup.jsx +2 -2
- package/lib/components/ReactionButton/ReactionButton.d.ts +1 -1
- package/lib/components/ReactionButton/ReactionButton.d.ts.map +1 -1
- package/lib/components/ReactionButton/ReactionButton.jsx +22 -13
- package/lib/components/ScrollFog/ScrollFog.d.ts +41 -0
- package/lib/components/ScrollFog/ScrollFog.d.ts.map +1 -0
- package/lib/components/ScrollFog/ScrollFog.jsx +65 -0
- package/lib/components/ScrollFog/index.d.ts +2 -0
- package/lib/components/ScrollFog/index.d.ts.map +1 -0
- package/lib/components/SegmentedControl/SegmentedControl.jsx +2 -2
- package/lib/components/Select/Select.d.ts +112 -0
- package/lib/components/Select/Select.d.ts.map +1 -0
- package/lib/components/Select/Select.jsx +885 -0
- package/lib/components/Select/Select.namespace.d.ts +2 -0
- package/lib/components/Select/Select.namespace.d.ts.map +1 -0
- package/lib/components/Select/Select.namespace.js +23 -0
- package/lib/components/Select/index.d.ts +3 -0
- package/lib/components/Select/index.d.ts.map +1 -0
- package/lib/components/Skeleton/Skeleton.d.ts +15 -0
- package/lib/components/Skeleton/Skeleton.d.ts.map +1 -0
- package/lib/components/Skeleton/Skeleton.jsx +18 -0
- package/lib/components/Skeleton/index.d.ts +2 -0
- package/lib/components/Skeleton/index.d.ts.map +1 -0
- package/lib/components/Slider/Slider.d.ts +101 -0
- package/lib/components/Slider/Slider.d.ts.map +1 -0
- package/lib/components/Slider/Slider.jsx +573 -0
- package/lib/components/Slider/Slider.namespace.d.ts +2 -0
- package/lib/components/Slider/Slider.namespace.d.ts.map +1 -0
- package/lib/components/Slider/Slider.namespace.js +19 -0
- package/lib/components/Slider/Slider.utils.d.ts +19 -0
- package/lib/components/Slider/Slider.utils.d.ts.map +1 -0
- package/lib/components/Slider/Slider.utils.js +106 -0
- package/lib/components/Slider/index.d.ts +3 -0
- package/lib/components/Slider/index.d.ts.map +1 -0
- package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.d.ts.map +1 -1
- package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.jsx +22 -8
- package/lib/components/Switch/Switch.d.ts.map +1 -1
- package/lib/components/Switch/Switch.jsx +5 -3
- package/lib/components/Tabs/Tabs.d.ts +34 -2
- package/lib/components/Tabs/Tabs.d.ts.map +1 -1
- package/lib/components/Tabs/Tabs.jsx +186 -56
- package/lib/components/Tabs/Tabs.utils.d.ts +14 -1
- package/lib/components/Tabs/Tabs.utils.d.ts.map +1 -1
- package/lib/components/Tabs/Tabs.utils.js +24 -3
- package/lib/components/Tabs/index.d.ts +1 -1
- package/lib/components/Tabs/index.d.ts.map +1 -1
- package/lib/components/index.d.ts +14 -0
- package/lib/components/index.d.ts.map +1 -1
- package/lib/hooks/useControllableState.d.ts +1 -14
- package/lib/hooks/useControllableState.d.ts.map +1 -1
- package/lib/hooks/usePressTap.d.ts +1 -27
- package/lib/hooks/usePressTap.d.ts.map +1 -1
- package/lib/hooks/useScaleFeedback.d.ts +1 -1
- package/lib/hooks/useScaleFeedback.js +1 -1
- package/lib/index.js +29 -6
- package/lib/{hooks/useControllableState.js → packages/lynx-react-headless/use-controllable-state/lib/index.js} +2 -3
- package/lib/{hooks/usePressTap.js → packages/lynx-react-headless/use-press-tap/lib/index.js} +13 -10
- package/lib/types.d.ts +1 -0
- package/lib/types.d.ts.map +1 -1
- package/package.json +9 -5
- package/src/components/ActionButton/ActionButton.test.tsx +7 -7
- package/src/components/AlertDialog/AlertDialog.namespace.ts +22 -0
- package/src/components/AlertDialog/AlertDialog.test.tsx +226 -0
- package/src/components/AlertDialog/AlertDialog.tsx +374 -0
- package/src/components/AlertDialog/index.ts +24 -0
- package/src/components/AppBar/AppBar.namespace.ts +1 -0
- package/src/components/AppBar/AppBar.test.tsx +257 -0
- package/src/components/AppBar/AppBar.tsx +109 -14
- package/src/components/AppBar/index.ts +1 -0
- package/src/components/AttachmentDisplay/AttachmentDisplay.tsx +886 -0
- package/src/components/AttachmentDisplay/index.ts +48 -0
- package/src/components/AttachmentInput/AttachmentInput.tsx +1001 -0
- package/src/components/AttachmentInput/index.ts +49 -0
- package/src/components/BottomSheet/BottomSheet.test.tsx +22 -7
- package/src/components/BottomSheet/BottomSheet.tsx +1 -19
- package/src/components/ChipTabs/ChipTabs.namespace.ts +14 -0
- package/src/components/ChipTabs/ChipTabs.tsx +119 -0
- package/src/components/ChipTabs/index.ts +16 -0
- package/src/components/ContextualFloatingButton/ContextualFloatingButton.test.tsx +63 -0
- package/src/components/ContextualFloatingButton/ContextualFloatingButton.tsx +232 -0
- package/src/components/ContextualFloatingButton/index.ts +4 -0
- package/src/components/Dialog/Dialog.namespace.ts +24 -0
- package/src/components/Dialog/Dialog.test.tsx +172 -0
- package/src/components/Dialog/Dialog.tsx +373 -0
- package/src/components/Dialog/index.ts +26 -0
- package/src/components/FloatingActionButton/FloatingActionButton.namespace.ts +8 -0
- package/src/components/FloatingActionButton/FloatingActionButton.test.tsx +59 -0
- package/src/components/FloatingActionButton/FloatingActionButton.tsx +207 -0
- package/src/components/FloatingActionButton/index.ts +10 -0
- package/src/components/HelpBubble/HelpBubble.namespace.ts +24 -0
- package/src/components/HelpBubble/HelpBubble.tsx +998 -0
- package/src/components/HelpBubble/index.ts +26 -0
- package/src/components/Icon/Icon.test.tsx +57 -5
- package/src/components/Icon/Icon.tsx +16 -2
- package/src/components/IdentityPlaceholder/IdentityPlaceholder.namespace.ts +6 -0
- package/src/components/IdentityPlaceholder/IdentityPlaceholder.tsx +103 -0
- package/src/components/IdentityPlaceholder/assets.d.ts +4 -0
- package/src/components/IdentityPlaceholder/identity-placeholder-business.webp +0 -0
- package/src/components/IdentityPlaceholder/identity-placeholder-person.webp +0 -0
- package/src/components/IdentityPlaceholder/index.ts +8 -0
- package/src/components/InputButton/InputButton.test.tsx +128 -1
- package/src/components/InputButton/InputButton.tsx +60 -9
- package/src/components/List/List.test.tsx +54 -2
- package/src/components/List/List.tsx +43 -10
- package/src/components/Menu/Menu.tsx +14 -4
- package/src/components/Menu/__tests__/Menu.test.tsx +34 -0
- package/src/components/PageBanner/PageBanner.test.tsx +110 -3
- package/src/components/PageBanner/PageBanner.tsx +83 -19
- package/src/components/QuantityPicker/QuantityPicker.namespace.ts +10 -0
- package/src/components/QuantityPicker/QuantityPicker.test.tsx +235 -0
- package/src/components/QuantityPicker/QuantityPicker.tsx +605 -0
- package/src/components/QuantityPicker/index.ts +14 -0
- package/src/components/ReactionButton/ReactionButton.feedback.test.tsx +50 -0
- package/src/components/ReactionButton/ReactionButton.test.tsx +6 -4
- package/src/components/ReactionButton/ReactionButton.tsx +20 -12
- package/src/components/ScrollFog/ScrollFog.test.tsx +222 -0
- package/src/components/ScrollFog/ScrollFog.tsx +172 -0
- package/src/components/ScrollFog/index.ts +1 -0
- package/src/components/Select/Select.namespace.ts +34 -0
- package/src/components/Select/Select.test.tsx +297 -0
- package/src/components/Select/Select.tsx +1483 -0
- package/src/components/Select/index.ts +39 -0
- package/src/components/Skeleton/Skeleton.test.tsx +64 -0
- package/src/components/Skeleton/Skeleton.tsx +45 -0
- package/src/components/Skeleton/index.ts +1 -0
- package/src/components/Slider/Slider.namespace.ts +26 -0
- package/src/components/Slider/Slider.test.tsx +260 -0
- package/src/components/Slider/Slider.tsx +897 -0
- package/src/components/Slider/Slider.utils.ts +167 -0
- package/src/components/Slider/index.ts +30 -0
- package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.test.tsx +21 -1
- package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.tsx +30 -7
- package/src/components/Switch/Switch.tsx +14 -13
- package/src/components/Tabs/Tabs.test.tsx +218 -1
- package/src/components/Tabs/Tabs.tsx +346 -75
- package/src/components/Tabs/Tabs.utils.ts +51 -1
- package/src/components/Tabs/index.ts +17 -1
- package/src/components/index.ts +14 -0
- package/src/hooks/useControllableState.ts +1 -43
- package/src/hooks/usePressTap.test.ts +23 -11
- package/src/hooks/usePressTap.ts +1 -60
- package/src/hooks/useScaleFeedback.ts +1 -1
- package/src/types.ts +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export {
|
|
2
|
+
HelpBubbleAnchor,
|
|
3
|
+
HelpBubbleArrow,
|
|
4
|
+
HelpBubbleArrowTip,
|
|
5
|
+
HelpBubbleBody,
|
|
6
|
+
HelpBubbleCloseButton,
|
|
7
|
+
HelpBubbleContent,
|
|
8
|
+
HelpBubbleDescription,
|
|
9
|
+
HelpBubblePositioner,
|
|
10
|
+
HelpBubbleRoot,
|
|
11
|
+
HelpBubbleTitle,
|
|
12
|
+
HelpBubbleTrigger,
|
|
13
|
+
type HelpBubbleAnchorProps,
|
|
14
|
+
type HelpBubbleArrowProps,
|
|
15
|
+
type HelpBubbleArrowTipProps,
|
|
16
|
+
type HelpBubbleBodyProps,
|
|
17
|
+
type HelpBubbleCloseButtonProps,
|
|
18
|
+
type HelpBubbleContentProps,
|
|
19
|
+
type HelpBubbleDescriptionProps,
|
|
20
|
+
type HelpBubblePositionerProps,
|
|
21
|
+
type HelpBubbleRootProps,
|
|
22
|
+
type HelpBubbleTitleProps,
|
|
23
|
+
type HelpBubbleTriggerProps,
|
|
24
|
+
} from "./HelpBubble";
|
|
25
|
+
|
|
26
|
+
export * as HelpBubble from "./HelpBubble.namespace";
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
|
|
1
4
|
import * as React from "@lynx-js/react";
|
|
2
5
|
import { act, render, waitSchedule } from "@lynx-js/react/testing-library";
|
|
3
6
|
import type { MainThread } from "@lynx-js/types";
|
|
4
7
|
import { describe, expect, it } from "vitest";
|
|
5
8
|
|
|
6
9
|
import type { LynxIconElementProps } from "../../types";
|
|
7
|
-
import { InternalIcon, PrefixIcon } from "./Icon";
|
|
10
|
+
import { Icon, IconSlotProvider, InternalIcon, PrefixIcon, SuffixIcon } from "./Icon";
|
|
8
11
|
|
|
9
12
|
const TestIcon = React.forwardRef<
|
|
10
13
|
MainThread.Element,
|
|
@@ -94,15 +97,13 @@ describe("InternalIcon", () => {
|
|
|
94
97
|
await waitSchedule();
|
|
95
98
|
|
|
96
99
|
const { image } = getSourceAndImage();
|
|
97
|
-
expect(frames.size).toBe(0);
|
|
98
|
-
expect(image.getAttribute("tint-color")).toBeNull();
|
|
99
100
|
|
|
100
101
|
rerender(renderIcon("checked"));
|
|
101
102
|
await waitSchedule();
|
|
102
103
|
|
|
103
104
|
expect(frames.size).toBe(1);
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
// The immediate read uses the old native style; the next frame must refresh it.
|
|
106
|
+
expect(image.getAttribute("tint-color")).toBe("rgb(134, 139, 148)");
|
|
106
107
|
computedColor = "rgb(255, 102, 0)";
|
|
107
108
|
runNextFrame();
|
|
108
109
|
|
|
@@ -149,3 +150,54 @@ describe("PrefixIcon", () => {
|
|
|
149
150
|
expect(getSourceAndImage().image.getAttribute("tint-color")).toBeNull();
|
|
150
151
|
});
|
|
151
152
|
});
|
|
153
|
+
|
|
154
|
+
describe("icon recipe ownership", () => {
|
|
155
|
+
it.each([
|
|
156
|
+
[Icon, "icon", "seed-icon"],
|
|
157
|
+
[PrefixIcon, "prefixIcon", "seed-prefix-icon"],
|
|
158
|
+
[SuffixIcon, "suffixIcon", "seed-suffix-icon"],
|
|
159
|
+
] as const)("keeps fallback styles exclusive to unbound %s", (Component, slot, baseClass) => {
|
|
160
|
+
const { container, rerender } = render(<Component icon={<TestIcon />} />);
|
|
161
|
+
expect(container.querySelector(`.${baseClass}`)).not.toBeNull();
|
|
162
|
+
rerender(
|
|
163
|
+
<IconSlotProvider value={{ classNames: { [slot]: "test-recipe-icon" }, deps: [] }}>
|
|
164
|
+
<Component size={28} color="#2475e8" className="user-icon" icon={<TestIcon />} />
|
|
165
|
+
</IconSlotProvider>,
|
|
166
|
+
);
|
|
167
|
+
const wrapper = container.querySelector(".test-recipe-icon")!;
|
|
168
|
+
expect(wrapper.classList.contains(`${baseClass}-slot`)).toBe(true);
|
|
169
|
+
expect(wrapper.classList.contains("user-icon")).toBe(true);
|
|
170
|
+
expect(wrapper.classList.contains(baseClass)).toBe(false);
|
|
171
|
+
expect(wrapper.getAttribute("style")).toContain("28px");
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// This checks generated CSS cascade only; native state/theme invalidation is verified separately.
|
|
176
|
+
describe("generated icon CSS import order", () => {
|
|
177
|
+
const { JSDOM } = createRequire(`${process.cwd()}/package.json`)("jsdom") as {
|
|
178
|
+
JSDOM: new (html: string) => { window: Window & typeof globalThis };
|
|
179
|
+
};
|
|
180
|
+
const base = readFileSync("../lynx-css/base.css", "utf8");
|
|
181
|
+
const recipe = readFileSync("../lynx-css/recipes/callout.css", "utf8");
|
|
182
|
+
|
|
183
|
+
it("keeps the legacy standalone fallback selectors", () => {
|
|
184
|
+
const dom = new JSDOM(`<style>${base}</style><div class="seed-suffix-icon"></div>`);
|
|
185
|
+
const style = dom.window.getComputedStyle(dom.window.document.querySelector("div")!);
|
|
186
|
+
expect(style.width).toBe("var(--seed-suffix-icon-size)");
|
|
187
|
+
expect(style.height).toBe("var(--seed-suffix-icon-size)");
|
|
188
|
+
expect(style.color).toBe("var(--seed-suffix-icon-color, currentcolor)");
|
|
189
|
+
dom.window.close();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it.each(["base-first", "recipe-first"])("preserves Callout slot styles with %s", (order) => {
|
|
193
|
+
const dom = new JSDOM(`<style>${order === "base-first" ? base + recipe : recipe + base}</style>
|
|
194
|
+
<div class="seed-suffix-icon-slot seed-callout__suffixIcon seed-callout__suffixIcon--tone_neutral"></div>`);
|
|
195
|
+
const icon = dom.window.document.querySelector("div")!;
|
|
196
|
+
const style = dom.window.getComputedStyle(icon);
|
|
197
|
+
expect(style.width).toBe("var(--seed-dimension-x4)");
|
|
198
|
+
expect(style.height).toBe("var(--seed-dimension-x4)");
|
|
199
|
+
expect(style.color).toBe("var(--seed-color-fg-neutral)");
|
|
200
|
+
expect(style.flexShrink).toBe("0");
|
|
201
|
+
dom.window.close();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -205,7 +205,11 @@ const IconSlotBase = React.forwardRef<unknown, IconSlotBaseProps>((props, ref) =
|
|
|
205
205
|
ref ? { ref: ref as LynxViewRef } : {},
|
|
206
206
|
nativeProps,
|
|
207
207
|
)}
|
|
208
|
-
className={clsx(
|
|
208
|
+
className={clsx(
|
|
209
|
+
slotClassName && baseClassName ? `${baseClassName}-slot` : baseClassName,
|
|
210
|
+
slotClassName,
|
|
211
|
+
className,
|
|
212
|
+
)}
|
|
209
213
|
style={mergeWrapperStyle({ size, color, style })}
|
|
210
214
|
>
|
|
211
215
|
{cloneElement(icon, {
|
|
@@ -258,10 +262,19 @@ export const SuffixIcon = createIconComponent<SuffixIconProps>(
|
|
|
258
262
|
export interface InternalIconProps extends LynxStyledElementProps {
|
|
259
263
|
icon: ReactElement<LynxIconElementProps>;
|
|
260
264
|
deps?: DependencyList;
|
|
265
|
+
disableDefaultResize?: boolean;
|
|
261
266
|
}
|
|
262
267
|
|
|
263
268
|
export const InternalIcon = React.forwardRef<unknown, InternalIconProps>((props, ref) => {
|
|
264
|
-
const {
|
|
269
|
+
const {
|
|
270
|
+
icon,
|
|
271
|
+
deps = [],
|
|
272
|
+
className,
|
|
273
|
+
style,
|
|
274
|
+
children: _children,
|
|
275
|
+
disableDefaultResize = false,
|
|
276
|
+
...nativeProps
|
|
277
|
+
} = props;
|
|
265
278
|
const sourceRef = useMainThreadRef<MainThread.Element>(null);
|
|
266
279
|
const styleColor = getStyleColor(style);
|
|
267
280
|
const iconColor = useIconColor([className, styleColor, ...(deps ?? [])], { sourceRef });
|
|
@@ -280,6 +293,7 @@ export const InternalIcon = React.forwardRef<unknown, InternalIconProps>((props,
|
|
|
280
293
|
>
|
|
281
294
|
{cloneElement(icon, {
|
|
282
295
|
...iconColor,
|
|
296
|
+
...(disableDefaultResize ? { "disable-default-resize": true } : {}),
|
|
283
297
|
className: icon.props.className,
|
|
284
298
|
style: {
|
|
285
299
|
...icon.props.style,
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
identityPlaceholder,
|
|
3
|
+
type IdentityPlaceholderVariantProps,
|
|
4
|
+
} from "@seed-design/lynx-css/recipes/identity-placeholder";
|
|
5
|
+
import type { IntrinsicElements, NodesRef } from "@lynx-js/types";
|
|
6
|
+
import clsx from "clsx";
|
|
7
|
+
import * as React from "@lynx-js/react";
|
|
8
|
+
|
|
9
|
+
import type { LynxStyledElementProps, LynxViewRef } from "../../types";
|
|
10
|
+
import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
|
|
11
|
+
import { mergeProps } from "../../utils/merge-props";
|
|
12
|
+
import businessSource from "./identity-placeholder-business.webp";
|
|
13
|
+
import personSource from "./identity-placeholder-person.webp";
|
|
14
|
+
|
|
15
|
+
const { ClassNamesProvider, PropsProvider, useClassNames, useProps } =
|
|
16
|
+
createSlotRecipeContext(identityPlaceholder);
|
|
17
|
+
|
|
18
|
+
type Identity = NonNullable<IdentityPlaceholderVariantProps["identity"]>;
|
|
19
|
+
|
|
20
|
+
const identitySources = {
|
|
21
|
+
person: personSource,
|
|
22
|
+
business: businessSource,
|
|
23
|
+
} satisfies Record<Identity, string>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @platform Lynx
|
|
27
|
+
*
|
|
28
|
+
* Web의 SVG/asChild API 대신 native `<view>` props를 받습니다.
|
|
29
|
+
*/
|
|
30
|
+
export interface IdentityPlaceholderRootProps
|
|
31
|
+
extends IdentityPlaceholderVariantProps,
|
|
32
|
+
LynxStyledElementProps {}
|
|
33
|
+
|
|
34
|
+
export const IdentityPlaceholderRoot = React.forwardRef<unknown, IdentityPlaceholderRootProps>(
|
|
35
|
+
(props, ref) => {
|
|
36
|
+
const [variantProps, otherProps] = identityPlaceholder.splitVariantProps(props);
|
|
37
|
+
const { children, className, style, ...nativeProps } = otherProps;
|
|
38
|
+
const classNames = identityPlaceholder(variantProps);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<ClassNamesProvider value={classNames}>
|
|
42
|
+
<PropsProvider value={variantProps}>
|
|
43
|
+
<view
|
|
44
|
+
{...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
|
|
45
|
+
className={clsx(classNames.root, className)}
|
|
46
|
+
style={style}
|
|
47
|
+
>
|
|
48
|
+
{children}
|
|
49
|
+
</view>
|
|
50
|
+
</PropsProvider>
|
|
51
|
+
</ClassNamesProvider>
|
|
52
|
+
);
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
IdentityPlaceholderRoot.displayName = "IdentityPlaceholderRoot";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @platform Lynx
|
|
59
|
+
*
|
|
60
|
+
* `src`와 `mode`는 identity variant가 소유합니다. Web SVG props 대신 native image
|
|
61
|
+
* props를 사용하며 SVG rendering과 `asChild`는 지원하지 않습니다.
|
|
62
|
+
*
|
|
63
|
+
* React SVG의 기본 `xMidYMid meet`처럼 도형 전체를 비율을 유지한 채 영역 안에 맞춥니다.
|
|
64
|
+
*
|
|
65
|
+
* 로컬 WebP는 `static-white-alpha-800`의 고정값 `#ffffffde`로 원본 SVG path를
|
|
66
|
+
* rasterize합니다. Native image의 SVG/fill 경로를 피하고, 토큰이 light/dark에서
|
|
67
|
+
* 동일하므로 동적 tint를 사용하지 않습니다.
|
|
68
|
+
*/
|
|
69
|
+
export interface IdentityPlaceholderImageProps
|
|
70
|
+
extends Omit<IntrinsicElements["image"], "src" | "mode" | "children"> {}
|
|
71
|
+
|
|
72
|
+
export const IdentityPlaceholderImage = React.forwardRef<unknown, IdentityPlaceholderImageProps>(
|
|
73
|
+
(props, ref) => {
|
|
74
|
+
const classNames = useClassNames();
|
|
75
|
+
const parentProps = useProps();
|
|
76
|
+
|
|
77
|
+
if (parentProps === null) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
"<IdentityPlaceholderImage/> must be rendered inside <IdentityPlaceholderRoot/>.",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const {
|
|
84
|
+
className,
|
|
85
|
+
"accessibility-label": accessibilityLabel,
|
|
86
|
+
"accessibility-traits": accessibilityTraits,
|
|
87
|
+
...nativeProps
|
|
88
|
+
} = props;
|
|
89
|
+
const identity = parentProps.identity ?? "person";
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<image
|
|
93
|
+
{...mergeProps(ref ? { ref: ref as React.Ref<NodesRef> } : {}, nativeProps)}
|
|
94
|
+
src={identitySources[identity]}
|
|
95
|
+
mode="aspectFit"
|
|
96
|
+
accessibility-label={accessibilityLabel ?? "Identity placeholder"}
|
|
97
|
+
accessibility-traits={accessibilityTraits ?? "image"}
|
|
98
|
+
className={clsx(classNames.image, className)}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
IdentityPlaceholderImage.displayName = "IdentityPlaceholderImage";
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import "@testing-library/jest-dom";
|
|
2
|
-
import
|
|
2
|
+
import * as React from "@lynx-js/react";
|
|
3
|
+
import { runOnBackground } from "@lynx-js/react";
|
|
4
|
+
import {
|
|
5
|
+
fireEvent,
|
|
6
|
+
getQueriesForElement,
|
|
7
|
+
render,
|
|
8
|
+
waitSchedule,
|
|
9
|
+
} from "@lynx-js/react/testing-library";
|
|
3
10
|
import { describe, expect, it, vi } from "vitest";
|
|
4
11
|
|
|
5
12
|
import { InputButton } from "./index";
|
|
13
|
+
import type { LynxIconElementProps } from "../../types";
|
|
14
|
+
import type { MainThread } from "@lynx-js/types";
|
|
15
|
+
|
|
16
|
+
const MockIcon = React.forwardRef<MainThread.Element, LynxIconElementProps>((props, ref) => (
|
|
17
|
+
<image {...props} {...(ref ? { "main-thread:ref": ref as React.Ref<MainThread.Element> } : {})} />
|
|
18
|
+
));
|
|
19
|
+
MockIcon.displayName = "MockIcon";
|
|
6
20
|
|
|
7
21
|
function getRenderedRoot() {
|
|
8
22
|
const root = elementTree.root;
|
|
@@ -76,6 +90,119 @@ describe("InputButton", () => {
|
|
|
76
90
|
);
|
|
77
91
|
});
|
|
78
92
|
|
|
93
|
+
it("groups fragment content while leaving the button and strokes outside the scale target", () => {
|
|
94
|
+
render(
|
|
95
|
+
<InputButton.Root>
|
|
96
|
+
<React.Fragment key="fragment-slots">
|
|
97
|
+
<InputButton.Button accessibility-label="Select" />
|
|
98
|
+
<InputButton.PrefixText>Prefix</InputButton.PrefixText>
|
|
99
|
+
<InputButton.Value>Value</InputButton.Value>
|
|
100
|
+
<InputButton.SuffixText>Suffix</InputButton.SuffixText>
|
|
101
|
+
</React.Fragment>
|
|
102
|
+
</InputButton.Root>,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const root = getRenderedRoot();
|
|
106
|
+
const content = root.querySelector(".seed-input-button__content");
|
|
107
|
+
const button = root.querySelector(".seed-input-button__button");
|
|
108
|
+
expect(content).toHaveAttribute("flatten", "false");
|
|
109
|
+
expect(content?.contains(button)).toBe(false);
|
|
110
|
+
expect(button?.querySelector(".seed-input-button__baseStroke")).toBeTruthy();
|
|
111
|
+
expect(button?.querySelector(".seed-input-button__stroke")).toBeTruthy();
|
|
112
|
+
expect(Array.from(content?.children ?? []).map((child) => child.textContent)).toEqual([
|
|
113
|
+
"Prefix",
|
|
114
|
+
"Value",
|
|
115
|
+
"Suffix",
|
|
116
|
+
]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("preserves custom wrapped Button composition without moving or dropping children", () => {
|
|
120
|
+
const CustomButton = () => <InputButton.Button accessibility-label="Custom" />;
|
|
121
|
+
render(
|
|
122
|
+
<InputButton.Root>
|
|
123
|
+
<CustomButton />
|
|
124
|
+
<InputButton.Value>Value</InputButton.Value>
|
|
125
|
+
</InputButton.Root>,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const root = getRenderedRoot();
|
|
129
|
+
expect(root.querySelector(".seed-input-button__button")).toHaveAttribute(
|
|
130
|
+
"accessibility-label",
|
|
131
|
+
"Custom",
|
|
132
|
+
);
|
|
133
|
+
expect(root.querySelector(".seed-input-button__content")).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("keeps the clear action independent and hides it in readonly mode", () => {
|
|
137
|
+
const onOpen = vi.fn();
|
|
138
|
+
const onClear = vi.fn();
|
|
139
|
+
const { rerender } = render(
|
|
140
|
+
<InputButton.Root>
|
|
141
|
+
<InputButton.Button accessibility-label="Select" bindtap={onOpen} />
|
|
142
|
+
<InputButton.Value>Value</InputButton.Value>
|
|
143
|
+
<InputButton.ClearButton
|
|
144
|
+
icon={<MockIcon />}
|
|
145
|
+
accessibility-label="Clear"
|
|
146
|
+
bindtap={onClear}
|
|
147
|
+
/>
|
|
148
|
+
</InputButton.Root>,
|
|
149
|
+
);
|
|
150
|
+
const root = getRenderedRoot();
|
|
151
|
+
const clear = root.querySelector(".seed-input-button__clearButton") as HTMLElement;
|
|
152
|
+
expect(clear).toHaveAttribute("flatten", "false");
|
|
153
|
+
fireEvent.tap(clear);
|
|
154
|
+
expect(onClear).toHaveBeenCalledTimes(1);
|
|
155
|
+
expect(onOpen).not.toHaveBeenCalled();
|
|
156
|
+
expect(root.querySelector(".seed-input-button__button")).not.toHaveClass(
|
|
157
|
+
"seed-input-button__button--pressed_true",
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
rerender(
|
|
161
|
+
<InputButton.Root readOnly>
|
|
162
|
+
<InputButton.Button accessibility-label="Select" bindtap={onOpen} />
|
|
163
|
+
<InputButton.ClearButton
|
|
164
|
+
icon={<MockIcon />}
|
|
165
|
+
accessibility-label="Clear"
|
|
166
|
+
bindtap={onClear}
|
|
167
|
+
/>
|
|
168
|
+
</InputButton.Root>,
|
|
169
|
+
);
|
|
170
|
+
expect(getRenderedRoot().querySelector(".seed-input-button__clearButton")).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("bridges Main Thread presses while preserving the consumer Main Thread handler", async () => {
|
|
174
|
+
function Example({ report }: { report: () => void }) {
|
|
175
|
+
function handleTouch() {
|
|
176
|
+
"main thread";
|
|
177
|
+
runOnBackground(report)();
|
|
178
|
+
}
|
|
179
|
+
return (
|
|
180
|
+
<InputButton.Root>
|
|
181
|
+
<InputButton.Button
|
|
182
|
+
accessibility-label="Select"
|
|
183
|
+
main-thread:bindtouchstart={handleTouch}
|
|
184
|
+
/>
|
|
185
|
+
<InputButton.Value>Value</InputButton.Value>
|
|
186
|
+
</InputButton.Root>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const report = vi.fn();
|
|
190
|
+
const { container } = render(<Example report={report} />, {
|
|
191
|
+
enableMainThread: true,
|
|
192
|
+
enableBackgroundThread: true,
|
|
193
|
+
});
|
|
194
|
+
await waitSchedule();
|
|
195
|
+
const button = container.querySelector(".seed-input-button__button");
|
|
196
|
+
if (!button) throw new Error("Expected button");
|
|
197
|
+
fireEvent.touchstart(button, {});
|
|
198
|
+
await waitSchedule();
|
|
199
|
+
expect(report).toHaveBeenCalledTimes(1);
|
|
200
|
+
expect(button.className).toContain("pressed_true");
|
|
201
|
+
fireEvent.touchcancel(button, {});
|
|
202
|
+
await waitSchedule();
|
|
203
|
+
expect(button.className).not.toContain("pressed_true");
|
|
204
|
+
});
|
|
205
|
+
|
|
79
206
|
it("throws when a slot is rendered outside InputButton.Root", () => {
|
|
80
207
|
expect(() => render(<InputButton.Value>값</InputButton.Value>)).toThrow(/InputButton\.Value/);
|
|
81
208
|
});
|
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
import clsx from "clsx";
|
|
8
8
|
|
|
9
9
|
import { usePressTap } from "../../hooks/usePressTap";
|
|
10
|
+
import { useScaleFeedback, type ScaleFeedbackTriggerProps } from "../../hooks/useScaleFeedback";
|
|
11
|
+
import { mergeProps } from "../../utils/merge-props";
|
|
12
|
+
import { toArray } from "../../utils/children";
|
|
10
13
|
import type {
|
|
11
14
|
LynxAccessibilityProps,
|
|
12
15
|
LynxPressableProps,
|
|
@@ -20,6 +23,8 @@ interface InputButtonContextValue {
|
|
|
20
23
|
variantProps: InputButtonVariantProps;
|
|
21
24
|
disabled: boolean;
|
|
22
25
|
readOnly: boolean;
|
|
26
|
+
pressed: boolean;
|
|
27
|
+
scaleFeedbackTriggerProps: ScaleFeedbackTriggerProps;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
const InputButtonContext = React.createContext<InputButtonContextValue | null>(null);
|
|
@@ -42,6 +47,8 @@ function useInputButtonContext(consumer: string): InputButtonContextValue {
|
|
|
42
47
|
* 웹 대비 미지원 기능:
|
|
43
48
|
* - `size="responsive"`: Lynx에는 CSS viewport breakpoint가 없음
|
|
44
49
|
* - HTML form 제출과 DOM ARIA id 연결
|
|
50
|
+
* - Content Scale을 적용하려면 Button을 Root의 직접 자식 또는 Fragment 안에 둡니다.
|
|
51
|
+
* 커스텀 컴포넌트로 감싼 Button은 기존 구조를 보존하며 Content Scale을 생략합니다.
|
|
45
52
|
*/
|
|
46
53
|
export interface InputButtonRootProps
|
|
47
54
|
extends Omit<InputButtonVariantProps, "pressed">,
|
|
@@ -54,9 +61,21 @@ export const InputButtonRoot = React.forwardRef<NodesRef, InputButtonRootProps>(
|
|
|
54
61
|
const disabled = variantProps.disabled ?? false;
|
|
55
62
|
const readOnly = variantProps.readOnly ?? false;
|
|
56
63
|
const classes = inputButton(variantProps);
|
|
64
|
+
const { pressed, bindtouchstart, bindtouchend, bindtouchcancel } = usePressTap({
|
|
65
|
+
disabled: disabled || readOnly,
|
|
66
|
+
});
|
|
67
|
+
const { scaleFeedbackTriggerProps, scaleFeedbackTargetProps } = useScaleFeedback({
|
|
68
|
+
disabled: disabled || readOnly,
|
|
69
|
+
onTouchStart: bindtouchstart,
|
|
70
|
+
onTouchEnd: bindtouchend,
|
|
71
|
+
onTouchCancel: bindtouchcancel,
|
|
72
|
+
});
|
|
73
|
+
const childArray = flattenInputButtonChildren(children);
|
|
74
|
+
const buttonChildren = childArray.filter(isInputButtonButton);
|
|
75
|
+
const contentChildren = childArray.filter((child) => !isInputButtonButton(child));
|
|
57
76
|
const contextValue = React.useMemo(
|
|
58
|
-
() => ({ variantProps, disabled, readOnly }),
|
|
59
|
-
[variantProps, disabled, readOnly],
|
|
77
|
+
() => ({ variantProps, disabled, readOnly, pressed, scaleFeedbackTriggerProps }),
|
|
78
|
+
[variantProps, disabled, readOnly, pressed, scaleFeedbackTriggerProps],
|
|
60
79
|
);
|
|
61
80
|
|
|
62
81
|
return (
|
|
@@ -66,7 +85,16 @@ export const InputButtonRoot = React.forwardRef<NodesRef, InputButtonRootProps>(
|
|
|
66
85
|
className={clsx(classes.root, className)}
|
|
67
86
|
{...nativeProps}
|
|
68
87
|
>
|
|
69
|
-
{
|
|
88
|
+
{buttonChildren.length > 0 ? (
|
|
89
|
+
<>
|
|
90
|
+
{buttonChildren}
|
|
91
|
+
<view {...scaleFeedbackTargetProps} className={classes.content}>
|
|
92
|
+
{contentChildren}
|
|
93
|
+
</view>
|
|
94
|
+
</>
|
|
95
|
+
) : (
|
|
96
|
+
children
|
|
97
|
+
)}
|
|
70
98
|
</view>
|
|
71
99
|
</InputButtonContext.Provider>
|
|
72
100
|
);
|
|
@@ -93,12 +121,12 @@ export const InputButtonButton = React.forwardRef<unknown, InputButtonButtonProp
|
|
|
93
121
|
...nativeProps
|
|
94
122
|
} = props;
|
|
95
123
|
const nonInteractive = context.disabled || context.readOnly;
|
|
96
|
-
const {
|
|
124
|
+
const { bindtap: handleTap, "main-thread:bindtap": handleMainThreadTap } = usePressTap({
|
|
97
125
|
disabled: nonInteractive,
|
|
98
126
|
onTap: bindtap,
|
|
99
127
|
mainThreadOnTap: mainThreadBindtap,
|
|
100
128
|
});
|
|
101
|
-
const classes = inputButton({ ...context.variantProps, pressed });
|
|
129
|
+
const classes = inputButton({ ...context.variantProps, pressed: context.pressed });
|
|
102
130
|
|
|
103
131
|
return (
|
|
104
132
|
<view
|
|
@@ -106,8 +134,11 @@ export const InputButtonButton = React.forwardRef<unknown, InputButtonButtonProp
|
|
|
106
134
|
className={clsx(classes.button, className)}
|
|
107
135
|
accessibility-element={accessibilityElement}
|
|
108
136
|
accessibility-traits={accessibilityTraits ?? (nonInteractive ? "disabled" : "button")}
|
|
109
|
-
{...
|
|
110
|
-
|
|
137
|
+
{...mergeProps(
|
|
138
|
+
{ bindtap: handleTap, "main-thread:bindtap": handleMainThreadTap },
|
|
139
|
+
context.scaleFeedbackTriggerProps,
|
|
140
|
+
nativeProps,
|
|
141
|
+
)}
|
|
111
142
|
>
|
|
112
143
|
<view className={classes.baseStroke} accessibility-elements-hidden={true} />
|
|
113
144
|
<view className={classes.stroke} accessibility-elements-hidden={true} />
|
|
@@ -117,6 +148,18 @@ export const InputButtonButton = React.forwardRef<unknown, InputButtonButtonProp
|
|
|
117
148
|
});
|
|
118
149
|
InputButtonButton.displayName = "InputButtonButton";
|
|
119
150
|
|
|
151
|
+
function isInputButtonButton(node: React.ReactNode) {
|
|
152
|
+
return React.isValidElement(node) && node.type === InputButtonButton;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function flattenInputButtonChildren(children: React.ReactNode): React.ReactNode[] {
|
|
156
|
+
return toArray(children).flatMap((child) =>
|
|
157
|
+
React.isValidElement<{ children?: React.ReactNode }>(child) && child.type === React.Fragment
|
|
158
|
+
? flattenInputButtonChildren(child.props.children)
|
|
159
|
+
: [child],
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
120
163
|
////////////////////////////////////////////////////////////////////////////////////
|
|
121
164
|
|
|
122
165
|
export interface InputButtonValueProps extends LynxStyledElementProps {}
|
|
@@ -272,6 +315,9 @@ export const InputButtonClearButton = React.forwardRef<unknown, InputButtonClear
|
|
|
272
315
|
mainThreadOnTap: mainThreadBindtap,
|
|
273
316
|
});
|
|
274
317
|
const classes = inputButton({ ...context.variantProps, pressed });
|
|
318
|
+
const { scaleFeedbackTriggerProps, scaleFeedbackTargetProps } = useScaleFeedback({
|
|
319
|
+
disabled: nonInteractive,
|
|
320
|
+
});
|
|
275
321
|
|
|
276
322
|
if (nonInteractive) return null;
|
|
277
323
|
|
|
@@ -286,8 +332,13 @@ export const InputButtonClearButton = React.forwardRef<unknown, InputButtonClear
|
|
|
286
332
|
accessibility-element={accessibilityElement}
|
|
287
333
|
accessibility-label={accessibilityLabel}
|
|
288
334
|
accessibility-traits="button"
|
|
289
|
-
{...
|
|
290
|
-
|
|
335
|
+
{...mergeProps(
|
|
336
|
+
pressHandlers,
|
|
337
|
+
scaleFeedbackTriggerProps,
|
|
338
|
+
scaleFeedbackTargetProps,
|
|
339
|
+
otherProps,
|
|
340
|
+
{ flatten: false },
|
|
341
|
+
)}
|
|
291
342
|
/>
|
|
292
343
|
);
|
|
293
344
|
},
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import "@testing-library/jest-dom";
|
|
2
|
-
import { fireEvent, render } from "@lynx-js/react/testing-library";
|
|
2
|
+
import { fireEvent, render, waitSchedule } from "@lynx-js/react/testing-library";
|
|
3
3
|
import { describe, expect, it, vi } from "vitest";
|
|
4
4
|
|
|
5
|
+
import { Checkbox } from "../Checkbox";
|
|
6
|
+
import { Switch } from "../Switch";
|
|
5
7
|
import { RadioGroup } from "../RadioGroup";
|
|
6
8
|
import { List, ListHeader } from "./index";
|
|
7
9
|
|
|
@@ -66,7 +68,7 @@ describe("List", () => {
|
|
|
66
68
|
);
|
|
67
69
|
});
|
|
68
70
|
|
|
69
|
-
it("tracks button pressed state and ignores a disabled tap", () => {
|
|
71
|
+
it("tracks button pressed state and ignores a disabled tap", async () => {
|
|
70
72
|
const onTap = vi.fn();
|
|
71
73
|
const { rerender } = render(
|
|
72
74
|
<List.ButtonItem className="button-item" bindtap={onTap} accessibility-label="열기">
|
|
@@ -74,15 +76,19 @@ describe("List", () => {
|
|
|
74
76
|
<List.Title>버튼</List.Title>
|
|
75
77
|
</List.Content>
|
|
76
78
|
</List.ButtonItem>,
|
|
79
|
+
{ enableMainThread: true, enableBackgroundThread: true },
|
|
77
80
|
);
|
|
78
81
|
|
|
82
|
+
await waitSchedule();
|
|
79
83
|
let item = getListItem("button-item");
|
|
80
84
|
fireEvent.touchstart(item, {});
|
|
85
|
+
await waitSchedule();
|
|
81
86
|
expect(item.querySelector(".seed-list-item__pressedOverlay")).toHaveClass(
|
|
82
87
|
"seed-list-item__pressedOverlay--pressed_true",
|
|
83
88
|
);
|
|
84
89
|
|
|
85
90
|
fireEvent.tap(item);
|
|
91
|
+
await waitSchedule();
|
|
86
92
|
expect(onTap).toHaveBeenCalledTimes(1);
|
|
87
93
|
|
|
88
94
|
rerender(
|
|
@@ -94,6 +100,7 @@ describe("List", () => {
|
|
|
94
100
|
);
|
|
95
101
|
item = getListItem("button-item");
|
|
96
102
|
fireEvent.tap(item);
|
|
103
|
+
await waitSchedule();
|
|
97
104
|
|
|
98
105
|
expect(onTap).toHaveBeenCalledTimes(1);
|
|
99
106
|
expect(item).toHaveAttribute("accessibility-traits", "disabled");
|
|
@@ -159,4 +166,49 @@ describe("List", () => {
|
|
|
159
166
|
expect(first).toHaveAttribute("accessibility-value", "선택 안 됨");
|
|
160
167
|
expect(second).toHaveAttribute("accessibility-value", "선택됨");
|
|
161
168
|
});
|
|
169
|
+
it("scales only interactive row content and suppresses nested control targets", () => {
|
|
170
|
+
render(
|
|
171
|
+
<List.Root>
|
|
172
|
+
<List.Item className="static-row">
|
|
173
|
+
<List.Title>Static</List.Title>
|
|
174
|
+
</List.Item>
|
|
175
|
+
<List.CheckboxItem className="check-row">
|
|
176
|
+
<List.Prefix>
|
|
177
|
+
<Checkbox.Control />
|
|
178
|
+
</List.Prefix>
|
|
179
|
+
<List.Title>Check</List.Title>
|
|
180
|
+
</List.CheckboxItem>
|
|
181
|
+
<List.SwitchItem className="switch-row">
|
|
182
|
+
<List.Suffix>
|
|
183
|
+
<Switch.Control>
|
|
184
|
+
<Switch.Thumb />
|
|
185
|
+
</Switch.Control>
|
|
186
|
+
</List.Suffix>
|
|
187
|
+
</List.SwitchItem>
|
|
188
|
+
<RadioGroup.Root>
|
|
189
|
+
<List.RadioItem value="a" className="radio-row">
|
|
190
|
+
<List.Prefix>
|
|
191
|
+
<RadioGroup.ItemControl />
|
|
192
|
+
</List.Prefix>
|
|
193
|
+
</List.RadioItem>
|
|
194
|
+
</RadioGroup.Root>
|
|
195
|
+
</List.Root>,
|
|
196
|
+
);
|
|
197
|
+
expect(getListItem("static-row").querySelector(".seed-list-item__layout")).not.toHaveAttribute(
|
|
198
|
+
"flatten",
|
|
199
|
+
"false",
|
|
200
|
+
);
|
|
201
|
+
for (const name of ["check-row", "switch-row", "radio-row"]) {
|
|
202
|
+
const row = getListItem(name);
|
|
203
|
+
const layout = row.querySelector(".seed-list-item__layout")!;
|
|
204
|
+
expect(layout).toHaveAttribute("flatten", "false");
|
|
205
|
+
expect(layout).not.toContainElement(row.querySelector(".seed-list-item__pressedOverlay"));
|
|
206
|
+
if (name === "switch-row") {
|
|
207
|
+
// Switch keeps its native view for its own thumb animation even without a scale target.
|
|
208
|
+
expect(layout.querySelector(".seed-switchmark__root")).toHaveAttribute("flatten", "false");
|
|
209
|
+
} else {
|
|
210
|
+
expect(layout.querySelectorAll('[flatten="false"]')).toHaveLength(0);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
162
214
|
});
|