@seed-design/lynx-react 0.5.2 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/Checkbox/Checkbox.d.ts +26 -0
- package/lib/components/Checkbox/Checkbox.d.ts.map +1 -1
- package/lib/components/Checkbox/Checkbox.jsx +1 -1
- package/lib/components/Icon/Icon.d.ts +5 -0
- package/lib/components/Icon/Icon.d.ts.map +1 -1
- package/lib/components/Icon/Icon.jsx +14 -3
- package/lib/components/MannerTemp/MannerTemp.d.ts +19 -0
- package/lib/components/MannerTemp/MannerTemp.d.ts.map +1 -0
- package/lib/components/MannerTemp/MannerTemp.jsx +47 -0
- package/lib/components/MannerTemp/index.d.ts +2 -0
- package/lib/components/MannerTemp/index.d.ts.map +1 -0
- package/lib/components/MannerTempBadge/MannerTempBadge.d.ts +13 -0
- package/lib/components/MannerTempBadge/MannerTempBadge.d.ts.map +1 -0
- package/lib/components/MannerTempBadge/MannerTempBadge.jsx +15 -0
- package/lib/components/MannerTempBadge/index.d.ts +2 -0
- package/lib/components/MannerTempBadge/index.d.ts.map +1 -0
- package/lib/components/NotificationBadge/NotificationBadge.d.ts +18 -0
- package/lib/components/NotificationBadge/NotificationBadge.d.ts.map +1 -0
- package/lib/components/NotificationBadge/NotificationBadge.jsx +34 -0
- package/lib/components/NotificationBadge/index.d.ts +2 -0
- package/lib/components/NotificationBadge/index.d.ts.map +1 -0
- package/lib/components/PageBanner/PageBanner.d.ts +35 -0
- package/lib/components/PageBanner/PageBanner.d.ts.map +1 -0
- package/lib/components/PageBanner/PageBanner.jsx +150 -0
- package/lib/components/PageBanner/PageBanner.namespace.d.ts +2 -0
- package/lib/components/PageBanner/PageBanner.namespace.d.ts.map +1 -0
- package/lib/components/PageBanner/PageBanner.namespace.js +14 -0
- package/lib/components/PageBanner/index.d.ts +3 -0
- package/lib/components/PageBanner/index.d.ts.map +1 -0
- package/lib/components/RadioGroup/RadioGroup.d.ts +9 -0
- package/lib/components/RadioGroup/RadioGroup.d.ts.map +1 -1
- package/lib/components/RadioGroup/RadioGroup.jsx +1 -1
- package/lib/components/SegmentedControl/SegmentedControl.d.ts +27 -0
- package/lib/components/SegmentedControl/SegmentedControl.d.ts.map +1 -0
- package/lib/components/SegmentedControl/SegmentedControl.jsx +120 -0
- package/lib/components/SegmentedControl/SegmentedControl.namespace.d.ts +2 -0
- package/lib/components/SegmentedControl/SegmentedControl.namespace.d.ts.map +1 -0
- package/lib/components/SegmentedControl/SegmentedControl.namespace.js +10 -0
- package/lib/components/SegmentedControl/index.d.ts +3 -0
- package/lib/components/SegmentedControl/index.d.ts.map +1 -0
- package/lib/components/SelectBox/CheckSelectBox.namespace.d.ts +2 -0
- package/lib/components/SelectBox/CheckSelectBox.namespace.d.ts.map +1 -0
- package/lib/components/SelectBox/CheckSelectBox.namespace.js +17 -0
- package/lib/components/SelectBox/RadioSelectBox.namespace.d.ts +2 -0
- package/lib/components/SelectBox/RadioSelectBox.namespace.d.ts.map +1 -0
- package/lib/components/SelectBox/RadioSelectBox.namespace.js +15 -0
- package/lib/components/SelectBox/SelectBox.d.ts +85 -0
- package/lib/components/SelectBox/SelectBox.d.ts.map +1 -0
- package/lib/components/SelectBox/SelectBox.jsx +237 -0
- package/lib/components/SelectBox/index.d.ts +4 -0
- package/lib/components/SelectBox/index.d.ts.map +1 -0
- package/lib/components/index.d.ts +6 -0
- package/lib/components/index.d.ts.map +1 -1
- package/lib/hooks/useIconColor.d.ts +3 -2
- package/lib/hooks/useIconColor.d.ts.map +1 -1
- package/lib/hooks/useIconColor.js +12 -4
- package/lib/index.js +11 -1
- package/package.json +3 -3
- package/src/components/Checkbox/Checkbox.tsx +1 -1
- package/src/components/Icon/Icon.test.tsx +25 -1
- package/src/components/Icon/Icon.tsx +22 -1
- package/src/components/MannerTemp/MannerTemp.test.tsx +41 -0
- package/src/components/MannerTemp/MannerTemp.tsx +85 -0
- package/src/components/MannerTemp/index.ts +1 -0
- package/src/components/MannerTempBadge/MannerTempBadge.test.tsx +35 -0
- package/src/components/MannerTempBadge/MannerTempBadge.tsx +34 -0
- package/src/components/MannerTempBadge/index.ts +1 -0
- package/src/components/NotificationBadge/NotificationBadge.test.tsx +28 -0
- package/src/components/NotificationBadge/NotificationBadge.tsx +81 -0
- package/src/components/NotificationBadge/index.ts +1 -0
- package/src/components/PageBanner/PageBanner.namespace.ts +16 -0
- package/src/components/PageBanner/PageBanner.test.tsx +124 -0
- package/src/components/PageBanner/PageBanner.tsx +340 -0
- package/src/components/PageBanner/index.ts +18 -0
- package/src/components/RadioGroup/RadioGroup.tsx +1 -1
- package/src/components/SegmentedControl/SegmentedControl.namespace.ts +8 -0
- package/src/components/SegmentedControl/SegmentedControl.test.tsx +81 -0
- package/src/components/SegmentedControl/SegmentedControl.tsx +236 -0
- package/src/components/SegmentedControl/index.ts +10 -0
- package/src/components/SelectBox/CheckSelectBox.namespace.ts +22 -0
- package/src/components/SelectBox/RadioSelectBox.namespace.ts +18 -0
- package/src/components/SelectBox/SelectBox.test.tsx +169 -0
- package/src/components/SelectBox/SelectBox.tsx +533 -0
- package/src/components/SelectBox/index.ts +3 -0
- package/src/components/index.ts +6 -0
- package/src/hooks/useIconColor.test.ts +37 -1
- package/src/hooks/useIconColor.ts +10 -5
|
@@ -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/>.`);
|
|
@@ -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
|
+
});
|
|
@@ -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";
|