@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.
- 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 +3 -2
- 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/Tabs/Tabs.d.ts +1 -1
- package/lib/components/Tabs/Tabs.d.ts.map +1 -1
- package/lib/components/Tabs/Tabs.jsx +10 -3
- package/lib/components/Tabs/Tabs.utils.d.ts +1 -0
- package/lib/components/Tabs/Tabs.utils.d.ts.map +1 -1
- package/lib/components/Tabs/Tabs.utils.js +4 -1
- package/lib/components/TextField/TextField.d.ts.map +1 -1
- package/lib/components/TextField/TextField.jsx +4 -0
- package/lib/components/index.d.ts +6 -0
- package/lib/components/index.d.ts.map +1 -1
- package/lib/hooks/useIconColor.d.ts +1 -0
- package/lib/hooks/useIconColor.d.ts.map +1 -1
- package/lib/hooks/useIconColor.js +4 -1
- 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 +3 -2
- 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/Tabs/Tabs.test.tsx +26 -1
- package/src/components/Tabs/Tabs.tsx +14 -2
- package/src/components/Tabs/Tabs.utils.ts +7 -0
- package/src/components/TextField/TextField.test.tsx +48 -0
- package/src/components/TextField/TextField.tsx +8 -1
- package/src/components/index.ts +6 -0
- package/src/hooks/useIconColor.ts +6 -1
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import * as React from "@lynx-js/react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
|
|
4
|
+
import { selectBox, type SelectBoxVariantProps } from "@seed-design/lynx-css/recipes/select-box";
|
|
5
|
+
import {
|
|
6
|
+
selectBoxCheckmark,
|
|
7
|
+
type SelectBoxCheckmarkVariantProps,
|
|
8
|
+
} from "@seed-design/lynx-css/recipes/select-box-checkmark";
|
|
9
|
+
import { selectBoxGroup } from "@seed-design/lynx-css/recipes/select-box-group";
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
LynxAccessibilityProps,
|
|
13
|
+
LynxStyledElementProps,
|
|
14
|
+
LynxTextRef,
|
|
15
|
+
LynxViewProps,
|
|
16
|
+
LynxViewRef,
|
|
17
|
+
} from "../../types";
|
|
18
|
+
import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
|
|
19
|
+
import { CheckboxRoot, type CheckboxRootProps, useCheckboxContext } from "../Checkbox/Checkbox";
|
|
20
|
+
import { IconSlotProvider, InternalIcon, type InternalIconProps } from "../Icon/Icon";
|
|
21
|
+
import {
|
|
22
|
+
RadioGroupItem,
|
|
23
|
+
type RadioGroupItemProps,
|
|
24
|
+
useRadioGroupItemContext,
|
|
25
|
+
} from "../RadioGroup/RadioGroup";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @platform Lynx
|
|
29
|
+
*
|
|
30
|
+
* 웹 대비 미지원 기능:
|
|
31
|
+
* - HiddenInput / name / required / invalid: Lynx에 native form 제출 모델이 없음
|
|
32
|
+
* - focus / focusVisible: Lynx에 키보드 포커스 개념이 없음
|
|
33
|
+
* - DOM field wrapper: Lynx Registry는 label/error 연결용 wrapper를 제공하지 않음
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
type FooterVisibility = "when-selected" | "when-not-selected" | "always";
|
|
37
|
+
type PublicSelectBoxVariantProps = Omit<
|
|
38
|
+
SelectBoxVariantProps,
|
|
39
|
+
"selected" | "pressed" | "disabled" | "footerOpen"
|
|
40
|
+
>;
|
|
41
|
+
|
|
42
|
+
interface SelectBoxRuntimeContextValue {
|
|
43
|
+
selected: boolean;
|
|
44
|
+
pressed: boolean;
|
|
45
|
+
disabled: boolean;
|
|
46
|
+
footerVisibility: FooterVisibility;
|
|
47
|
+
variantProps: PublicSelectBoxVariantProps;
|
|
48
|
+
accessibilityRole: "checkbox" | "radio";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const SelectBoxRuntimeContext = React.createContext<SelectBoxRuntimeContextValue | null>(null);
|
|
52
|
+
const SelectBoxLayoutContext = React.createContext<PublicSelectBoxVariantProps>({});
|
|
53
|
+
const { ClassNamesProvider, useClassNames } = createSlotRecipeContext(selectBox);
|
|
54
|
+
|
|
55
|
+
function useSelectBoxRuntimeContext(consumer: string): SelectBoxRuntimeContextValue {
|
|
56
|
+
const context = React.useContext(SelectBoxRuntimeContext);
|
|
57
|
+
if (!context) {
|
|
58
|
+
throw new Error(`<${consumer}/> must be rendered inside a SelectBox root or item.`);
|
|
59
|
+
}
|
|
60
|
+
return context;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface SelectBoxSurfaceProps extends LynxStyledElementProps {
|
|
64
|
+
accessibilityProps: LynxAccessibilityProps;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function SelectBoxSurface({
|
|
68
|
+
children,
|
|
69
|
+
className,
|
|
70
|
+
style,
|
|
71
|
+
accessibilityProps,
|
|
72
|
+
}: SelectBoxSurfaceProps) {
|
|
73
|
+
const context = useSelectBoxRuntimeContext("SelectBoxSurface");
|
|
74
|
+
const footerOpen =
|
|
75
|
+
context.footerVisibility === "always" ||
|
|
76
|
+
(context.footerVisibility === "when-selected" ? context.selected : !context.selected);
|
|
77
|
+
const classes = selectBox({
|
|
78
|
+
...context.variantProps,
|
|
79
|
+
selected: context.selected,
|
|
80
|
+
pressed: context.pressed,
|
|
81
|
+
disabled: context.disabled,
|
|
82
|
+
footerOpen,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<ClassNamesProvider value={classes}>
|
|
87
|
+
<IconSlotProvider
|
|
88
|
+
value={{
|
|
89
|
+
classNames: { prefixIcon: classes.prefixIcon },
|
|
90
|
+
deps: [context.selected, context.pressed, context.disabled],
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
<view
|
|
94
|
+
className={clsx(classes.root, className)}
|
|
95
|
+
style={style}
|
|
96
|
+
accessibility-element={true}
|
|
97
|
+
accessibility-role-description={context.accessibilityRole}
|
|
98
|
+
accessibility-value={context.selected ? "selected" : "not selected"}
|
|
99
|
+
accessibility-traits={context.disabled ? "disabled" : undefined}
|
|
100
|
+
{...accessibilityProps}
|
|
101
|
+
>
|
|
102
|
+
{children}
|
|
103
|
+
<view className={classes.selectedStroke} accessibility-elements-hidden={true} />
|
|
104
|
+
</view>
|
|
105
|
+
</IconSlotProvider>
|
|
106
|
+
</ClassNamesProvider>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
111
|
+
|
|
112
|
+
export interface SelectBoxGroupProps extends LynxStyledElementProps {
|
|
113
|
+
/**
|
|
114
|
+
* 열 개수입니다. 2 이상이면 자식 Select Box의 기본 layout이 vertical이 됩니다.
|
|
115
|
+
* @default 1
|
|
116
|
+
*/
|
|
117
|
+
columns?: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const SelectBoxGroup = React.forwardRef<unknown, SelectBoxGroupProps>((props, ref) => {
|
|
121
|
+
const { children, columns = 1, className, style, ...nativeProps } = props;
|
|
122
|
+
const classes = selectBoxGroup({ multiColumn: columns > 1 });
|
|
123
|
+
const layout: PublicSelectBoxVariantProps["layout"] = columns > 1 ? "vertical" : "horizontal";
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<SelectBoxLayoutContext.Provider value={{ layout }}>
|
|
127
|
+
<view
|
|
128
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
129
|
+
className={clsx(classes, className)}
|
|
130
|
+
style={{ ...style, gridTemplateColumns: `repeat(${columns}, 1fr)` }}
|
|
131
|
+
{...nativeProps}
|
|
132
|
+
>
|
|
133
|
+
{children}
|
|
134
|
+
</view>
|
|
135
|
+
</SelectBoxLayoutContext.Provider>
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
SelectBoxGroup.displayName = "SelectBoxGroup";
|
|
139
|
+
|
|
140
|
+
export interface CheckSelectBoxGroupProps extends SelectBoxGroupProps {}
|
|
141
|
+
export const CheckSelectBoxGroup = SelectBoxGroup;
|
|
142
|
+
|
|
143
|
+
export interface RadioSelectBoxGroupProps extends SelectBoxGroupProps {}
|
|
144
|
+
export const RadioSelectBoxGroup = SelectBoxGroup;
|
|
145
|
+
|
|
146
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
147
|
+
|
|
148
|
+
export interface CheckSelectBoxRootProps
|
|
149
|
+
extends PublicSelectBoxVariantProps,
|
|
150
|
+
LynxStyledElementProps,
|
|
151
|
+
LynxAccessibilityProps,
|
|
152
|
+
Pick<
|
|
153
|
+
CheckboxRootProps,
|
|
154
|
+
"checked" | "defaultChecked" | "indeterminate" | "disabled" | "onCheckedChange"
|
|
155
|
+
> {
|
|
156
|
+
/** @default "when-selected" */
|
|
157
|
+
footerVisibility?: FooterVisibility;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
interface CheckSelectBoxSurfaceProps extends SelectBoxSurfaceProps {
|
|
161
|
+
footerVisibility: FooterVisibility;
|
|
162
|
+
variantProps: PublicSelectBoxVariantProps;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function CheckSelectBoxSurface(props: CheckSelectBoxSurfaceProps) {
|
|
166
|
+
const checkbox = useCheckboxContext("CheckSelectBoxRoot");
|
|
167
|
+
const contextValue = React.useMemo<SelectBoxRuntimeContextValue>(
|
|
168
|
+
() => ({
|
|
169
|
+
selected: checkbox.checked,
|
|
170
|
+
pressed: checkbox.pressed,
|
|
171
|
+
disabled: checkbox.disabled,
|
|
172
|
+
footerVisibility: props.footerVisibility,
|
|
173
|
+
variantProps: props.variantProps,
|
|
174
|
+
accessibilityRole: "checkbox",
|
|
175
|
+
}),
|
|
176
|
+
[
|
|
177
|
+
checkbox.checked,
|
|
178
|
+
checkbox.disabled,
|
|
179
|
+
checkbox.pressed,
|
|
180
|
+
props.footerVisibility,
|
|
181
|
+
props.variantProps,
|
|
182
|
+
],
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<SelectBoxRuntimeContext.Provider value={contextValue}>
|
|
187
|
+
<SelectBoxSurface {...props} />
|
|
188
|
+
</SelectBoxRuntimeContext.Provider>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export const CheckSelectBoxRoot = React.forwardRef<unknown, CheckSelectBoxRootProps>(
|
|
193
|
+
(props, ref) => {
|
|
194
|
+
const {
|
|
195
|
+
children,
|
|
196
|
+
className,
|
|
197
|
+
style,
|
|
198
|
+
checked,
|
|
199
|
+
defaultChecked,
|
|
200
|
+
indeterminate,
|
|
201
|
+
disabled,
|
|
202
|
+
onCheckedChange,
|
|
203
|
+
footerVisibility = "when-selected",
|
|
204
|
+
...restProps
|
|
205
|
+
} = props;
|
|
206
|
+
const inheritedVariantProps = React.useContext(SelectBoxLayoutContext);
|
|
207
|
+
const [variantProps, accessibilityProps] = selectBox.splitVariantProps(restProps);
|
|
208
|
+
const resolvedVariantProps = {
|
|
209
|
+
...variantProps,
|
|
210
|
+
layout: variantProps.layout ?? inheritedVariantProps.layout,
|
|
211
|
+
};
|
|
212
|
+
const interactionRootClassName = selectBox(resolvedVariantProps).interactionRoot;
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<CheckboxRoot
|
|
216
|
+
ref={ref}
|
|
217
|
+
className={interactionRootClassName}
|
|
218
|
+
checked={checked}
|
|
219
|
+
defaultChecked={defaultChecked}
|
|
220
|
+
indeterminate={indeterminate}
|
|
221
|
+
disabled={disabled}
|
|
222
|
+
onCheckedChange={onCheckedChange}
|
|
223
|
+
accessibility-element={false}
|
|
224
|
+
>
|
|
225
|
+
<CheckSelectBoxSurface
|
|
226
|
+
className={className}
|
|
227
|
+
style={style}
|
|
228
|
+
footerVisibility={footerVisibility}
|
|
229
|
+
variantProps={resolvedVariantProps}
|
|
230
|
+
accessibilityProps={accessibilityProps}
|
|
231
|
+
>
|
|
232
|
+
{children}
|
|
233
|
+
</CheckSelectBoxSurface>
|
|
234
|
+
</CheckboxRoot>
|
|
235
|
+
);
|
|
236
|
+
},
|
|
237
|
+
);
|
|
238
|
+
CheckSelectBoxRoot.displayName = "CheckSelectBoxRoot";
|
|
239
|
+
|
|
240
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
241
|
+
|
|
242
|
+
export interface RadioSelectBoxItemProps
|
|
243
|
+
extends PublicSelectBoxVariantProps,
|
|
244
|
+
LynxStyledElementProps,
|
|
245
|
+
LynxAccessibilityProps,
|
|
246
|
+
Pick<RadioGroupItemProps, "value" | "disabled"> {
|
|
247
|
+
/** @default "when-selected" */
|
|
248
|
+
footerVisibility?: FooterVisibility;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
interface RadioSelectBoxSurfaceProps extends SelectBoxSurfaceProps {
|
|
252
|
+
footerVisibility: FooterVisibility;
|
|
253
|
+
variantProps: PublicSelectBoxVariantProps;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function RadioSelectBoxSurface(props: RadioSelectBoxSurfaceProps) {
|
|
257
|
+
const item = useRadioGroupItemContext("RadioSelectBoxItem");
|
|
258
|
+
const contextValue = React.useMemo<SelectBoxRuntimeContextValue>(
|
|
259
|
+
() => ({
|
|
260
|
+
selected: item.checked,
|
|
261
|
+
pressed: item.pressed,
|
|
262
|
+
disabled: item.disabled,
|
|
263
|
+
footerVisibility: props.footerVisibility,
|
|
264
|
+
variantProps: props.variantProps,
|
|
265
|
+
accessibilityRole: "radio",
|
|
266
|
+
}),
|
|
267
|
+
[item.checked, item.disabled, item.pressed, props.footerVisibility, props.variantProps],
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<SelectBoxRuntimeContext.Provider value={contextValue}>
|
|
272
|
+
<SelectBoxSurface {...props} />
|
|
273
|
+
</SelectBoxRuntimeContext.Provider>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export const RadioSelectBoxItem = React.forwardRef<unknown, RadioSelectBoxItemProps>(
|
|
278
|
+
(props, ref) => {
|
|
279
|
+
const {
|
|
280
|
+
children,
|
|
281
|
+
className,
|
|
282
|
+
style,
|
|
283
|
+
value,
|
|
284
|
+
disabled,
|
|
285
|
+
footerVisibility = "when-selected",
|
|
286
|
+
...restProps
|
|
287
|
+
} = props;
|
|
288
|
+
const inheritedVariantProps = React.useContext(SelectBoxLayoutContext);
|
|
289
|
+
const [variantProps, accessibilityProps] = selectBox.splitVariantProps(restProps);
|
|
290
|
+
const resolvedVariantProps = {
|
|
291
|
+
...variantProps,
|
|
292
|
+
layout: variantProps.layout ?? inheritedVariantProps.layout,
|
|
293
|
+
};
|
|
294
|
+
const interactionRootClassName = selectBox(resolvedVariantProps).interactionRoot;
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<RadioGroupItem
|
|
298
|
+
ref={ref}
|
|
299
|
+
className={interactionRootClassName}
|
|
300
|
+
value={value}
|
|
301
|
+
disabled={disabled}
|
|
302
|
+
accessibility-element={false}
|
|
303
|
+
>
|
|
304
|
+
<RadioSelectBoxSurface
|
|
305
|
+
className={className}
|
|
306
|
+
style={style}
|
|
307
|
+
footerVisibility={footerVisibility}
|
|
308
|
+
variantProps={resolvedVariantProps}
|
|
309
|
+
accessibilityProps={accessibilityProps}
|
|
310
|
+
>
|
|
311
|
+
{children}
|
|
312
|
+
</RadioSelectBoxSurface>
|
|
313
|
+
</RadioGroupItem>
|
|
314
|
+
);
|
|
315
|
+
},
|
|
316
|
+
);
|
|
317
|
+
RadioSelectBoxItem.displayName = "RadioSelectBoxItem";
|
|
318
|
+
|
|
319
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
320
|
+
|
|
321
|
+
function createViewSlot(displayName: string, slot: keyof ReturnType<typeof selectBox>) {
|
|
322
|
+
const Component = React.forwardRef<unknown, LynxStyledElementProps>((props, ref) => {
|
|
323
|
+
const { children, className, ...nativeProps } = props;
|
|
324
|
+
const classes = useClassNames();
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<view
|
|
328
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
329
|
+
className={clsx(classes[slot], className)}
|
|
330
|
+
{...nativeProps}
|
|
331
|
+
>
|
|
332
|
+
{children}
|
|
333
|
+
</view>
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
Component.displayName = displayName;
|
|
337
|
+
return Component;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function createTextSlot(displayName: string, slot: keyof ReturnType<typeof selectBox>) {
|
|
341
|
+
const Component = React.forwardRef<unknown, LynxStyledElementProps>((props, ref) => {
|
|
342
|
+
const { children, className, ...nativeProps } = props;
|
|
343
|
+
const classes = useClassNames();
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<text
|
|
347
|
+
{...(ref ? { ref: ref as LynxTextRef } : {})}
|
|
348
|
+
className={clsx(classes[slot], className)}
|
|
349
|
+
{...nativeProps}
|
|
350
|
+
>
|
|
351
|
+
{children}
|
|
352
|
+
</text>
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
Component.displayName = displayName;
|
|
356
|
+
return Component;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function createLabelSlot(displayName: string) {
|
|
360
|
+
const Component = React.forwardRef<unknown, LynxStyledElementProps>((props, ref) => {
|
|
361
|
+
const { children, className, ...nativeProps } = props;
|
|
362
|
+
const classes = useClassNames();
|
|
363
|
+
const labelChildren =
|
|
364
|
+
typeof children === "string" || typeof children === "number" ? (
|
|
365
|
+
<text>{children}</text>
|
|
366
|
+
) : (
|
|
367
|
+
children
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
return (
|
|
371
|
+
<view
|
|
372
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
373
|
+
className={clsx(classes.label, className)}
|
|
374
|
+
{...nativeProps}
|
|
375
|
+
>
|
|
376
|
+
{labelChildren}
|
|
377
|
+
</view>
|
|
378
|
+
);
|
|
379
|
+
});
|
|
380
|
+
Component.displayName = displayName;
|
|
381
|
+
return Component;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export interface CheckSelectBoxTriggerProps extends LynxStyledElementProps {}
|
|
385
|
+
export const CheckSelectBoxTrigger = createViewSlot("CheckSelectBoxTrigger", "trigger");
|
|
386
|
+
export interface CheckSelectBoxContentProps extends LynxStyledElementProps {}
|
|
387
|
+
export const CheckSelectBoxContent = createViewSlot("CheckSelectBoxContent", "content");
|
|
388
|
+
export interface CheckSelectBoxBodyProps extends LynxStyledElementProps {}
|
|
389
|
+
export const CheckSelectBoxBody = createViewSlot("CheckSelectBoxBody", "body");
|
|
390
|
+
export interface CheckSelectBoxLabelProps extends LynxStyledElementProps {}
|
|
391
|
+
export const CheckSelectBoxLabel = createLabelSlot("CheckSelectBoxLabel");
|
|
392
|
+
export interface CheckSelectBoxDescriptionProps extends LynxStyledElementProps {}
|
|
393
|
+
export const CheckSelectBoxDescription = createTextSlot("CheckSelectBoxDescription", "description");
|
|
394
|
+
|
|
395
|
+
export interface RadioSelectBoxTriggerProps extends LynxStyledElementProps {}
|
|
396
|
+
export const RadioSelectBoxTrigger = createViewSlot("RadioSelectBoxTrigger", "trigger");
|
|
397
|
+
export interface RadioSelectBoxContentProps extends LynxStyledElementProps {}
|
|
398
|
+
export const RadioSelectBoxContent = createViewSlot("RadioSelectBoxContent", "content");
|
|
399
|
+
export interface RadioSelectBoxBodyProps extends LynxStyledElementProps {}
|
|
400
|
+
export const RadioSelectBoxBody = createViewSlot("RadioSelectBoxBody", "body");
|
|
401
|
+
export interface RadioSelectBoxLabelProps extends LynxStyledElementProps {}
|
|
402
|
+
export const RadioSelectBoxLabel = createLabelSlot("RadioSelectBoxLabel");
|
|
403
|
+
export interface RadioSelectBoxDescriptionProps extends LynxStyledElementProps {}
|
|
404
|
+
export const RadioSelectBoxDescription = createTextSlot("RadioSelectBoxDescription", "description");
|
|
405
|
+
|
|
406
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
407
|
+
|
|
408
|
+
export interface SelectBoxFooterProps extends LynxStyledElementProps, LynxAccessibilityProps {}
|
|
409
|
+
|
|
410
|
+
type FooterLayoutChangeHandler = NonNullable<LynxViewProps["bindlayoutchange"]>;
|
|
411
|
+
|
|
412
|
+
function getFooterLayoutHeight(event: Parameters<FooterLayoutChangeHandler>[0]): number | null {
|
|
413
|
+
const eventWithHeight = event as Parameters<FooterLayoutChangeHandler>[0] & { height?: number };
|
|
414
|
+
const height = event.detail?.height ?? event.params?.height ?? eventWithHeight.height;
|
|
415
|
+
if (typeof height !== "number" || !Number.isFinite(height)) return null;
|
|
416
|
+
return Math.max(0, height);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const SelectBoxFooter = React.forwardRef<unknown, SelectBoxFooterProps>((props, ref) => {
|
|
420
|
+
const {
|
|
421
|
+
children,
|
|
422
|
+
className,
|
|
423
|
+
style,
|
|
424
|
+
"accessibility-elements-hidden": accessibilityElementsHidden = false,
|
|
425
|
+
...nativeProps
|
|
426
|
+
} = props;
|
|
427
|
+
const context = useSelectBoxRuntimeContext("SelectBoxFooter");
|
|
428
|
+
const classes = useClassNames();
|
|
429
|
+
const [contentHeight, setContentHeight] = React.useState(0);
|
|
430
|
+
const open =
|
|
431
|
+
context.footerVisibility === "always" ||
|
|
432
|
+
(context.footerVisibility === "when-selected" ? context.selected : !context.selected);
|
|
433
|
+
const handleLayoutChange = React.useCallback<FooterLayoutChangeHandler>((event) => {
|
|
434
|
+
const height = getFooterLayoutHeight(event);
|
|
435
|
+
if (height !== null) setContentHeight((current) => (current === height ? current : height));
|
|
436
|
+
}, []);
|
|
437
|
+
|
|
438
|
+
return (
|
|
439
|
+
<view
|
|
440
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
441
|
+
className={clsx(classes.footer, className)}
|
|
442
|
+
style={{
|
|
443
|
+
...style,
|
|
444
|
+
height:
|
|
445
|
+
context.footerVisibility === "always" || (open && contentHeight === 0)
|
|
446
|
+
? "auto"
|
|
447
|
+
: open
|
|
448
|
+
? `${contentHeight}px`
|
|
449
|
+
: "0px",
|
|
450
|
+
}}
|
|
451
|
+
accessibility-elements-hidden={!open || accessibilityElementsHidden}
|
|
452
|
+
{...nativeProps}
|
|
453
|
+
>
|
|
454
|
+
<view className={classes.footerInner} bindlayoutchange={handleLayoutChange}>
|
|
455
|
+
{children}
|
|
456
|
+
</view>
|
|
457
|
+
</view>
|
|
458
|
+
);
|
|
459
|
+
});
|
|
460
|
+
SelectBoxFooter.displayName = "SelectBoxFooter";
|
|
461
|
+
|
|
462
|
+
export interface CheckSelectBoxFooterProps extends SelectBoxFooterProps {}
|
|
463
|
+
export const CheckSelectBoxFooter = SelectBoxFooter;
|
|
464
|
+
export interface RadioSelectBoxFooterProps extends SelectBoxFooterProps {}
|
|
465
|
+
export const RadioSelectBoxFooter = SelectBoxFooter;
|
|
466
|
+
|
|
467
|
+
////////////////////////////////////////////////////////////////////////////////////
|
|
468
|
+
|
|
469
|
+
interface SelectBoxCheckmarkContextValue {
|
|
470
|
+
iconClassName: string;
|
|
471
|
+
variantProps: SelectBoxCheckmarkVariantProps;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const SelectBoxCheckmarkContext = React.createContext<SelectBoxCheckmarkContextValue | null>(null);
|
|
475
|
+
|
|
476
|
+
export interface CheckSelectBoxCheckmarkControlProps extends LynxStyledElementProps {}
|
|
477
|
+
|
|
478
|
+
export const CheckSelectBoxCheckmarkControl = React.forwardRef<
|
|
479
|
+
unknown,
|
|
480
|
+
CheckSelectBoxCheckmarkControlProps
|
|
481
|
+
>((props, ref) => {
|
|
482
|
+
const { children, className, ...nativeProps } = props;
|
|
483
|
+
const context = useSelectBoxRuntimeContext("CheckSelectBoxCheckmarkControl");
|
|
484
|
+
const variantProps = {
|
|
485
|
+
selected: context.selected,
|
|
486
|
+
pressed: context.pressed,
|
|
487
|
+
disabled: context.disabled,
|
|
488
|
+
};
|
|
489
|
+
const classes = selectBoxCheckmark(variantProps);
|
|
490
|
+
|
|
491
|
+
return (
|
|
492
|
+
<SelectBoxCheckmarkContext.Provider value={{ iconClassName: classes.icon, variantProps }}>
|
|
493
|
+
<view
|
|
494
|
+
{...(ref ? { ref: ref as LynxViewRef } : {})}
|
|
495
|
+
className={clsx(classes.root, className)}
|
|
496
|
+
accessibility-elements-hidden={true}
|
|
497
|
+
{...nativeProps}
|
|
498
|
+
>
|
|
499
|
+
{children}
|
|
500
|
+
</view>
|
|
501
|
+
</SelectBoxCheckmarkContext.Provider>
|
|
502
|
+
);
|
|
503
|
+
});
|
|
504
|
+
CheckSelectBoxCheckmarkControl.displayName = "CheckSelectBoxCheckmarkControl";
|
|
505
|
+
|
|
506
|
+
export interface CheckSelectBoxCheckmarkIconProps extends Omit<InternalIconProps, "deps"> {}
|
|
507
|
+
|
|
508
|
+
export const CheckSelectBoxCheckmarkIcon = React.forwardRef<
|
|
509
|
+
unknown,
|
|
510
|
+
CheckSelectBoxCheckmarkIconProps
|
|
511
|
+
>((props, ref) => {
|
|
512
|
+
const { className, ...otherProps } = props;
|
|
513
|
+
const context = React.useContext(SelectBoxCheckmarkContext);
|
|
514
|
+
if (!context) {
|
|
515
|
+
throw new Error(
|
|
516
|
+
"<CheckSelectBoxCheckmarkIcon/> must be rendered inside <CheckSelectBoxCheckmarkControl/>.",
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return (
|
|
521
|
+
<InternalIcon
|
|
522
|
+
ref={ref}
|
|
523
|
+
className={clsx(context.iconClassName, className)}
|
|
524
|
+
deps={[
|
|
525
|
+
context.variantProps.selected,
|
|
526
|
+
context.variantProps.pressed,
|
|
527
|
+
context.variantProps.disabled,
|
|
528
|
+
]}
|
|
529
|
+
{...otherProps}
|
|
530
|
+
/>
|
|
531
|
+
);
|
|
532
|
+
});
|
|
533
|
+
CheckSelectBoxCheckmarkIcon.displayName = "CheckSelectBoxCheckmarkIcon";
|
|
@@ -3,7 +3,12 @@ import { fireEvent, render } from "@lynx-js/react/testing-library";
|
|
|
3
3
|
import { describe, expect, it, vi } from "vitest";
|
|
4
4
|
|
|
5
5
|
import * as Tabs from "./Tabs.namespace";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
areTabsTransitionsEnabled,
|
|
8
|
+
getTabsLayoutWidth,
|
|
9
|
+
getTabsOrderedItems,
|
|
10
|
+
getTabsTriggerRects,
|
|
11
|
+
} from "./Tabs.utils";
|
|
7
12
|
|
|
8
13
|
function BasicTabs(props: {
|
|
9
14
|
value?: string;
|
|
@@ -41,6 +46,26 @@ describe("Tabs", () => {
|
|
|
41
46
|
expect(listContent?.querySelectorAll(".seed-tabs__trigger")).toHaveLength(2);
|
|
42
47
|
});
|
|
43
48
|
|
|
49
|
+
it("disables label and indicator transitions during the initial render", () => {
|
|
50
|
+
const { container } = render(<BasicTabs defaultValue="two" />);
|
|
51
|
+
const labels = container.querySelectorAll<HTMLElement>(".seed-tabs__triggerLabel");
|
|
52
|
+
const indicator = container.querySelector<HTMLElement>(".seed-tabs__indicator");
|
|
53
|
+
|
|
54
|
+
expect(indicator).toHaveClass("seed-tabs__indicator--transitionEnabled_false");
|
|
55
|
+
expect(indicator?.style.transitionDuration).toBe("");
|
|
56
|
+
for (const label of labels) {
|
|
57
|
+
expect(label).toHaveClass("seed-tabs__triggerLabel--transitionEnabled_false");
|
|
58
|
+
expect(label.style.transitionDuration).toBe("");
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("disables transitions when a dynamically added trigger is unmeasured", () => {
|
|
63
|
+
const rects = getTabsTriggerRects(["one", "two"], { one: 80, two: 80 });
|
|
64
|
+
|
|
65
|
+
expect(areTabsTransitionsEnabled(["one", "two"], rects)).toBe(true);
|
|
66
|
+
expect(areTabsTransitionsEnabled(["one", "two", "three"], rects)).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
44
69
|
it("changes an uncontrolled value when a trigger is tapped", () => {
|
|
45
70
|
const onValueChange = vi.fn();
|
|
46
71
|
const { container } = render(<BasicTabs defaultValue="one" onValueChange={onValueChange} />);
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
} from "../../types";
|
|
21
21
|
import { createSlotRecipeContext } from "../../utils/create-slot-recipe-context";
|
|
22
22
|
import {
|
|
23
|
+
areTabsTransitionsEnabled,
|
|
23
24
|
getTabsLayoutWidth,
|
|
24
25
|
getTabsOrderedItems,
|
|
25
26
|
getTabsTriggerRects,
|
|
@@ -31,7 +32,10 @@ type NativeViewPagerProps = IntrinsicElements["viewpager"];
|
|
|
31
32
|
type LayoutChangeHandler = NonNullable<NativeViewProps["bindlayoutchange"]>;
|
|
32
33
|
type TriggerRect = TabsLayoutRect;
|
|
33
34
|
type TriggerItem = { value: string; disabled: boolean };
|
|
34
|
-
type TabsPublicVariantProps = Omit<
|
|
35
|
+
type TabsPublicVariantProps = Omit<
|
|
36
|
+
TabsVariantProps,
|
|
37
|
+
"selected" | "disabled" | "inCarousel" | "transitionEnabled"
|
|
38
|
+
>;
|
|
35
39
|
|
|
36
40
|
const { ClassNamesProvider, useClassNames } = createSlotRecipeContext(tabs);
|
|
37
41
|
|
|
@@ -65,6 +69,7 @@ interface TabsContextValue {
|
|
|
65
69
|
selectedPagerIndex: number;
|
|
66
70
|
indicatorRef: React.RefObject<MainThread.Element>;
|
|
67
71
|
triggerRects: Record<string, TriggerRect>;
|
|
72
|
+
transitionsEnabled: boolean;
|
|
68
73
|
registerTrigger: (value: string, disabled: boolean) => () => void;
|
|
69
74
|
registerContent: (value: string) => () => void;
|
|
70
75
|
updateTriggerDisabled: (value: string, disabled: boolean) => void;
|
|
@@ -137,7 +142,6 @@ export const TabsRoot = React.forwardRef<unknown, TabsRootProps>((props, ref) =>
|
|
|
137
142
|
onValueChange,
|
|
138
143
|
...nativeProps
|
|
139
144
|
} = otherProps;
|
|
140
|
-
const classNames = tabs(variantProps);
|
|
141
145
|
const [value, setValueInternal] = useControllableState<string | undefined>({
|
|
142
146
|
value: valueProp,
|
|
143
147
|
defaultValue,
|
|
@@ -169,6 +173,11 @@ export const TabsRoot = React.forwardRef<unknown, TabsRootProps>((props, ref) =>
|
|
|
169
173
|
),
|
|
170
174
|
[items, triggerWidths],
|
|
171
175
|
);
|
|
176
|
+
const transitionsEnabled = areTabsTransitionsEnabled(
|
|
177
|
+
items.map((item) => item.value),
|
|
178
|
+
triggerRects,
|
|
179
|
+
);
|
|
180
|
+
const classNames = tabs({ ...variantProps, transitionEnabled: transitionsEnabled });
|
|
172
181
|
const visualValue = indicatorValue ?? value;
|
|
173
182
|
const indicatorIndex = items.findIndex((item) => item.value === visualValue);
|
|
174
183
|
const selectedPagerIndex = value === undefined ? -1 : pagerValues.indexOf(value);
|
|
@@ -283,6 +292,7 @@ export const TabsRoot = React.forwardRef<unknown, TabsRootProps>((props, ref) =>
|
|
|
283
292
|
indicatorIndex,
|
|
284
293
|
selectedPagerIndex,
|
|
285
294
|
indicatorRef,
|
|
295
|
+
transitionsEnabled,
|
|
286
296
|
triggerRects,
|
|
287
297
|
registerTrigger,
|
|
288
298
|
registerContent,
|
|
@@ -305,6 +315,7 @@ export const TabsRoot = React.forwardRef<unknown, TabsRootProps>((props, ref) =>
|
|
|
305
315
|
selectedPagerIndex,
|
|
306
316
|
indicatorRef,
|
|
307
317
|
triggerRects,
|
|
318
|
+
transitionsEnabled,
|
|
308
319
|
registerTrigger,
|
|
309
320
|
registerContent,
|
|
310
321
|
updateTriggerDisabled,
|
|
@@ -447,6 +458,7 @@ export const TabsTrigger = React.forwardRef<unknown, TabsTriggerProps>((props, r
|
|
|
447
458
|
...context.variantProps,
|
|
448
459
|
selected: visuallySelected,
|
|
449
460
|
disabled,
|
|
461
|
+
transitionEnabled: context.transitionsEnabled,
|
|
450
462
|
});
|
|
451
463
|
const label =
|
|
452
464
|
typeof children === "string" || typeof children === "number" ? String(children) : undefined;
|
|
@@ -54,3 +54,10 @@ export function getTabsTriggerRects(
|
|
|
54
54
|
|
|
55
55
|
return rects;
|
|
56
56
|
}
|
|
57
|
+
|
|
58
|
+
export function areTabsTransitionsEnabled(
|
|
59
|
+
values: string[],
|
|
60
|
+
rects: Record<string, TabsLayoutRect>,
|
|
61
|
+
): boolean {
|
|
62
|
+
return values.length > 0 && values.every((value) => rects[value] !== undefined);
|
|
63
|
+
}
|