@seed-design/lynx-react 0.5.1 → 0.6.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.
Files changed (99) hide show
  1. package/lib/components/Checkbox/Checkbox.d.ts +26 -0
  2. package/lib/components/Checkbox/Checkbox.d.ts.map +1 -1
  3. package/lib/components/Checkbox/Checkbox.jsx +1 -1
  4. package/lib/components/Icon/Icon.d.ts +5 -0
  5. package/lib/components/Icon/Icon.d.ts.map +1 -1
  6. package/lib/components/Icon/Icon.jsx +14 -3
  7. package/lib/components/MannerTemp/MannerTemp.d.ts +19 -0
  8. package/lib/components/MannerTemp/MannerTemp.d.ts.map +1 -0
  9. package/lib/components/MannerTemp/MannerTemp.jsx +47 -0
  10. package/lib/components/MannerTemp/index.d.ts +2 -0
  11. package/lib/components/MannerTemp/index.d.ts.map +1 -0
  12. package/lib/components/MannerTempBadge/MannerTempBadge.d.ts +13 -0
  13. package/lib/components/MannerTempBadge/MannerTempBadge.d.ts.map +1 -0
  14. package/lib/components/MannerTempBadge/MannerTempBadge.jsx +15 -0
  15. package/lib/components/MannerTempBadge/index.d.ts +2 -0
  16. package/lib/components/MannerTempBadge/index.d.ts.map +1 -0
  17. package/lib/components/NotificationBadge/NotificationBadge.d.ts +18 -0
  18. package/lib/components/NotificationBadge/NotificationBadge.d.ts.map +1 -0
  19. package/lib/components/NotificationBadge/NotificationBadge.jsx +34 -0
  20. package/lib/components/NotificationBadge/index.d.ts +2 -0
  21. package/lib/components/NotificationBadge/index.d.ts.map +1 -0
  22. package/lib/components/PageBanner/PageBanner.d.ts +35 -0
  23. package/lib/components/PageBanner/PageBanner.d.ts.map +1 -0
  24. package/lib/components/PageBanner/PageBanner.jsx +150 -0
  25. package/lib/components/PageBanner/PageBanner.namespace.d.ts +2 -0
  26. package/lib/components/PageBanner/PageBanner.namespace.d.ts.map +1 -0
  27. package/lib/components/PageBanner/PageBanner.namespace.js +14 -0
  28. package/lib/components/PageBanner/index.d.ts +3 -0
  29. package/lib/components/PageBanner/index.d.ts.map +1 -0
  30. package/lib/components/RadioGroup/RadioGroup.d.ts +9 -0
  31. package/lib/components/RadioGroup/RadioGroup.d.ts.map +1 -1
  32. package/lib/components/RadioGroup/RadioGroup.jsx +3 -2
  33. package/lib/components/SegmentedControl/SegmentedControl.d.ts +27 -0
  34. package/lib/components/SegmentedControl/SegmentedControl.d.ts.map +1 -0
  35. package/lib/components/SegmentedControl/SegmentedControl.jsx +120 -0
  36. package/lib/components/SegmentedControl/SegmentedControl.namespace.d.ts +2 -0
  37. package/lib/components/SegmentedControl/SegmentedControl.namespace.d.ts.map +1 -0
  38. package/lib/components/SegmentedControl/SegmentedControl.namespace.js +10 -0
  39. package/lib/components/SegmentedControl/index.d.ts +3 -0
  40. package/lib/components/SegmentedControl/index.d.ts.map +1 -0
  41. package/lib/components/SelectBox/CheckSelectBox.namespace.d.ts +2 -0
  42. package/lib/components/SelectBox/CheckSelectBox.namespace.d.ts.map +1 -0
  43. package/lib/components/SelectBox/CheckSelectBox.namespace.js +17 -0
  44. package/lib/components/SelectBox/RadioSelectBox.namespace.d.ts +2 -0
  45. package/lib/components/SelectBox/RadioSelectBox.namespace.d.ts.map +1 -0
  46. package/lib/components/SelectBox/RadioSelectBox.namespace.js +15 -0
  47. package/lib/components/SelectBox/SelectBox.d.ts +85 -0
  48. package/lib/components/SelectBox/SelectBox.d.ts.map +1 -0
  49. package/lib/components/SelectBox/SelectBox.jsx +237 -0
  50. package/lib/components/SelectBox/index.d.ts +4 -0
  51. package/lib/components/SelectBox/index.d.ts.map +1 -0
  52. package/lib/components/Tabs/Tabs.d.ts +1 -1
  53. package/lib/components/Tabs/Tabs.d.ts.map +1 -1
  54. package/lib/components/Tabs/Tabs.jsx +10 -3
  55. package/lib/components/Tabs/Tabs.utils.d.ts +1 -0
  56. package/lib/components/Tabs/Tabs.utils.d.ts.map +1 -1
  57. package/lib/components/Tabs/Tabs.utils.js +4 -1
  58. package/lib/components/TextField/TextField.d.ts.map +1 -1
  59. package/lib/components/TextField/TextField.jsx +4 -0
  60. package/lib/components/index.d.ts +6 -0
  61. package/lib/components/index.d.ts.map +1 -1
  62. package/lib/hooks/useIconColor.d.ts +1 -0
  63. package/lib/hooks/useIconColor.d.ts.map +1 -1
  64. package/lib/hooks/useIconColor.js +4 -1
  65. package/lib/index.js +11 -1
  66. package/package.json +3 -3
  67. package/src/components/Checkbox/Checkbox.tsx +1 -1
  68. package/src/components/Icon/Icon.test.tsx +25 -1
  69. package/src/components/Icon/Icon.tsx +22 -1
  70. package/src/components/MannerTemp/MannerTemp.test.tsx +41 -0
  71. package/src/components/MannerTemp/MannerTemp.tsx +85 -0
  72. package/src/components/MannerTemp/index.ts +1 -0
  73. package/src/components/MannerTempBadge/MannerTempBadge.test.tsx +35 -0
  74. package/src/components/MannerTempBadge/MannerTempBadge.tsx +34 -0
  75. package/src/components/MannerTempBadge/index.ts +1 -0
  76. package/src/components/NotificationBadge/NotificationBadge.test.tsx +28 -0
  77. package/src/components/NotificationBadge/NotificationBadge.tsx +81 -0
  78. package/src/components/NotificationBadge/index.ts +1 -0
  79. package/src/components/PageBanner/PageBanner.namespace.ts +16 -0
  80. package/src/components/PageBanner/PageBanner.test.tsx +124 -0
  81. package/src/components/PageBanner/PageBanner.tsx +340 -0
  82. package/src/components/PageBanner/index.ts +18 -0
  83. package/src/components/RadioGroup/RadioGroup.tsx +3 -2
  84. package/src/components/SegmentedControl/SegmentedControl.namespace.ts +8 -0
  85. package/src/components/SegmentedControl/SegmentedControl.test.tsx +81 -0
  86. package/src/components/SegmentedControl/SegmentedControl.tsx +236 -0
  87. package/src/components/SegmentedControl/index.ts +10 -0
  88. package/src/components/SelectBox/CheckSelectBox.namespace.ts +22 -0
  89. package/src/components/SelectBox/RadioSelectBox.namespace.ts +18 -0
  90. package/src/components/SelectBox/SelectBox.test.tsx +169 -0
  91. package/src/components/SelectBox/SelectBox.tsx +533 -0
  92. package/src/components/SelectBox/index.ts +3 -0
  93. package/src/components/Tabs/Tabs.test.tsx +26 -1
  94. package/src/components/Tabs/Tabs.tsx +14 -2
  95. package/src/components/Tabs/Tabs.utils.ts +7 -0
  96. package/src/components/TextField/TextField.test.tsx +48 -0
  97. package/src/components/TextField/TextField.tsx +8 -1
  98. package/src/components/index.ts +6 -0
  99. package/src/hooks/useIconColor.ts +6 -1
