@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,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";
@@ -0,0 +1,3 @@
1
+ export * from "./SelectBox";
2
+ export * as CheckSelectBox from "./CheckSelectBox.namespace";
3
+ export * as RadioSelectBox from "./RadioSelectBox.namespace";
@@ -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 { getTabsLayoutWidth, getTabsOrderedItems, getTabsTriggerRects } from "./Tabs.utils";
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<TabsVariantProps, "selected" | "disabled" | "inCarousel">;
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
+ }