@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,124 @@
1
+ import "@testing-library/jest-dom";
2
+ import { fireEvent, render } from "@lynx-js/react/testing-library";
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import * as PageBanner from "./PageBanner.namespace";
6
+
7
+ function getRenderedRoot() {
8
+ const root = elementTree.root;
9
+
10
+ if (!root) {
11
+ throw new Error("Expected Lynx render root to exist.");
12
+ }
13
+
14
+ return root;
15
+ }
16
+
17
+ function getPageBannerRoot() {
18
+ const root = getRenderedRoot();
19
+
20
+ if (root.classList.contains("seed-page-banner__root")) return root;
21
+
22
+ const pageBannerRoot = root.querySelector<HTMLElement>(".seed-page-banner__root");
23
+ if (!pageBannerRoot) {
24
+ throw new Error("Expected PageBanner root to exist.");
25
+ }
26
+
27
+ return pageBannerRoot;
28
+ }
29
+
30
+ describe("PageBanner", () => {
31
+ it("renders compound slots with the default neutral weak style", () => {
32
+ render(
33
+ <PageBanner.Root>
34
+ <PageBanner.Content>
35
+ <PageBanner.Body>
36
+ <PageBanner.Title>Title</PageBanner.Title>
37
+ <PageBanner.Description>Description</PageBanner.Description>
38
+ </PageBanner.Body>
39
+ <PageBanner.Button>Action</PageBanner.Button>
40
+ </PageBanner.Content>
41
+ <PageBanner.CloseButton accessibility-label="Close" />
42
+ </PageBanner.Root>,
43
+ );
44
+
45
+ const root = getPageBannerRoot();
46
+
47
+ expect(root).toHaveClass("seed-page-banner__root--tone_neutral-variant_weak");
48
+ expect(root.querySelector(".seed-page-banner__content")?.tagName.toLowerCase()).toBe("view");
49
+ expect(root.querySelector(".seed-page-banner__body")?.tagName.toLowerCase()).toBe("text");
50
+ expect(root.querySelector(".seed-page-banner__title")).toHaveClass(
51
+ "seed-page-banner__title--tone_neutral-variant_weak",
52
+ );
53
+ expect(root.querySelector(".seed-page-banner__description")).toHaveClass(
54
+ "seed-page-banner__description--tone_neutral-variant_weak",
55
+ );
56
+ expect(root.querySelector(".seed-page-banner__button")).toHaveAttribute(
57
+ "accessibility-traits",
58
+ "button",
59
+ );
60
+ });
61
+
62
+ it("tracks pressed state and maps actionable accessibility", () => {
63
+ const onTap = vi.fn();
64
+
65
+ render(
66
+ <PageBanner.Root tone="positive" variant="solid" bindtap={onTap}>
67
+ <PageBanner.Content>
68
+ <PageBanner.Body>
69
+ <PageBanner.Description>Actionable</PageBanner.Description>
70
+ </PageBanner.Body>
71
+ </PageBanner.Content>
72
+ </PageBanner.Root>,
73
+ );
74
+
75
+ const root = getPageBannerRoot();
76
+
77
+ expect(root).toHaveAttribute("accessibility-element", "true");
78
+ expect(root).toHaveAttribute("accessibility-traits", "button");
79
+
80
+ fireEvent.touchstart(root, {});
81
+ expect(root).toHaveClass("seed-page-banner__root--tone_positive-variant_solid-pressed_true");
82
+
83
+ fireEvent.tap(root);
84
+ expect(onTap).toHaveBeenCalledTimes(1);
85
+ });
86
+
87
+ it("dismisses an uncontrolled banner and composes the close tap handler", () => {
88
+ const onTap = vi.fn();
89
+ const onDismiss = vi.fn();
90
+
91
+ render(
92
+ <PageBanner.Root onDismiss={onDismiss}>
93
+ <PageBanner.CloseButton bindtap={onTap} accessibility-label="Close" />
94
+ </PageBanner.Root>,
95
+ );
96
+
97
+ const closeButton = getPageBannerRoot().querySelector(".seed-page-banner__closeButton");
98
+ if (!closeButton) throw new Error("Expected close button to exist.");
99
+
100
+ fireEvent.tap(closeButton);
101
+
102
+ expect(onTap).toHaveBeenCalledTimes(1);
103
+ expect(onDismiss).toHaveBeenCalledTimes(1);
104
+ expect(getRenderedRoot().querySelector(".seed-page-banner__root")).not.toBeInTheDocument();
105
+ });
106
+
107
+ it("notifies without hiding a controlled banner", () => {
108
+ const onDismiss = vi.fn();
109
+
110
+ render(
111
+ <PageBanner.Root open onDismiss={onDismiss}>
112
+ <PageBanner.CloseButton accessibility-label="Close" />
113
+ </PageBanner.Root>,
114
+ );
115
+
116
+ const closeButton = getPageBannerRoot().querySelector(".seed-page-banner__closeButton");
117
+ if (!closeButton) throw new Error("Expected close button to exist.");
118
+
119
+ fireEvent.tap(closeButton);
120
+
121
+ expect(onDismiss).toHaveBeenCalledTimes(1);
122
+ expect(getPageBannerRoot()).toBeInTheDocument();
123
+ });
124
+ });
@@ -0,0 +1,340 @@
1
+ import { pageBanner, type PageBannerVariantProps } from "@seed-design/lynx-css/recipes/page-banner";
2
+ import * as React from "@lynx-js/react";
3
+ import { useMemoizedFn } from "@lynx-js/lynx-ui-common";
4
+ import clsx from "clsx";
5
+
6
+ import { useControllableState } from "../../hooks/useControllableState";
7
+ import { usePressTap } from "../../hooks/usePressTap";
8
+ import type {
9
+ LynxAccessibilityProps,
10
+ LynxPressableProps,
11
+ LynxStyledElementProps,
12
+ LynxTextRef,
13
+ LynxViewProps,
14
+ LynxViewRef,
15
+ } from "../../types";
16
+ import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
17
+ import { IconSlotProvider } from "../Icon/Icon";
18
+
19
+ const { ClassNamesProvider, PropsProvider, useClassNames, useProps } =
20
+ createSlotRecipeContext(pageBanner);
21
+
22
+ type TapHandler = NonNullable<LynxViewProps["bindtap"]>;
23
+
24
+ interface PageBannerContextValue {
25
+ dismiss: () => void;
26
+ }
27
+
28
+ const PageBannerContext = React.createContext<PageBannerContextValue | null>(null);
29
+
30
+ function usePageBannerContext(consumer: string) {
31
+ const context = React.useContext(PageBannerContext);
32
+
33
+ if (!context) {
34
+ throw new Error(`<${consumer}/> must be rendered inside <PageBannerRoot/>.`);
35
+ }
36
+
37
+ return context;
38
+ }
39
+
40
+ ////////////////////////////////////////////////////////////////////////////////////
41
+
42
+ /**
43
+ * @platform Lynx
44
+ *
45
+ * 웹 대비 차이:
46
+ * - `asChild`와 DOM 이벤트 대신 Lynx native `<view>`와 `bindtap`을 사용합니다.
47
+ * - 웹 focus ring과 동적 scale feedback은 지원하지 않습니다.
48
+ */
49
+ export interface PageBannerRootProps
50
+ extends Omit<PageBannerVariantProps, "pressed" | "closeButtonPressed">,
51
+ LynxStyledElementProps,
52
+ LynxPressableProps,
53
+ LynxAccessibilityProps {
54
+ defaultOpen?: boolean;
55
+ open?: boolean;
56
+ onDismiss?: () => void;
57
+ }
58
+
59
+ export const PageBannerRoot = React.forwardRef<unknown, PageBannerRootProps>((props, ref) => {
60
+ if (
61
+ process.env.NODE_ENV !== "production" &&
62
+ props.variant === "solid" &&
63
+ props.tone === "magic"
64
+ ) {
65
+ console.error(
66
+ '`magic` tone is not available for `solid` variant in PageBanner components. Please use variant="weak" or a different tone instead.',
67
+ );
68
+ }
69
+
70
+ const [variantProps, otherProps] = pageBanner.splitVariantProps(props);
71
+ const {
72
+ children,
73
+ className,
74
+ style,
75
+ defaultOpen = true,
76
+ open: openProp,
77
+ onDismiss,
78
+ bindtap,
79
+ "main-thread:bindtap": mainThreadBindtap,
80
+ "accessibility-element": accessibilityElement,
81
+ "accessibility-traits": accessibilityTraits,
82
+ ...nativeProps
83
+ } = otherProps;
84
+ const [open, setOpen] = useControllableState({
85
+ value: openProp,
86
+ defaultValue: defaultOpen,
87
+ });
88
+ const isInteractive = bindtap != null || mainThreadBindtap != null;
89
+ const pressTap = usePressTap({
90
+ disabled: !isInteractive,
91
+ onTap: bindtap,
92
+ mainThreadOnTap: mainThreadBindtap,
93
+ });
94
+ const classNames = pageBanner({ ...variantProps, pressed: pressTap.pressed });
95
+ const dismiss = useMemoizedFn(() => {
96
+ if (!open) return;
97
+
98
+ setOpen(false);
99
+ onDismiss?.();
100
+ });
101
+ const contextValue = React.useMemo(() => ({ dismiss }), [dismiss]);
102
+ const iconSlotContextValue = React.useMemo(
103
+ () => ({
104
+ classNames: {
105
+ prefixIcon: classNames.prefixIcon,
106
+ suffixIcon: classNames.suffixIcon,
107
+ },
108
+ deps: [variantProps.tone ?? "neutral", variantProps.variant ?? "weak", pressTap.pressed],
109
+ }),
110
+ [
111
+ classNames.prefixIcon,
112
+ classNames.suffixIcon,
113
+ pressTap.pressed,
114
+ variantProps.tone,
115
+ variantProps.variant,
116
+ ],
117
+ );
118
+
119
+ if (!open) return null;
120
+
121
+ return (
122
+ <PageBannerContext.Provider value={contextValue}>
123
+ <ClassNamesProvider value={classNames}>
124
+ <PropsProvider value={variantProps}>
125
+ <IconSlotProvider value={iconSlotContextValue}>
126
+ <view
127
+ {...(ref ? { ref: ref as LynxViewRef } : {})}
128
+ {...nativeProps}
129
+ {...(isInteractive ? pressTap : {})}
130
+ accessibility-element={accessibilityElement ?? (isInteractive ? true : undefined)}
131
+ accessibility-traits={accessibilityTraits ?? (isInteractive ? "button" : undefined)}
132
+ className={clsx(classNames.root, className)}
133
+ style={style}
134
+ >
135
+ {children}
136
+ </view>
137
+ </IconSlotProvider>
138
+ </PropsProvider>
139
+ </ClassNamesProvider>
140
+ </PageBannerContext.Provider>
141
+ );
142
+ });
143
+ PageBannerRoot.displayName = "PageBannerRoot";
144
+
145
+ ////////////////////////////////////////////////////////////////////////////////////
146
+
147
+ export interface PageBannerContentProps extends LynxStyledElementProps, LynxAccessibilityProps {}
148
+
149
+ export const PageBannerContent = React.forwardRef<unknown, PageBannerContentProps>((props, ref) => {
150
+ const { children, className, style, ...nativeProps } = props;
151
+ const classNames = useClassNames();
152
+
153
+ return (
154
+ <view
155
+ {...(ref ? { ref: ref as LynxViewRef } : {})}
156
+ {...nativeProps}
157
+ className={clsx(classNames.content, className)}
158
+ style={style}
159
+ >
160
+ {children}
161
+ </view>
162
+ );
163
+ });
164
+ PageBannerContent.displayName = "PageBannerContent";
165
+
166
+ ////////////////////////////////////////////////////////////////////////////////////
167
+
168
+ export interface PageBannerBodyProps extends LynxStyledElementProps, LynxAccessibilityProps {}
169
+
170
+ export const PageBannerBody = React.forwardRef<unknown, PageBannerBodyProps>((props, ref) => {
171
+ const { children, className, style, ...nativeProps } = props;
172
+ const classNames = useClassNames();
173
+
174
+ return (
175
+ <text
176
+ {...(ref ? { ref: ref as LynxTextRef } : {})}
177
+ {...nativeProps}
178
+ className={clsx(classNames.body, className)}
179
+ style={style}
180
+ >
181
+ {children}
182
+ </text>
183
+ );
184
+ });
185
+ PageBannerBody.displayName = "PageBannerBody";
186
+
187
+ ////////////////////////////////////////////////////////////////////////////////////
188
+
189
+ export interface PageBannerTitleProps extends LynxStyledElementProps, LynxAccessibilityProps {}
190
+
191
+ export const PageBannerTitle = React.forwardRef<unknown, PageBannerTitleProps>((props, ref) => {
192
+ const { children, className, style, ...nativeProps } = props;
193
+ const classNames = useClassNames();
194
+
195
+ return (
196
+ <text
197
+ {...(ref ? { ref: ref as LynxTextRef } : {})}
198
+ {...nativeProps}
199
+ className={clsx(classNames.title, className)}
200
+ style={style}
201
+ >
202
+ {children}
203
+ {" "}
204
+ </text>
205
+ );
206
+ });
207
+ PageBannerTitle.displayName = "PageBannerTitle";
208
+
209
+ ////////////////////////////////////////////////////////////////////////////////////
210
+
211
+ export interface PageBannerDescriptionProps
212
+ extends LynxStyledElementProps,
213
+ LynxAccessibilityProps {}
214
+
215
+ export const PageBannerDescription = React.forwardRef<unknown, PageBannerDescriptionProps>(
216
+ (props, ref) => {
217
+ const { children, className, style, ...nativeProps } = props;
218
+ const classNames = useClassNames();
219
+
220
+ return (
221
+ <text
222
+ {...(ref ? { ref: ref as LynxTextRef } : {})}
223
+ {...nativeProps}
224
+ className={clsx(classNames.description, className)}
225
+ style={style}
226
+ >
227
+ {children}
228
+ </text>
229
+ );
230
+ },
231
+ );
232
+ PageBannerDescription.displayName = "PageBannerDescription";
233
+
234
+ ////////////////////////////////////////////////////////////////////////////////////
235
+
236
+ export interface PageBannerButtonProps
237
+ extends LynxStyledElementProps,
238
+ LynxPressableProps,
239
+ LynxAccessibilityProps {}
240
+
241
+ export const PageBannerButton = React.forwardRef<unknown, PageBannerButtonProps>((props, ref) => {
242
+ const {
243
+ children,
244
+ className,
245
+ style,
246
+ "accessibility-element": accessibilityElement = true,
247
+ "accessibility-traits": accessibilityTraits = "button",
248
+ ...nativeProps
249
+ } = props;
250
+ const classNames = useClassNames();
251
+
252
+ return (
253
+ <text
254
+ {...(ref ? { ref: ref as LynxTextRef } : {})}
255
+ {...nativeProps}
256
+ accessibility-element={accessibilityElement}
257
+ accessibility-traits={accessibilityTraits}
258
+ className={clsx(classNames.button, className)}
259
+ style={style}
260
+ >
261
+ {children}
262
+ </text>
263
+ );
264
+ });
265
+ PageBannerButton.displayName = "PageBannerButton";
266
+
267
+ ////////////////////////////////////////////////////////////////////////////////////
268
+
269
+ export interface PageBannerCloseButtonProps
270
+ extends LynxStyledElementProps,
271
+ LynxPressableProps,
272
+ LynxAccessibilityProps {}
273
+
274
+ export const PageBannerCloseButton = React.forwardRef<unknown, PageBannerCloseButtonProps>(
275
+ (props, ref) => {
276
+ const {
277
+ children,
278
+ className,
279
+ style,
280
+ bindtap,
281
+ "main-thread:bindtap": mainThreadBindtap,
282
+ "accessibility-element": accessibilityElement = true,
283
+ "accessibility-label": accessibilityLabel,
284
+ "accessibility-traits": accessibilityTraits = "button",
285
+ ...nativeProps
286
+ } = props;
287
+ const parentVariantProps = useProps() ?? {};
288
+ const { dismiss } = usePageBannerContext("PageBannerCloseButton");
289
+ const handleTap = useMemoizedFn<TapHandler>((event) => {
290
+ bindtap?.(event);
291
+ dismiss();
292
+ });
293
+ const pressTap = usePressTap({
294
+ onTap: handleTap,
295
+ mainThreadOnTap: mainThreadBindtap,
296
+ });
297
+ const classNames = pageBanner({
298
+ ...parentVariantProps,
299
+ closeButtonPressed: pressTap.pressed,
300
+ });
301
+ const iconSlotContextValue = React.useMemo(
302
+ () => ({
303
+ classNames: { suffixIcon: classNames.closeButtonIcon },
304
+ deps: [
305
+ parentVariantProps.tone ?? "neutral",
306
+ parentVariantProps.variant ?? "weak",
307
+ pressTap.pressed,
308
+ ],
309
+ }),
310
+ [
311
+ classNames.closeButtonIcon,
312
+ parentVariantProps.tone,
313
+ parentVariantProps.variant,
314
+ pressTap.pressed,
315
+ ],
316
+ );
317
+
318
+ if (process.env.NODE_ENV !== "production" && accessibilityElement && !accessibilityLabel) {
319
+ console.warn("PageBannerCloseButton requires `accessibility-label` for accessibility.");
320
+ }
321
+
322
+ return (
323
+ <IconSlotProvider value={iconSlotContextValue}>
324
+ <view
325
+ {...(ref ? { ref: ref as LynxViewRef } : {})}
326
+ {...nativeProps}
327
+ {...pressTap}
328
+ accessibility-element={accessibilityElement}
329
+ accessibility-label={accessibilityLabel}
330
+ accessibility-traits={accessibilityTraits}
331
+ className={clsx(classNames.closeButton, className)}
332
+ style={style}
333
+ >
334
+ {children}
335
+ </view>
336
+ </IconSlotProvider>
337
+ );
338
+ },
339
+ );
340
+ PageBannerCloseButton.displayName = "PageBannerCloseButton";
@@ -0,0 +1,18 @@
1
+ export {
2
+ PageBannerBody,
3
+ PageBannerButton,
4
+ PageBannerCloseButton,
5
+ PageBannerContent,
6
+ PageBannerDescription,
7
+ PageBannerRoot,
8
+ PageBannerTitle,
9
+ type PageBannerBodyProps,
10
+ type PageBannerButtonProps,
11
+ type PageBannerCloseButtonProps,
12
+ type PageBannerContentProps,
13
+ type PageBannerDescriptionProps,
14
+ type PageBannerRootProps,
15
+ type PageBannerTitleProps,
16
+ } from "./PageBanner";
17
+
18
+ export * as PageBanner from "./PageBanner.namespace";
@@ -60,7 +60,7 @@ interface RadioGroupItemContextValue {
60
60
 
61
61
  const RadioGroupItemContext = React.createContext<RadioGroupItemContextValue | null>(null);
62
62
 
63
- function useRadioGroupItemContext(consumer: string): RadioGroupItemContextValue {
63
+ export function useRadioGroupItemContext(consumer: string): RadioGroupItemContextValue {
64
64
  const ctx = React.useContext(RadioGroupItemContext);
65
65
  if (!ctx) {
66
66
  throw new Error(`<${consumer}/> must be rendered inside <RadioGroupItem/>.`);
@@ -232,6 +232,7 @@ export const RadioGroupItemControl = React.forwardRef<unknown, RadioGroupItemCon
232
232
  pressed: itemContext.pressed,
233
233
  };
234
234
  const classes = radiomark(radiomarkVariantProps);
235
+ const controlClassName = radio(groupContext.radioVariantProps).control;
235
236
 
236
237
  return (
237
238
  <RadiomarkControlContext.Provider
@@ -239,7 +240,7 @@ export const RadioGroupItemControl = React.forwardRef<unknown, RadioGroupItemCon
239
240
  >
240
241
  <view
241
242
  {...(ref ? { ref: ref as LynxViewRef } : {})}
242
- className={clsx(classes.root, className)}
243
+ className={clsx(classes.root, controlClassName, className)}
243
244
  {...nativeProps}
244
245
  >
245
246
  {children}
@@ -0,0 +1,8 @@
1
+ export {
2
+ SegmentedControlIndicator as Indicator,
3
+ SegmentedControlItem as Item,
4
+ SegmentedControlRoot as Root,
5
+ type SegmentedControlIndicatorProps as IndicatorProps,
6
+ type SegmentedControlItemProps as ItemProps,
7
+ type SegmentedControlRootProps as RootProps,
8
+ } from "./SegmentedControl";
@@ -0,0 +1,81 @@
1
+ import "@testing-library/jest-dom";
2
+ import { fireEvent, render } from "@lynx-js/react/testing-library";
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import * as SegmentedControl from "./SegmentedControl.namespace";
6
+
7
+ function BasicSegmentedControl(props: {
8
+ value?: string;
9
+ defaultValue?: string;
10
+ onValueChange?: (value: string) => void;
11
+ }) {
12
+ return (
13
+ <SegmentedControl.Root {...props} accessibility-label="정렬 기준">
14
+ <SegmentedControl.Item value="hot">인기순</SegmentedControl.Item>
15
+ <SegmentedControl.Item value="new">최신순</SegmentedControl.Item>
16
+ <SegmentedControl.Indicator />
17
+ </SegmentedControl.Root>
18
+ );
19
+ }
20
+
21
+ function getItem(container: HTMLElement, label: string) {
22
+ const item = Array.from(
23
+ container.querySelectorAll<HTMLElement>(".seed-segmented-control__item"),
24
+ ).find((element) => element.textContent === label);
25
+ if (!item) throw new Error(`Expected item for ${label} to exist.`);
26
+ return item;
27
+ }
28
+
29
+ describe("SegmentedControl", () => {
30
+ it("changes an uncontrolled value when an item is tapped", () => {
31
+ const onValueChange = vi.fn();
32
+ const { container } = render(
33
+ <BasicSegmentedControl defaultValue="hot" onValueChange={onValueChange} />,
34
+ );
35
+ const hot = getItem(container, "인기순");
36
+ const latest = getItem(container, "최신순");
37
+
38
+ expect(hot).toHaveAttribute("accessibility-value", "selected");
39
+ expect(latest).toHaveAttribute("accessibility-value", "not selected");
40
+
41
+ fireEvent.tap(latest);
42
+
43
+ expect(onValueChange).toHaveBeenCalledWith("new");
44
+ expect(latest).toHaveAttribute("accessibility-value", "selected");
45
+ });
46
+
47
+ it("does not select a disabled item", () => {
48
+ const onValueChange = vi.fn();
49
+ const { container } = render(
50
+ <SegmentedControl.Root defaultValue="hot" onValueChange={onValueChange}>
51
+ <SegmentedControl.Item value="hot">인기순</SegmentedControl.Item>
52
+ <SegmentedControl.Item value="new" disabled>
53
+ 최신순
54
+ </SegmentedControl.Item>
55
+ </SegmentedControl.Root>,
56
+ );
57
+ const disabledItem = getItem(container, "최신순");
58
+
59
+ fireEvent.tap(disabledItem);
60
+
61
+ expect(onValueChange).not.toHaveBeenCalled();
62
+ expect(disabledItem).toHaveAttribute("accessibility-traits", "disabled");
63
+ });
64
+
65
+ it("keeps the press-start selection on the fading item background", () => {
66
+ const { container } = render(<BasicSegmentedControl defaultValue="hot" />);
67
+ const latest = getItem(container, "최신순");
68
+ const getBackground = () =>
69
+ latest.querySelector<HTMLElement>(".seed-segmented-control__itemBackground");
70
+
71
+ fireEvent.touchstart(latest, {});
72
+ expect(getBackground()).toHaveClass("seed-segmented-control__itemBackground--pressed_true");
73
+ expect(getBackground()).toHaveClass("seed-segmented-control__itemBackground--selected_false");
74
+
75
+ fireEvent.touchend(latest, {});
76
+ fireEvent.tap(latest);
77
+
78
+ expect(getBackground()).toHaveClass("seed-segmented-control__itemBackground--pressed_false");
79
+ expect(getBackground()).toHaveClass("seed-segmented-control__itemBackground--selected_false");
80
+ });
81
+ });