@seed-design/lynx-react 0.6.3 → 0.7.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.d.ts.map +1 -1
- package/lib/components/Accordion/Accordion.jsx +24 -14
- package/lib/components/ActionButton/ActionButton.d.ts +1 -1
- package/lib/components/ActionButton/ActionButton.d.ts.map +1 -1
- package/lib/components/ActionButton/ActionButton.jsx +12 -4
- package/lib/components/AppBar/AppBar.d.ts.map +1 -1
- package/lib/components/AppBar/AppBar.jsx +9 -8
- package/lib/components/Badge/Badge.d.ts.map +1 -1
- package/lib/components/Badge/Badge.jsx +2 -1
- package/lib/components/BottomSheet/BottomSheet.d.ts.map +1 -1
- package/lib/components/BottomSheet/BottomSheet.jsx +9 -9
- package/lib/components/Box/Box.d.ts.map +1 -1
- package/lib/components/Box/Box.jsx +2 -1
- package/lib/components/Callout/Callout.d.ts +2 -2
- package/lib/components/Callout/Callout.d.ts.map +1 -1
- package/lib/components/Callout/Callout.jsx +20 -10
- package/lib/components/Checkbox/Checkbox.d.ts +4 -2
- package/lib/components/Checkbox/Checkbox.d.ts.map +1 -1
- package/lib/components/Checkbox/Checkbox.jsx +33 -17
- package/lib/components/Chip/Chip.d.ts +1 -1
- package/lib/components/Chip/Chip.d.ts.map +1 -1
- package/lib/components/Chip/Chip.jsx +36 -16
- package/lib/components/Count/Count.d.ts +7 -0
- package/lib/components/Count/Count.d.ts.map +1 -0
- package/lib/components/Count/Count.jsx +19 -0
- package/lib/components/Count/index.d.ts +2 -0
- package/lib/components/Count/index.d.ts.map +1 -0
- package/lib/components/Divider/Divider.d.ts.map +1 -1
- package/lib/components/Divider/Divider.jsx +2 -1
- package/lib/components/Field/Field.d.ts.map +1 -1
- package/lib/components/Field/Field.jsx +11 -15
- package/lib/components/Icon/Icon.d.ts.map +1 -1
- package/lib/components/Icon/Icon.jsx +4 -3
- package/lib/components/InputButton/InputButton.d.ts +40 -0
- package/lib/components/InputButton/InputButton.d.ts.map +1 -0
- package/lib/components/InputButton/InputButton.jsx +118 -0
- package/lib/components/InputButton/InputButton.namespace.d.ts +2 -0
- package/lib/components/InputButton/InputButton.namespace.d.ts.map +1 -0
- package/lib/components/InputButton/InputButton.namespace.js +16 -0
- package/lib/components/InputButton/index.d.ts +3 -0
- package/lib/components/InputButton/index.d.ts.map +1 -0
- package/lib/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.d.ts.map +1 -1
- package/lib/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.jsx +12 -8
- package/lib/components/List/List.d.ts +51 -0
- package/lib/components/List/List.d.ts.map +1 -0
- package/lib/components/List/List.jsx +176 -0
- package/lib/components/List/List.namespace.d.ts +2 -0
- package/lib/components/List/List.namespace.d.ts.map +1 -0
- package/lib/components/List/List.namespace.js +18 -0
- package/lib/components/List/index.d.ts +3 -0
- package/lib/components/List/index.d.ts.map +1 -0
- package/lib/components/MannerTemp/MannerTemp.d.ts.map +1 -1
- package/lib/components/MannerTemp/MannerTemp.jsx +3 -2
- package/lib/components/MannerTempBadge/MannerTempBadge.d.ts.map +1 -1
- package/lib/components/MannerTempBadge/MannerTempBadge.jsx +2 -1
- package/lib/components/Menu/Menu.d.ts +69 -0
- package/lib/components/Menu/Menu.d.ts.map +1 -0
- package/lib/components/Menu/Menu.jsx +520 -0
- package/lib/components/Menu/Menu.namespace.d.ts +2 -0
- package/lib/components/Menu/Menu.namespace.d.ts.map +1 -0
- package/lib/components/Menu/Menu.namespace.js +21 -0
- package/lib/components/Menu/index.d.ts +3 -0
- package/lib/components/Menu/index.d.ts.map +1 -0
- package/lib/components/NotificationBadge/NotificationBadge.d.ts.map +1 -1
- package/lib/components/NotificationBadge/NotificationBadge.jsx +3 -2
- package/lib/components/PageBanner/PageBanner.d.ts +5 -5
- package/lib/components/PageBanner/PageBanner.d.ts.map +1 -1
- package/lib/components/PageBanner/PageBanner.jsx +25 -14
- package/lib/components/ProgressCircle/ProgressCircle.d.ts +2 -2
- package/lib/components/ProgressCircle/ProgressCircle.d.ts.map +1 -1
- package/lib/components/ProgressCircle/ProgressCircle.jsx +4 -3
- package/lib/components/RadioGroup/RadioGroup.d.ts +5 -3
- package/lib/components/RadioGroup/RadioGroup.d.ts.map +1 -1
- package/lib/components/RadioGroup/RadioGroup.jsx +22 -9
- package/lib/components/ReactionButton/ReactionButton.d.ts +20 -0
- package/lib/components/ReactionButton/ReactionButton.d.ts.map +1 -0
- package/lib/components/ReactionButton/ReactionButton.jsx +88 -0
- package/lib/components/ReactionButton/index.d.ts +2 -0
- package/lib/components/ReactionButton/index.d.ts.map +1 -0
- package/lib/components/ScaleFeedback/ScaleFeedback.d.ts +11 -0
- package/lib/components/ScaleFeedback/ScaleFeedback.d.ts.map +1 -0
- package/lib/components/ScaleFeedback/ScaleFeedback.jsx +16 -0
- package/lib/components/ScaleFeedback/index.d.ts +2 -0
- package/lib/components/ScaleFeedback/index.d.ts.map +1 -0
- package/lib/components/SegmentedControl/SegmentedControl.d.ts +1 -0
- package/lib/components/SegmentedControl/SegmentedControl.d.ts.map +1 -1
- package/lib/components/SegmentedControl/SegmentedControl.jsx +23 -10
- package/lib/components/SelectBox/SelectBox.d.ts.map +1 -1
- package/lib/components/SelectBox/SelectBox.jsx +23 -13
- package/lib/components/Stack/Stack.d.ts.map +1 -1
- package/lib/components/Stack/Stack.jsx +2 -1
- package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.d.ts +85 -0
- package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.d.ts.map +1 -0
- package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.jsx +256 -0
- package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.namespace.d.ts +2 -0
- package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.namespace.d.ts.map +1 -0
- package/lib/components/SwipeableMenuSheet/SwipeableMenuSheet.namespace.js +24 -0
- package/lib/components/SwipeableMenuSheet/index.d.ts +3 -0
- package/lib/components/SwipeableMenuSheet/index.d.ts.map +1 -0
- package/lib/components/Switch/Switch.d.ts +27 -3
- package/lib/components/Switch/Switch.d.ts.map +1 -1
- package/lib/components/Switch/Switch.jsx +22 -9
- package/lib/components/Tabs/Tabs.d.ts +4 -2
- package/lib/components/Tabs/Tabs.d.ts.map +1 -1
- package/lib/components/Tabs/Tabs.jsx +37 -22
- package/lib/components/TagGroup/TagGroup.d.ts.map +1 -1
- package/lib/components/TagGroup/TagGroup.jsx +4 -3
- package/lib/components/Text/Text.d.ts.map +1 -1
- package/lib/components/Text/Text.jsx +2 -1
- package/lib/components/TextField/TextField.d.ts.map +1 -1
- package/lib/components/TextField/TextField.jsx +35 -23
- package/lib/components/index.d.ts +7 -0
- package/lib/components/index.d.ts.map +1 -1
- package/lib/components/private/Positioning.d.ts +69 -0
- package/lib/components/private/Positioning.d.ts.map +1 -0
- package/lib/components/private/Positioning.js +109 -0
- package/lib/components/private/TrackPositioning.d.ts +22 -0
- package/lib/components/private/TrackPositioning.d.ts.map +1 -0
- package/lib/contexts/ScaleFeedbackContentContext.d.ts +4 -0
- package/lib/contexts/ScaleFeedbackContentContext.d.ts.map +1 -0
- package/lib/contexts/ScaleFeedbackContentContext.js +6 -0
- package/lib/contexts/index.d.ts +2 -0
- package/lib/contexts/index.d.ts.map +1 -0
- package/lib/hooks/index.d.ts +1 -0
- package/lib/hooks/index.d.ts.map +1 -1
- package/lib/hooks/useIconColor.d.ts.map +1 -1
- package/lib/hooks/useIconColor.js +1 -1
- package/lib/hooks/usePressTap.d.ts +4 -3
- package/lib/hooks/usePressTap.d.ts.map +1 -1
- package/lib/hooks/useScaleFeedback.d.ts +48 -0
- package/lib/hooks/useScaleFeedback.d.ts.map +1 -0
- package/lib/hooks/useScaleFeedback.js +108 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +17 -3
- package/lib/node_modules/.bun/@floating-ui_core@1.8.0/node_modules/@floating-ui/core/dist/floating-ui.core.js +493 -0
- package/lib/node_modules/.bun/@floating-ui_utils@0.2.12/node_modules/@floating-ui/utils/dist/floating-ui.utils.js +114 -0
- package/lib/utils/calculate-scale-feedback.d.ts +5 -0
- package/lib/utils/calculate-scale-feedback.d.ts.map +1 -0
- package/lib/utils/calculate-scale-feedback.js +13 -0
- package/lib/utils/compose-event-handlers.d.ts +5 -0
- package/lib/utils/compose-event-handlers.d.ts.map +1 -0
- package/lib/utils/compose-event-handlers.js +16 -0
- package/lib/utils/create-slot-recipe-context.d.ts.map +1 -1
- package/lib/utils/create-slot-recipe-context.jsx +6 -16
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.d.ts.map +1 -1
- package/lib/utils/merge-props.d.ts +15 -0
- package/lib/utils/merge-props.d.ts.map +1 -0
- package/lib/utils/merge-props.js +36 -0
- package/lib/utils/merge-refs.d.ts +8 -0
- package/lib/utils/merge-refs.d.ts.map +1 -0
- package/lib/utils/merge-refs.js +38 -0
- package/package.json +6 -5
- package/src/components/Accordion/Accordion.tsx +36 -27
- package/src/components/ActionButton/ActionButton.tsx +27 -12
- package/src/components/AppBar/AppBar.tsx +17 -18
- package/src/components/Badge/Badge.tsx +2 -2
- package/src/components/BottomSheet/BottomSheet.tsx +9 -17
- package/src/components/Box/Box.tsx +2 -2
- package/src/components/Callout/Callout.tsx +39 -22
- package/src/components/Checkbox/Checkbox.test.tsx +44 -0
- package/src/components/Checkbox/Checkbox.tsx +52 -21
- package/src/components/Chip/Chip.test.tsx +50 -1
- package/src/components/Chip/Chip.tsx +75 -31
- package/src/components/Count/Count.tsx +35 -0
- package/src/components/Count/index.ts +1 -0
- package/src/components/Divider/Divider.tsx +2 -2
- package/src/components/Field/Field.tsx +15 -28
- package/src/components/Icon/Icon.tsx +14 -7
- package/src/components/InputButton/InputButton.namespace.ts +20 -0
- package/src/components/InputButton/InputButton.test.tsx +82 -0
- package/src/components/InputButton/InputButton.tsx +295 -0
- package/src/components/InputButton/index.ts +2 -0
- package/src/components/KeyboardAvoidingScrollView/KeyboardAvoidingScrollView.tsx +16 -21
- package/src/components/List/List.namespace.ts +24 -0
- package/src/components/List/List.test.tsx +162 -0
- package/src/components/List/List.tsx +476 -0
- package/src/components/List/index.ts +3 -0
- package/src/components/MannerTemp/MannerTemp.tsx +3 -4
- package/src/components/MannerTempBadge/MannerTempBadge.tsx +2 -2
- package/src/components/Menu/Menu.namespace.ts +26 -0
- package/src/components/Menu/Menu.tsx +883 -0
- package/src/components/Menu/index.ts +31 -0
- package/src/components/NotificationBadge/NotificationBadge.tsx +4 -9
- package/src/components/PageBanner/PageBanner.test.tsx +3 -0
- package/src/components/PageBanner/PageBanner.tsx +54 -45
- package/src/components/ProgressCircle/ProgressCircle.test.tsx +48 -0
- package/src/components/ProgressCircle/ProgressCircle.tsx +22 -3
- package/src/components/RadioGroup/RadioGroup.test.tsx +87 -0
- package/src/components/RadioGroup/RadioGroup.tsx +50 -14
- package/src/components/ReactionButton/ReactionButton.test.tsx +122 -0
- package/src/components/ReactionButton/ReactionButton.tsx +139 -0
- package/src/components/ReactionButton/index.ts +1 -0
- package/src/components/ScaleFeedback/ScaleFeedback.test.tsx +41 -0
- package/src/components/ScaleFeedback/ScaleFeedback.tsx +32 -0
- package/src/components/ScaleFeedback/index.ts +1 -0
- package/src/components/SegmentedControl/SegmentedControl.test.tsx +15 -3
- package/src/components/SegmentedControl/SegmentedControl.tsx +35 -14
- package/src/components/SelectBox/SelectBox.test.tsx +39 -1
- package/src/components/SelectBox/SelectBox.tsx +27 -16
- package/src/components/Stack/Stack.tsx +2 -2
- package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.namespace.ts +37 -0
- package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.test.tsx +258 -0
- package/src/components/SwipeableMenuSheet/SwipeableMenuSheet.tsx +655 -0
- package/src/components/SwipeableMenuSheet/index.ts +43 -0
- package/src/components/Switch/Switch.test.tsx +57 -0
- package/src/components/Switch/Switch.tsx +60 -16
- package/src/components/Tabs/Tabs.test.tsx +19 -0
- package/src/components/Tabs/Tabs.tsx +72 -40
- package/src/components/TagGroup/TagGroup.tsx +4 -6
- package/src/components/Text/Text.tsx +2 -2
- package/src/components/TextField/TextField.tsx +60 -54
- package/src/components/index.ts +7 -0
- package/src/components/private/Positioning.test.ts +235 -0
- package/src/components/private/Positioning.ts +223 -0
- package/src/components/private/TrackPositioning.test.ts +58 -0
- package/src/components/private/TrackPositioning.ts +37 -0
- package/src/contexts/ScaleFeedbackContentContext.ts +4 -0
- package/src/contexts/index.ts +1 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useIconColor.ts +5 -4
- package/src/hooks/usePressTap.ts +4 -3
- package/src/hooks/useScaleFeedback.integration.test.tsx +85 -0
- package/src/hooks/useScaleFeedback.test.ts +50 -0
- package/src/hooks/useScaleFeedback.ts +199 -0
- package/src/index.ts +1 -0
- package/src/utils/calculate-scale-feedback.ts +12 -0
- package/src/utils/compose-event-handlers.ts +16 -0
- package/src/utils/create-slot-recipe-context.tsx +6 -7
- package/src/utils/index.ts +1 -0
- package/src/utils/merge-props.test.tsx +152 -0
- package/src/utils/merge-props.ts +62 -0
- package/src/utils/merge-refs.ts +44 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { fireEvent, getQueriesForElement, render } from "@lynx-js/react/testing-library";
|
|
3
|
+
import type { MainThread } from "@lynx-js/types";
|
|
4
|
+
import type * as LynxReact from "@lynx-js/react";
|
|
5
|
+
import * as React from "@lynx-js/react";
|
|
6
|
+
import { describe, expect, it, vi } from "vitest";
|
|
7
|
+
|
|
8
|
+
import type { LynxIconElementProps } from "../../types";
|
|
9
|
+
import { Count } from "../Count";
|
|
10
|
+
import { PrefixIcon } from "../Icon";
|
|
11
|
+
import { ReactionButton } from "./ReactionButton";
|
|
12
|
+
|
|
13
|
+
vi.mock("@lynx-js/react", async (importOriginal) => {
|
|
14
|
+
const actual = await importOriginal<typeof LynxReact>();
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
runOnMainThread: () => () => undefined,
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function getRenderedRoot() {
|
|
23
|
+
const root = elementTree.root;
|
|
24
|
+
|
|
25
|
+
if (!root) {
|
|
26
|
+
throw new Error("Expected Lynx render root to exist.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return root;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getReactionButtonRoot() {
|
|
33
|
+
const root = getRenderedRoot();
|
|
34
|
+
|
|
35
|
+
if (root.classList.contains("seed-reaction-button__root")) return root;
|
|
36
|
+
|
|
37
|
+
const reactionButtonRoot = root.querySelector<HTMLElement>(".seed-reaction-button__root");
|
|
38
|
+
if (!reactionButtonRoot) throw new Error("Expected ReactionButton root to exist.");
|
|
39
|
+
|
|
40
|
+
return reactionButtonRoot;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const MockIcon = React.forwardRef<MainThread.Element, LynxIconElementProps>((props, ref) => {
|
|
44
|
+
const { className, style, ...rest } = props;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<image
|
|
48
|
+
{...rest}
|
|
49
|
+
{...(ref ? { "main-thread:ref": ref as React.Ref<MainThread.Element> } : {})}
|
|
50
|
+
className={className}
|
|
51
|
+
style={style}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
MockIcon.displayName = "MockIcon";
|
|
56
|
+
|
|
57
|
+
describe("ReactionButton", () => {
|
|
58
|
+
it("composes slots and toggles uncontrolled selected state", () => {
|
|
59
|
+
const onPressedChange = vi.fn();
|
|
60
|
+
render(
|
|
61
|
+
<ReactionButton onPressedChange={onPressedChange}>
|
|
62
|
+
<PrefixIcon icon={<MockIcon />} />
|
|
63
|
+
좋아요
|
|
64
|
+
<Count>12</Count>
|
|
65
|
+
</ReactionButton>,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const root = getReactionButtonRoot();
|
|
69
|
+
const queries = getQueriesForElement(getRenderedRoot());
|
|
70
|
+
|
|
71
|
+
expect(root).toHaveClass("seed-reaction-button__root--size_small");
|
|
72
|
+
expect(root.querySelector(".seed-prefix-icon")).toHaveClass("seed-reaction-button__prefixIcon");
|
|
73
|
+
expect(queries.getByText("좋아요")).toHaveClass("seed-reaction-button__label");
|
|
74
|
+
expect(queries.getByText("12")).toHaveClass("seed-count", "seed-reaction-button__count");
|
|
75
|
+
|
|
76
|
+
fireEvent.tap(root);
|
|
77
|
+
|
|
78
|
+
expect(onPressedChange).toHaveBeenCalledWith(true);
|
|
79
|
+
expect(getReactionButtonRoot()).toHaveClass("seed-reaction-button__root--selected_true");
|
|
80
|
+
expect(getReactionButtonRoot()).toHaveAttribute("accessibility-traits", "selected");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("does not toggle when disabled", () => {
|
|
84
|
+
const onPressedChange = vi.fn();
|
|
85
|
+
render(
|
|
86
|
+
<ReactionButton disabled onPressedChange={onPressedChange}>
|
|
87
|
+
좋아요
|
|
88
|
+
</ReactionButton>,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const root = getReactionButtonRoot();
|
|
92
|
+
fireEvent.tap(root);
|
|
93
|
+
|
|
94
|
+
expect(onPressedChange).not.toHaveBeenCalled();
|
|
95
|
+
expect(root).toHaveClass("seed-reaction-button__root--disabled_true");
|
|
96
|
+
expect(root).toHaveAttribute("accessibility-traits", "disabled");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("keeps content mounted and handles taps while loading", () => {
|
|
100
|
+
const onPressedChange = vi.fn();
|
|
101
|
+
render(
|
|
102
|
+
<ReactionButton loading onPressedChange={onPressedChange}>
|
|
103
|
+
좋아요
|
|
104
|
+
<Count>12</Count>
|
|
105
|
+
</ReactionButton>,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const root = getReactionButtonRoot();
|
|
109
|
+
const queries = getQueriesForElement(getRenderedRoot());
|
|
110
|
+
const content = root.querySelector(".seed-reaction-button__content");
|
|
111
|
+
|
|
112
|
+
expect(content).toHaveClass("seed-reaction-button__content--loading_true");
|
|
113
|
+
expect(queries.getByText("좋아요")).toBeInTheDocument();
|
|
114
|
+
expect(queries.getByText("12")).toBeInTheDocument();
|
|
115
|
+
expect(root.querySelector(".seed-reaction-button__loadingIndicator")).toBeInTheDocument();
|
|
116
|
+
expect(root.querySelector(".seed-progress-circle__root")).toBeInTheDocument();
|
|
117
|
+
|
|
118
|
+
fireEvent.tap(root);
|
|
119
|
+
|
|
120
|
+
expect(onPressedChange).toHaveBeenCalledWith(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { reactionButton } from "@seed-design/lynx-css/recipes/reaction-button";
|
|
2
|
+
import type { ReactionButtonVariantProps } from "@seed-design/lynx-css/recipes/reaction-button";
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import * as React from "@lynx-js/react";
|
|
5
|
+
import { cloneElement, useMemo } from "@lynx-js/react";
|
|
6
|
+
|
|
7
|
+
import { useControllableState } from "../../hooks/useControllableState";
|
|
8
|
+
import { usePressTap } from "../../hooks/usePressTap";
|
|
9
|
+
import type {
|
|
10
|
+
LynxAccessibilityProps,
|
|
11
|
+
LynxPressableProps,
|
|
12
|
+
LynxStyledElementProps,
|
|
13
|
+
LynxViewRef,
|
|
14
|
+
} from "../../types";
|
|
15
|
+
import { toArray } from "../../utils/children";
|
|
16
|
+
import { isCountElement, type CountProps } from "../Count/Count";
|
|
17
|
+
import { getIconSlotName, IconSlotProvider } from "../Icon/Icon";
|
|
18
|
+
import { ProgressCircleRange, ProgressCircleRoot } from "../ProgressCircle";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @platform Lynx
|
|
22
|
+
*
|
|
23
|
+
* 웹 대비 미지원 기능:
|
|
24
|
+
* - HTML button 속성 및 native form 제출
|
|
25
|
+
* - 키보드 focus / focusVisible
|
|
26
|
+
* - `asChild`
|
|
27
|
+
*/
|
|
28
|
+
export interface ReactionButtonProps
|
|
29
|
+
extends Omit<ReactionButtonVariantProps, "selected" | "pressed" | "disabled" | "loading">,
|
|
30
|
+
LynxStyledElementProps,
|
|
31
|
+
LynxPressableProps,
|
|
32
|
+
LynxAccessibilityProps {
|
|
33
|
+
pressed?: boolean;
|
|
34
|
+
defaultPressed?: boolean;
|
|
35
|
+
onPressedChange?: (pressed: boolean) => void;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
loading?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const ReactionButton = React.forwardRef<unknown, ReactionButtonProps>((props, ref) => {
|
|
41
|
+
const [variantProps, otherProps] = reactionButton.splitVariantProps(props);
|
|
42
|
+
const {
|
|
43
|
+
children,
|
|
44
|
+
className,
|
|
45
|
+
style,
|
|
46
|
+
defaultPressed = false,
|
|
47
|
+
onPressedChange,
|
|
48
|
+
bindtap,
|
|
49
|
+
"main-thread:bindtap": mainThreadBindtap,
|
|
50
|
+
"accessibility-element": accessibilityElement = true,
|
|
51
|
+
"accessibility-traits": accessibilityTraits,
|
|
52
|
+
...nativeProps
|
|
53
|
+
} = otherProps;
|
|
54
|
+
const {
|
|
55
|
+
size,
|
|
56
|
+
pressed: pressedProp,
|
|
57
|
+
disabled = false,
|
|
58
|
+
loading = false,
|
|
59
|
+
} = variantProps;
|
|
60
|
+
const [selected, setSelected] = useControllableState({
|
|
61
|
+
value: pressedProp,
|
|
62
|
+
defaultValue: defaultPressed,
|
|
63
|
+
onChange: onPressedChange,
|
|
64
|
+
});
|
|
65
|
+
const handleTap = React.useCallback(
|
|
66
|
+
(...args: Parameters<NonNullable<LynxPressableProps["bindtap"]>>) => {
|
|
67
|
+
setSelected(!selected);
|
|
68
|
+
bindtap?.(...args);
|
|
69
|
+
},
|
|
70
|
+
[bindtap, selected, setSelected],
|
|
71
|
+
);
|
|
72
|
+
const { pressed, ...pressHandlers } = usePressTap({
|
|
73
|
+
disabled,
|
|
74
|
+
onTap: handleTap,
|
|
75
|
+
mainThreadOnTap: mainThreadBindtap,
|
|
76
|
+
});
|
|
77
|
+
const classNames = reactionButton({ ...variantProps, selected, pressed, disabled, loading });
|
|
78
|
+
const iconSlotContextValue = useMemo(
|
|
79
|
+
() => ({
|
|
80
|
+
classNames: { prefixIcon: classNames.prefixIcon },
|
|
81
|
+
deps: [size ?? "small", selected, pressed, disabled, loading],
|
|
82
|
+
}),
|
|
83
|
+
[classNames.prefixIcon, size, selected, pressed, disabled, loading],
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const prefixIconChildren: React.ReactNode[] = [];
|
|
87
|
+
const labelChildren: React.ReactNode[] = [];
|
|
88
|
+
const countChildren: React.ReactElement<CountProps>[] = [];
|
|
89
|
+
|
|
90
|
+
for (const child of toArray(children)) {
|
|
91
|
+
if (getIconSlotName(child) === "prefixIcon") {
|
|
92
|
+
prefixIconChildren.push(child);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (isCountElement(child)) {
|
|
97
|
+
countChildren.push(child);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
labelChildren.push(child);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<IconSlotProvider value={iconSlotContextValue}>
|
|
106
|
+
<view
|
|
107
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
108
|
+
{...nativeProps}
|
|
109
|
+
className={clsx(classNames.root, className)}
|
|
110
|
+
style={style}
|
|
111
|
+
accessibility-element={accessibilityElement}
|
|
112
|
+
accessibility-traits={
|
|
113
|
+
disabled ? "disabled" : selected ? "selected" : (accessibilityTraits ?? "button")
|
|
114
|
+
}
|
|
115
|
+
{...pressHandlers}
|
|
116
|
+
>
|
|
117
|
+
<view className={classNames.content}>
|
|
118
|
+
{prefixIconChildren}
|
|
119
|
+
{labelChildren.length > 0 ? (
|
|
120
|
+
<text className={classNames.label}>{labelChildren}</text>
|
|
121
|
+
) : null}
|
|
122
|
+
{countChildren.map((child) =>
|
|
123
|
+
cloneElement(child, {
|
|
124
|
+
className: clsx(classNames.count, child.props.className),
|
|
125
|
+
}),
|
|
126
|
+
)}
|
|
127
|
+
</view>
|
|
128
|
+
{loading ? (
|
|
129
|
+
<view className={classNames.loadingIndicator}>
|
|
130
|
+
<ProgressCircleRoot size="14" tone="inherit">
|
|
131
|
+
<ProgressCircleRange />
|
|
132
|
+
</ProgressCircleRoot>
|
|
133
|
+
</view>
|
|
134
|
+
) : null}
|
|
135
|
+
</view>
|
|
136
|
+
</IconSlotProvider>
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
ReactionButton.displayName = "ReactionButton";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ReactionButton, type ReactionButtonProps } from "./ReactionButton";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createRef } from "@lynx-js/react";
|
|
2
|
+
import { render, waitSchedule } from "@lynx-js/react/testing-library";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { ScaleFeedback } from "./ScaleFeedback";
|
|
6
|
+
|
|
7
|
+
describe("ScaleFeedback", () => {
|
|
8
|
+
it("forwards the consumer ref and clears it on unmount", async () => {
|
|
9
|
+
const ref = createRef<unknown>();
|
|
10
|
+
const { unmount } = render(
|
|
11
|
+
<ScaleFeedback ref={ref}>
|
|
12
|
+
<view />
|
|
13
|
+
</ScaleFeedback>,
|
|
14
|
+
{ enableMainThread: true, enableBackgroundThread: true },
|
|
15
|
+
);
|
|
16
|
+
await waitSchedule();
|
|
17
|
+
|
|
18
|
+
expect(ref.current).toBeTruthy();
|
|
19
|
+
unmount();
|
|
20
|
+
await waitSchedule();
|
|
21
|
+
expect(ref.current).toBeNull();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("renders its child inside one native feedback target", async () => {
|
|
25
|
+
const { container } = render(
|
|
26
|
+
<ScaleFeedback>
|
|
27
|
+
<view id="target" />
|
|
28
|
+
</ScaleFeedback>,
|
|
29
|
+
{ enableMainThread: true, enableBackgroundThread: false },
|
|
30
|
+
);
|
|
31
|
+
await waitSchedule();
|
|
32
|
+
|
|
33
|
+
const target = container.querySelector("#target");
|
|
34
|
+
const feedbackTarget = container.children[0];
|
|
35
|
+
|
|
36
|
+
expect(target).not.toBeNull();
|
|
37
|
+
expect(container.children).toHaveLength(1);
|
|
38
|
+
expect(feedbackTarget).not.toBe(target);
|
|
39
|
+
expect(feedbackTarget.children).toHaveLength(1);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from "@lynx-js/react";
|
|
2
|
+
import { useScaleFeedback, type UseScaleFeedbackOptions } from "../../hooks/useScaleFeedback";
|
|
3
|
+
import type { LynxViewRef } from "../../types";
|
|
4
|
+
import { mergeProps } from "../../utils/merge-props";
|
|
5
|
+
|
|
6
|
+
export interface ScaleFeedbackProps extends UseScaleFeedbackOptions {
|
|
7
|
+
/** Content rendered inside the native element that receives Self Scale Feedback. */
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Applies Self Scale Feedback through a native Lynx view.
|
|
13
|
+
*/
|
|
14
|
+
export const ScaleFeedback = React.forwardRef<unknown, ScaleFeedbackProps>(
|
|
15
|
+
({ children, ...options }, ref) => {
|
|
16
|
+
const { scaleFeedbackTriggerProps, scaleFeedbackTargetProps } = useScaleFeedback(options);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<view
|
|
20
|
+
{...mergeProps(
|
|
21
|
+
ref ? { ref: ref as LynxViewRef } : {},
|
|
22
|
+
scaleFeedbackTriggerProps,
|
|
23
|
+
scaleFeedbackTargetProps,
|
|
24
|
+
)}
|
|
25
|
+
>
|
|
26
|
+
{children}
|
|
27
|
+
</view>
|
|
28
|
+
);
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
ScaleFeedback.displayName = "ScaleFeedback";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./ScaleFeedback";
|
|
@@ -1,5 +1,5 @@
|
|
|
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
5
|
import * as SegmentedControl from "./SegmentedControl.namespace";
|
|
@@ -62,19 +62,31 @@ describe("SegmentedControl", () => {
|
|
|
62
62
|
expect(disabledItem).toHaveAttribute("accessibility-traits", "disabled");
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
it("keeps the press-start selection
|
|
66
|
-
const
|
|
65
|
+
it("keeps the press-start selection when main-thread feedback hands off to selection", async () => {
|
|
66
|
+
const onValueChange = vi.fn();
|
|
67
|
+
const { container } = render(
|
|
68
|
+
<BasicSegmentedControl defaultValue="hot" onValueChange={onValueChange} />,
|
|
69
|
+
{
|
|
70
|
+
enableMainThread: true,
|
|
71
|
+
enableBackgroundThread: true,
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
await waitSchedule();
|
|
67
75
|
const latest = getItem(container, "최신순");
|
|
68
76
|
const getBackground = () =>
|
|
69
77
|
latest.querySelector<HTMLElement>(".seed-segmented-control__itemBackground");
|
|
70
78
|
|
|
71
79
|
fireEvent.touchstart(latest, {});
|
|
80
|
+
await waitSchedule();
|
|
72
81
|
expect(getBackground()).toHaveClass("seed-segmented-control__itemBackground--pressed_true");
|
|
73
82
|
expect(getBackground()).toHaveClass("seed-segmented-control__itemBackground--selected_false");
|
|
74
83
|
|
|
75
84
|
fireEvent.touchend(latest, {});
|
|
85
|
+
await waitSchedule();
|
|
76
86
|
fireEvent.tap(latest);
|
|
87
|
+
await waitSchedule();
|
|
77
88
|
|
|
89
|
+
expect(onValueChange).toHaveBeenCalledExactlyOnceWith("new");
|
|
78
90
|
expect(getBackground()).toHaveClass("seed-segmented-control__itemBackground--pressed_false");
|
|
79
91
|
expect(getBackground()).toHaveClass("seed-segmented-control__itemBackground--selected_false");
|
|
80
92
|
});
|
|
@@ -4,6 +4,7 @@ import clsx from "clsx";
|
|
|
4
4
|
|
|
5
5
|
import { useControllableState } from "../../hooks/useControllableState";
|
|
6
6
|
import { usePressTap } from "../../hooks/usePressTap";
|
|
7
|
+
import { useScaleFeedback } from "../../hooks/useScaleFeedback";
|
|
7
8
|
import type {
|
|
8
9
|
LynxAccessibilityProps,
|
|
9
10
|
LynxPressableProps,
|
|
@@ -12,6 +13,8 @@ import type {
|
|
|
12
13
|
LynxViewRef,
|
|
13
14
|
} from "../../types";
|
|
14
15
|
import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
|
|
16
|
+
import { HStack } from "../Stack";
|
|
17
|
+
import { mergeProps } from "../../utils/merge-props";
|
|
15
18
|
|
|
16
19
|
interface SegmentedControlContextValue {
|
|
17
20
|
value: string | undefined;
|
|
@@ -105,8 +108,7 @@ export const SegmentedControlRoot = React.forwardRef<unknown, SegmentedControlRo
|
|
|
105
108
|
<SegmentedControlContext.Provider value={contextValue}>
|
|
106
109
|
<ClassNamesProvider value={classNames}>
|
|
107
110
|
<view
|
|
108
|
-
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
109
|
-
{...nativeProps}
|
|
111
|
+
{...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
|
|
110
112
|
accessibility-element={accessibilityElement}
|
|
111
113
|
accessibility-role-description={accessibilityRoleDescription}
|
|
112
114
|
className={clsx(classNames.root, className)}
|
|
@@ -132,6 +134,7 @@ export interface SegmentedControlItemProps
|
|
|
132
134
|
LynxAccessibilityProps,
|
|
133
135
|
LynxPressableProps {
|
|
134
136
|
children: string | number;
|
|
137
|
+
notification?: React.ReactNode;
|
|
135
138
|
value: string;
|
|
136
139
|
disabled?: boolean;
|
|
137
140
|
}
|
|
@@ -140,6 +143,7 @@ export const SegmentedControlItem = React.forwardRef<unknown, SegmentedControlIt
|
|
|
140
143
|
(props, ref) => {
|
|
141
144
|
const {
|
|
142
145
|
children,
|
|
146
|
+
notification,
|
|
143
147
|
className,
|
|
144
148
|
style,
|
|
145
149
|
value: itemValue,
|
|
@@ -170,11 +174,12 @@ export const SegmentedControlItem = React.forwardRef<unknown, SegmentedControlIt
|
|
|
170
174
|
[bindtap, context.selectValue, itemValue, selected],
|
|
171
175
|
);
|
|
172
176
|
const pressSelectionRef = React.useRef(selected);
|
|
173
|
-
const { pressed, bindtouchstart, ...pressHandlers } =
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
const { pressed, bindtouchstart, bindtouchend, bindtouchcancel, ...pressHandlers } =
|
|
178
|
+
usePressTap({
|
|
179
|
+
disabled,
|
|
180
|
+
onTap: handleTap,
|
|
181
|
+
mainThreadOnTap: mainThreadBindtap,
|
|
182
|
+
});
|
|
178
183
|
const handleTouchStart = React.useCallback(
|
|
179
184
|
(...args: Parameters<typeof bindtouchstart>) => {
|
|
180
185
|
pressSelectionRef.current = selected;
|
|
@@ -182,6 +187,12 @@ export const SegmentedControlItem = React.forwardRef<unknown, SegmentedControlIt
|
|
|
182
187
|
},
|
|
183
188
|
[bindtouchstart, selected],
|
|
184
189
|
);
|
|
190
|
+
const { scaleFeedbackTriggerProps, scaleFeedbackTargetProps } = useScaleFeedback({
|
|
191
|
+
disabled,
|
|
192
|
+
onTouchStart: handleTouchStart,
|
|
193
|
+
onTouchEnd: bindtouchend,
|
|
194
|
+
onTouchCancel: bindtouchcancel,
|
|
195
|
+
});
|
|
185
196
|
const classes = segmentedControl({ selected, disabled, pressed });
|
|
186
197
|
const pressStartClasses = segmentedControl({
|
|
187
198
|
selected: pressSelectionRef.current,
|
|
@@ -193,10 +204,12 @@ export const SegmentedControlItem = React.forwardRef<unknown, SegmentedControlIt
|
|
|
193
204
|
|
|
194
205
|
return (
|
|
195
206
|
<view
|
|
196
|
-
{...(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
207
|
+
{...mergeProps(
|
|
208
|
+
ref ? { ref: ref as LynxViewRef } : {},
|
|
209
|
+
scaleFeedbackTriggerProps,
|
|
210
|
+
pressHandlers,
|
|
211
|
+
nativeProps,
|
|
212
|
+
)}
|
|
200
213
|
accessibility-element={accessibilityElement}
|
|
201
214
|
accessibility-label={accessibilityLabel ?? label}
|
|
202
215
|
accessibility-role-description={accessibilityRoleDescription}
|
|
@@ -208,7 +221,16 @@ export const SegmentedControlItem = React.forwardRef<unknown, SegmentedControlIt
|
|
|
208
221
|
style={style}
|
|
209
222
|
>
|
|
210
223
|
<view accessibility-elements-hidden={true} className={pressStartClasses.itemBackground} />
|
|
211
|
-
<
|
|
224
|
+
<view className={classes.itemContent} {...scaleFeedbackTargetProps}>
|
|
225
|
+
{notification ? (
|
|
226
|
+
<HStack position="relative" align="flex-start">
|
|
227
|
+
<text className={classes.label}>{children}</text>
|
|
228
|
+
<view accessibility-elements-hidden={true}>{notification}</view>
|
|
229
|
+
</HStack>
|
|
230
|
+
) : (
|
|
231
|
+
<text className={classes.label}>{children}</text>
|
|
232
|
+
)}
|
|
233
|
+
</view>
|
|
212
234
|
</view>
|
|
213
235
|
);
|
|
214
236
|
},
|
|
@@ -224,8 +246,7 @@ export const SegmentedControlIndicator = React.forwardRef<unknown, SegmentedCont
|
|
|
224
246
|
|
|
225
247
|
return (
|
|
226
248
|
<view
|
|
227
|
-
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
228
|
-
{...nativeProps}
|
|
249
|
+
{...mergeProps(ref ? { ref: ref as LynxViewRef } : {}, nativeProps)}
|
|
229
250
|
accessibility-elements-hidden={true}
|
|
230
251
|
className={clsx(classNames.indicator, className)}
|
|
231
252
|
style={style}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "@testing-library/jest-dom";
|
|
2
|
-
import { act, fireEvent, render } from "@lynx-js/react/testing-library";
|
|
2
|
+
import { act, fireEvent, render, waitSchedule } from "@lynx-js/react/testing-library";
|
|
3
3
|
import { describe, expect, it, vi } from "vitest";
|
|
4
4
|
|
|
5
5
|
import { RadioGroup } from "../RadioGroup";
|
|
@@ -29,6 +29,36 @@ function CheckItem() {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
describe("SelectBox", () => {
|
|
32
|
+
it("shares checkbox touch state with content feedback and cancels without selecting", async () => {
|
|
33
|
+
const onCheckedChange = vi.fn();
|
|
34
|
+
const { container } = render(
|
|
35
|
+
<CheckSelectBox.Root onCheckedChange={onCheckedChange}>
|
|
36
|
+
<CheckSelectBox.Trigger>
|
|
37
|
+
<CheckSelectBox.Label>선택</CheckSelectBox.Label>
|
|
38
|
+
</CheckSelectBox.Trigger>
|
|
39
|
+
</CheckSelectBox.Root>,
|
|
40
|
+
{ enableMainThread: true, enableBackgroundThread: true },
|
|
41
|
+
);
|
|
42
|
+
await waitSchedule();
|
|
43
|
+
const trigger = container.querySelector<HTMLElement>(".seed-select-box__interactionRoot")!;
|
|
44
|
+
const surface = container.querySelector(".seed-select-box__root")!;
|
|
45
|
+
const content = container.querySelector(".seed-select-box__scaleContent")!;
|
|
46
|
+
expect(content).toHaveAttribute("flatten", "false");
|
|
47
|
+
expect(content).not.toContainElement(surface.querySelector(".seed-select-box__selectedStroke"));
|
|
48
|
+
|
|
49
|
+
fireEvent.touchstart(trigger, {});
|
|
50
|
+
await waitSchedule();
|
|
51
|
+
expect(surface).toHaveClass("seed-select-box__root--pressed_true");
|
|
52
|
+
fireEvent.touchcancel(trigger, {});
|
|
53
|
+
await waitSchedule();
|
|
54
|
+
expect(surface).toHaveClass("seed-select-box__root--pressed_false");
|
|
55
|
+
expect(onCheckedChange).not.toHaveBeenCalled();
|
|
56
|
+
|
|
57
|
+
fireEvent.tap(trigger);
|
|
58
|
+
await waitSchedule();
|
|
59
|
+
expect(onCheckedChange).toHaveBeenCalledExactlyOnceWith(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
32
62
|
it("lays out custom label content in a view", () => {
|
|
33
63
|
render(
|
|
34
64
|
<CheckSelectBox.Root accessibility-label="Melon New">
|
|
@@ -137,6 +167,9 @@ describe("SelectBox", () => {
|
|
|
137
167
|
<RadioSelectBox.Item value="second" accessibility-label="두 번째">
|
|
138
168
|
<RadioSelectBox.Trigger>
|
|
139
169
|
<RadioSelectBox.Label>두 번째</RadioSelectBox.Label>
|
|
170
|
+
<RadioGroup.ItemControl>
|
|
171
|
+
<RadioGroup.ItemIndicator />
|
|
172
|
+
</RadioGroup.ItemControl>
|
|
140
173
|
</RadioSelectBox.Trigger>
|
|
141
174
|
</RadioSelectBox.Item>
|
|
142
175
|
<RadioSelectBox.Item value="disabled" accessibility-label="비활성" disabled>
|
|
@@ -153,6 +186,11 @@ describe("SelectBox", () => {
|
|
|
153
186
|
".seed-select-box__interactionRoot",
|
|
154
187
|
);
|
|
155
188
|
const surfaces = root.querySelectorAll<HTMLElement>(".seed-select-box__root");
|
|
189
|
+
const radioContent = surfaces[1].querySelector(".seed-select-box__scaleContent");
|
|
190
|
+
const radioMark = surfaces[1].querySelector(".seed-radiomark__root");
|
|
191
|
+
|
|
192
|
+
expect(radioContent).toHaveAttribute("flatten", "false");
|
|
193
|
+
expect(radioMark).not.toHaveAttribute("flatten", "false");
|
|
156
194
|
|
|
157
195
|
expect(surfaces[0]).toHaveAttribute("accessibility-value", "selected");
|
|
158
196
|
expect(surfaces[1]).toHaveAttribute("accessibility-value", "not selected");
|