@@ -0,0 +1,236 @@
1
+ import { segmentedControl } from "@seed-design/lynx-css/recipes/segmented-control";
2
+ import * as React from "@lynx-js/react";
3
+ import clsx from "clsx";
4
+
5
+ import { useControllableState } from "../../hooks/useControllableState";
6
+ import { usePressTap } from "../../hooks/usePressTap";
7
+ import type {
8
+ LynxAccessibilityProps,
9
+ LynxPressableProps,
10
+ LynxStyledElementProps,
11
+ LynxViewProps,
12
+ LynxViewRef,
13
+ } from "../../types";
14
+ import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
15
+
16
+ interface SegmentedControlContextValue {
17
+ value: string | undefined;
18
+ disabled: boolean;
19
+ items: string[];
20
+ selectValue: (value: string) => void;
21
+ registerItem: (value: string) => () => void;
22
+ }
23
+
24
+ const SegmentedControlContext = React.createContext<SegmentedControlContextValue | null>(null);
25
+ const { ClassNamesProvider, useClassNames } = createSlotRecipeContext(segmentedControl);
26
+
27
+ function useSegmentedControlContext(consumer: string) {
28
+ const context = React.useContext(SegmentedControlContext);
29
+ if (!context) {
30
+ throw new Error(`<${consumer}/> must be rendered inside <SegmentedControlRoot/>.`);
31
+ }
32
+ return context;
33
+ }
34
+
35
+ /**
36
+ * @platform Lynx
37
+ *
38
+ * 웹 대비 미지원 기능:
39
+ * - HiddenInput, name, form: Lynx에 HTML form 제출 모델이 없음
40
+ * - focus, focusVisible: Lynx에 키보드 포커스 모델이 없음
41
+ * - raw DOM change event: 선택 변경은 onValueChange로 노출
42
+ */
43
+ export interface SegmentedControlRootProps extends LynxStyledElementProps, LynxAccessibilityProps {
44
+ value?: string;
45
+ defaultValue?: string;
46
+ disabled?: boolean;
47
+ onValueChange?: (value: string) => void;
48
+ }
49
+
50
+ export const SegmentedControlRoot = React.forwardRef<unknown, SegmentedControlRootProps>(
51
+ (props, ref) => {
52
+ const {
53
+ children,
54
+ className,
55
+ style,
56
+ value: valueProp,
57
+ defaultValue,
58
+ disabled = false,
59
+ onValueChange,
60
+ "accessibility-element": accessibilityElement = true,
61
+ "accessibility-role-description": accessibilityRoleDescription = "radiogroup",
62
+ ...nativeProps
63
+ } = props;
64
+ const [value, setValue] = useControllableState<string | undefined>({
65
+ value: valueProp,
66
+ defaultValue,
67
+ onChange(nextValue) {
68
+ "background only";
69
+ if (nextValue !== undefined) onValueChange?.(nextValue);
70
+ },
71
+ });
72
+ const [items, setItems] = React.useState<string[]>([]);
73
+
74
+ const registerItem = React.useCallback((itemValue: string) => {
75
+ setItems((current) => {
76
+ if (current.includes(itemValue)) return current;
77
+ return [...current, itemValue];
78
+ });
79
+ return () => {
80
+ "background only";
81
+ setItems((current) => current.filter((item) => item !== itemValue));
82
+ };
83
+ }, []);
84
+
85
+ const selectValue = React.useCallback(
86
+ (nextValue: string) => {
87
+ if (!disabled) setValue(nextValue);
88
+ },
89
+ [disabled, setValue],
90
+ );
91
+ const selectedIndex = items.indexOf(value ?? "");
92
+ const classNames = segmentedControl({ hasSelection: selectedIndex >= 0 });
93
+ const contextValue = React.useMemo<SegmentedControlContextValue>(
94
+ () => ({
95
+ value,
96
+ disabled,
97
+ items,
98
+ selectValue,
99
+ registerItem,
100
+ }),
101
+ [value, disabled, items, selectValue, registerItem],
102
+ );
103
+
104
+ return (
105
+ <SegmentedControlContext.Provider value={contextValue}>
106
+ <ClassNamesProvider value={classNames}>
107
+ <view
108
+ {...(ref ? { ref: ref as LynxViewRef } : {})}
109
+ {...nativeProps}
110
+ accessibility-element={accessibilityElement}
111
+ accessibility-role-description={accessibilityRoleDescription}
112
+ className={clsx(classNames.root, className)}
113
+ style={
114
+ {
115
+ "--segment-count": Math.max(items.length, 1).toString(),
116
+ "--segment-index": Math.max(selectedIndex, 0).toString(),
117
+ ...style,
118
+ } as LynxViewProps["style"]
119
+ }
120
+ >
121
+ {children}
122
+ </view>
123
+ </ClassNamesProvider>
124
+ </SegmentedControlContext.Provider>
125
+ );
126
+ },
127
+ );
128
+ SegmentedControlRoot.displayName = "SegmentedControlRoot";
129
+
130
+ export interface SegmentedControlItemProps
131
+ extends Omit<LynxStyledElementProps, "children">,
132
+ LynxAccessibilityProps,
133
+ LynxPressableProps {
134
+ children: string | number;
135
+ value: string;
136
+ disabled?: boolean;
137
+ }
138
+
139
+ export const SegmentedControlItem = React.forwardRef<unknown, SegmentedControlItemProps>(
140
+ (props, ref) => {
141
+ const {
142
+ children,
143
+ className,
144
+ style,
145
+ value: itemValue,
146
+ disabled: itemDisabled = false,
147
+ bindtap,
148
+ "main-thread:bindtap": mainThreadBindtap,
149
+ "accessibility-element": accessibilityElement = true,
150
+ "accessibility-label": accessibilityLabel,
151
+ "accessibility-role-description": accessibilityRoleDescription = "radio",
152
+ "accessibility-traits": accessibilityTraits,
153
+ ...nativeProps
154
+ } = props;
155
+ const context = useSegmentedControlContext("SegmentedControlItem");
156
+ const disabled = context.disabled || itemDisabled;
157
+ const selected = context.value === itemValue;
158
+
159
+ React.useEffect(() => {
160
+ "background only";
161
+ return context.registerItem(itemValue);
162
+ }, [context.registerItem, itemValue]);
163
+
164
+ const handleTap = React.useCallback(
165
+ (...args: Parameters<NonNullable<LynxPressableProps["bindtap"]>>) => {
166
+ "background only";
167
+ if (!selected) context.selectValue(itemValue);
168
+ bindtap?.(...args);
169
+ },
170
+ [bindtap, context.selectValue, itemValue, selected],
171
+ );
172
+ const pressSelectionRef = React.useRef(selected);
173
+ const { pressed, bindtouchstart, ...pressHandlers } = usePressTap({
174
+ disabled,
175
+ onTap: handleTap,
176
+ mainThreadOnTap: mainThreadBindtap,
177
+ });
178
+ const handleTouchStart = React.useCallback(
179
+ (...args: Parameters<typeof bindtouchstart>) => {
180
+ pressSelectionRef.current = selected;
181
+ bindtouchstart(...args);
182
+ },
183
+ [bindtouchstart, selected],
184
+ );
185
+ const classes = segmentedControl({ selected, disabled, pressed });
186
+ const pressStartClasses = segmentedControl({
187
+ selected: pressSelectionRef.current,
188
+ disabled,
189
+ pressed,
190
+ });
191
+ const label =
192
+ typeof children === "string" || typeof children === "number" ? String(children) : undefined;
193
+
194
+ return (
195
+ <view
196
+ {...(ref ? { ref: ref as LynxViewRef } : {})}
197
+ {...nativeProps}
198
+ bindtouchstart={handleTouchStart}
199
+ {...pressHandlers}
200
+ accessibility-element={accessibilityElement}
201
+ accessibility-label={accessibilityLabel ?? label}
202
+ accessibility-role-description={accessibilityRoleDescription}
203
+ accessibility-value={selected ? "selected" : "not selected"}
204
+ accessibility-traits={
205
+ disabled ? "disabled" : selected ? "selected" : (accessibilityTraits ?? "button")
206
+ }
207
+ className={clsx(classes.item, className)}
208
+ style={style}
209
+ >
210
+ <view accessibility-elements-hidden={true} className={pressStartClasses.itemBackground} />
211
+ <text className={classes.label}>{children}</text>
212
+ </view>
213
+ );
214
+ },
215
+ );
216
+ SegmentedControlItem.displayName = "SegmentedControlItem";
217
+
218
+ export interface SegmentedControlIndicatorProps extends LynxStyledElementProps {}
219
+
220
+ export const SegmentedControlIndicator = React.forwardRef<unknown, SegmentedControlIndicatorProps>(
221
+ (props, ref) => {
222
+ const { className, style, ...nativeProps } = props;
223
+ const classNames = useClassNames();
224
+
225
+ return (
226
+ <view
227
+ {...(ref ? { ref: ref as LynxViewRef } : {})}
228
+ {...nativeProps}
229
+ accessibility-elements-hidden={true}
230
+ className={clsx(classNames.indicator, className)}
231
+ style={style}
232
+ />
233
+ );
234
+ },
235
+ );
236
+ SegmentedControlIndicator.displayName = "SegmentedControlIndicator";
@@ -0,0 +1,10 @@
1
+ export {
2
+ SegmentedControlIndicator,
3
+ SegmentedControlItem,
4
+ SegmentedControlRoot,
5
+ type SegmentedControlIndicatorProps,
6
+ type SegmentedControlItemProps,
7
+ type SegmentedControlRootProps,
8
+ } from "./SegmentedControl";
9
+
10
+ export * as SegmentedControl from "./SegmentedControl.namespace";
@@ -0,0 +1,22 @@
1
+ export {
2
+ CheckSelectBoxBody as Body,
3
+ CheckSelectBoxCheckmarkControl as CheckmarkControl,
4
+ CheckSelectBoxCheckmarkIcon as CheckmarkIcon,
5
+ CheckSelectBoxContent as Content,
6
+ CheckSelectBoxDescription as Description,
7
+ CheckSelectBoxFooter as Footer,
8
+ CheckSelectBoxGroup as Group,
9
+ CheckSelectBoxLabel as Label,
10
+ CheckSelectBoxRoot as Root,
11
+ CheckSelectBoxTrigger as Trigger,
12
+ type CheckSelectBoxBodyProps as BodyProps,
13
+ type CheckSelectBoxCheckmarkControlProps as CheckmarkControlProps,
14
+ type CheckSelectBoxCheckmarkIconProps as CheckmarkIconProps,
15
+ type CheckSelectBoxContentProps as ContentProps,
16
+ type CheckSelectBoxDescriptionProps as DescriptionProps,
17
+ type CheckSelectBoxFooterProps as FooterProps,
18
+ type CheckSelectBoxGroupProps as GroupProps,
19
+ type CheckSelectBoxLabelProps as LabelProps,
20
+ type CheckSelectBoxRootProps as RootProps,
21
+ type CheckSelectBoxTriggerProps as TriggerProps,
22
+ } from "./SelectBox";
@@ -0,0 +1,18 @@
1
+ export {
2
+ RadioSelectBoxBody as Body,
3
+ RadioSelectBoxContent as Content,
4
+ RadioSelectBoxDescription as Description,
5
+ RadioSelectBoxFooter as Footer,
6
+ RadioSelectBoxGroup as Group,
7
+ RadioSelectBoxItem as Item,
8
+ RadioSelectBoxLabel as Label,
9
+ RadioSelectBoxTrigger as Trigger,
10
+ type RadioSelectBoxBodyProps as BodyProps,
11
+ type RadioSelectBoxContentProps as ContentProps,
12
+ type RadioSelectBoxDescriptionProps as DescriptionProps,
13
+ type RadioSelectBoxFooterProps as FooterProps,
14
+ type RadioSelectBoxGroupProps as GroupProps,
15
+ type RadioSelectBoxItemProps as ItemProps,
16
+ type RadioSelectBoxLabelProps as LabelProps,
17
+ type RadioSelectBoxTriggerProps as TriggerProps,
18
+ } from "./SelectBox";
@@ -0,0 +1,169 @@
1
+ import "@testing-library/jest-dom";
2
+ import { act, fireEvent, render } from "@lynx-js/react/testing-library";
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import { RadioGroup } from "../RadioGroup";
6
+ import { CheckSelectBox, RadioSelectBox } from "./index";
7
+
8
+ function getRenderedRoot() {
9
+ const root = elementTree.root;
10
+ if (!root) throw new Error("Expected Lynx render root to exist.");
11
+ return root;
12
+ }
13
+
14
+ function CheckItem() {
15
+ return (
16
+ <CheckSelectBox.Root accessibility-label="마케팅 정보 수신">
17
+ <CheckSelectBox.Trigger>
18
+ <CheckSelectBox.Content>
19
+ <CheckSelectBox.Body>
20
+ <CheckSelectBox.Label>마케팅 정보 수신</CheckSelectBox.Label>
21
+ </CheckSelectBox.Body>
22
+ </CheckSelectBox.Content>
23
+ </CheckSelectBox.Trigger>
24
+ <CheckSelectBox.Footer className="check-footer">
25
+ <text>혜택 알림을 받습니다.</text>
26
+ </CheckSelectBox.Footer>
27
+ </CheckSelectBox.Root>
28
+ );
29
+ }
30
+
31
+ describe("SelectBox", () => {
32
+ it("lays out custom label content in a view", () => {
33
+ render(
34
+ <CheckSelectBox.Root accessibility-label="Melon New">
35
+ <CheckSelectBox.Trigger>
36
+ <CheckSelectBox.Label>
37
+ <text>Melon</text>
38
+ <view className="label-badge">
39
+ <text>New</text>
40
+ </view>
41
+ </CheckSelectBox.Label>
42
+ </CheckSelectBox.Trigger>
43
+ </CheckSelectBox.Root>,
44
+ );
45
+
46
+ const label = getRenderedRoot().querySelector<HTMLElement>(".seed-select-box__label");
47
+
48
+ expect(label?.tagName.toLowerCase()).toBe("view");
49
+ expect(label?.querySelector(".label-badge")).toBeInTheDocument();
50
+ });
51
+
52
+ it("renders an initially open footer without starting from zero height", () => {
53
+ render(
54
+ <CheckSelectBox.Root accessibility-label="마케팅 정보 수신" defaultChecked>
55
+ <CheckSelectBox.Trigger>
56
+ <CheckSelectBox.Label>마케팅 정보 수신</CheckSelectBox.Label>
57
+ </CheckSelectBox.Trigger>
58
+ <CheckSelectBox.Footer className="check-footer">
59
+ <text>혜택 알림을 받습니다.</text>
60
+ </CheckSelectBox.Footer>
61
+ </CheckSelectBox.Root>,
62
+ );
63
+
64
+ const root = getRenderedRoot();
65
+ const footer = root.querySelector<HTMLElement>(".check-footer");
66
+ const footerInner = footer?.querySelector<HTMLElement>(".seed-select-box__footerInner");
67
+
68
+ expect(footer).toHaveStyle({ height: "auto" });
69
+
70
+ act(() => {
71
+ fireEvent.layoutchange(footerInner as HTMLElement, { height: 48 });
72
+ });
73
+
74
+ expect(footer).toHaveStyle({ height: "48px" });
75
+ });
76
+
77
+ it("toggles a check item and reveals the measured footer", () => {
78
+ const onCheckedChange = vi.fn();
79
+ render(
80
+ <CheckSelectBox.Root accessibility-label="마케팅 정보 수신" onCheckedChange={onCheckedChange}>
81
+ <CheckSelectBox.Trigger>
82
+ <CheckSelectBox.Label>마케팅 정보 수신</CheckSelectBox.Label>
83
+ </CheckSelectBox.Trigger>
84
+ <CheckSelectBox.Footer className="check-footer">
85
+ <text>혜택 알림을 받습니다.</text>
86
+ </CheckSelectBox.Footer>
87
+ </CheckSelectBox.Root>,
88
+ );
89
+
90
+ const root = getRenderedRoot();
91
+ const interactionRoot = root.querySelector<HTMLElement>(".seed-select-box__interactionRoot");
92
+ const surface = root.querySelector<HTMLElement>(".seed-select-box__root");
93
+ const footer = root.querySelector<HTMLElement>(".check-footer");
94
+ const footerInner = footer?.querySelector<HTMLElement>(".seed-select-box__footerInner");
95
+
96
+ expect(surface).toHaveAttribute("accessibility-value", "not selected");
97
+ expect(footer).toHaveStyle({ height: "0px" });
98
+ expect(footer).toHaveAttribute("accessibility-elements-hidden", "true");
99
+
100
+ act(() => {
101
+ fireEvent.layoutchange(footerInner as HTMLElement, { height: 48 });
102
+ });
103
+ fireEvent.tap(interactionRoot as HTMLElement);
104
+
105
+ expect(onCheckedChange).toHaveBeenCalledWith(true);
106
+ expect(surface).toHaveAttribute("accessibility-value", "selected");
107
+ expect(footer).toHaveStyle({ height: "48px" });
108
+ expect(footer).toHaveAttribute("accessibility-elements-hidden", "false");
109
+ });
110
+
111
+ it("uses vertical item layout for a multi-column group", () => {
112
+ render(
113
+ <CheckSelectBox.Group columns={2} className="select-box-group">
114
+ <CheckItem />
115
+ <CheckItem />
116
+ </CheckSelectBox.Group>,
117
+ );
118
+
119
+ const root = getRenderedRoot();
120
+ const group = root.querySelector<HTMLElement>(".select-box-group");
121
+ const trigger = root.querySelector<HTMLElement>(".seed-select-box__trigger");
122
+
123
+ expect(group).toHaveStyle({ gridTemplateColumns: "repeat(2, 1fr)" });
124
+ expect(trigger?.className).toContain("seed-select-box__trigger--layout_vertical");
125
+ });
126
+
127
+ it("selects one radio item and ignores a disabled item", () => {
128
+ const onValueChange = vi.fn();
129
+ render(
130
+ <RadioGroup.Root defaultValue="first" onValueChange={onValueChange}>
131
+ <RadioSelectBox.Group>
132
+ <RadioSelectBox.Item value="first" accessibility-label="첫 번째">
133
+ <RadioSelectBox.Trigger>
134
+ <RadioSelectBox.Label>첫 번째</RadioSelectBox.Label>
135
+ </RadioSelectBox.Trigger>
136
+ </RadioSelectBox.Item>
137
+ <RadioSelectBox.Item value="second" accessibility-label="두 번째">
138
+ <RadioSelectBox.Trigger>
139
+ <RadioSelectBox.Label>두 번째</RadioSelectBox.Label>
140
+ </RadioSelectBox.Trigger>
141
+ </RadioSelectBox.Item>
142
+ <RadioSelectBox.Item value="disabled" accessibility-label="비활성" disabled>
143
+ <RadioSelectBox.Trigger>
144
+ <RadioSelectBox.Label>비활성</RadioSelectBox.Label>
145
+ </RadioSelectBox.Trigger>
146
+ </RadioSelectBox.Item>
147
+ </RadioSelectBox.Group>
148
+ </RadioGroup.Root>,
149
+ );
150
+
151
+ const root = getRenderedRoot();
152
+ const interactionRoots = root.querySelectorAll<HTMLElement>(
153
+ ".seed-select-box__interactionRoot",
154
+ );
155
+ const surfaces = root.querySelectorAll<HTMLElement>(".seed-select-box__root");
156
+
157
+ expect(surfaces[0]).toHaveAttribute("accessibility-value", "selected");
158
+ expect(surfaces[1]).toHaveAttribute("accessibility-value", "not selected");
159
+
160
+ fireEvent.tap(interactionRoots[1]);
161
+ expect(onValueChange).toHaveBeenCalledWith("second");
162
+ expect(surfaces[0]).toHaveAttribute("accessibility-value", "not selected");
163
+ expect(surfaces[1]).toHaveAttribute("accessibility-value", "selected");
164
+
165
+ fireEvent.tap(interactionRoots[2]);
166
+ expect(onValueChange).toHaveBeenCalledTimes(1);
167
+ expect(surfaces[2]).toHaveAttribute("accessibility-traits", "disabled");
168
+ });
169
+ });