@xsolla/xui-b2c-card-selector 0.217.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.
@@ -0,0 +1,752 @@
1
+ // src/CardSelector.tsx
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+
4
+ // ../../foundation/primitives-native/src/Box.tsx
5
+ import {
6
+ View,
7
+ Pressable,
8
+ Image
9
+ } from "react-native";
10
+ import { jsx } from "react/jsx-runtime";
11
+ var roleToAccessibilityRole = {
12
+ button: "button",
13
+ link: "link",
14
+ img: "image",
15
+ image: "image",
16
+ summary: "summary",
17
+ switch: "switch",
18
+ checkbox: "checkbox",
19
+ radio: "radio",
20
+ heading: "header",
21
+ text: "text",
22
+ search: "search",
23
+ alert: "alert",
24
+ none: "none",
25
+ presentation: "none",
26
+ progressbar: "progressbar",
27
+ scrollbar: "scrollbar",
28
+ adjustable: "adjustable",
29
+ tab: "tab",
30
+ menu: "menu",
31
+ menuitem: "menuitem",
32
+ list: "list",
33
+ timer: "timer"
34
+ };
35
+ var toGapValue = (gap) => {
36
+ if (gap === void 0) return void 0;
37
+ if (typeof gap === "number") return gap;
38
+ const pxMatch = /^(\d+(?:\.\d+)?)px$/.exec(gap);
39
+ if (pxMatch) return Number(pxMatch[1]);
40
+ const asNumber = Number(gap);
41
+ return Number.isFinite(asNumber) ? asNumber : void 0;
42
+ };
43
+ var Box = ({
44
+ children,
45
+ onPress,
46
+ onLayout,
47
+ onMoveShouldSetResponder,
48
+ onResponderGrant,
49
+ onResponderMove,
50
+ onResponderRelease,
51
+ onResponderTerminate,
52
+ backgroundColor,
53
+ borderColor,
54
+ borderWidth,
55
+ borderBottomWidth,
56
+ borderBottomColor,
57
+ borderTopWidth,
58
+ borderTopColor,
59
+ borderLeftWidth,
60
+ borderLeftColor,
61
+ borderRightWidth,
62
+ borderRightColor,
63
+ borderRadius,
64
+ borderStyle,
65
+ height,
66
+ padding,
67
+ paddingHorizontal,
68
+ paddingVertical,
69
+ margin,
70
+ marginTop,
71
+ marginBottom,
72
+ marginLeft,
73
+ marginRight,
74
+ flexDirection,
75
+ flexWrap,
76
+ alignItems,
77
+ justifyContent,
78
+ alignSelf,
79
+ position,
80
+ top,
81
+ bottom,
82
+ left,
83
+ right,
84
+ width,
85
+ minWidth,
86
+ minHeight,
87
+ maxWidth,
88
+ maxHeight,
89
+ flex,
90
+ flexShrink,
91
+ gap,
92
+ overflow,
93
+ opacity,
94
+ zIndex,
95
+ hoverStyle,
96
+ pressStyle,
97
+ style,
98
+ disabled,
99
+ "data-testid": dataTestId,
100
+ testID,
101
+ as,
102
+ src,
103
+ alt,
104
+ ...rest
105
+ }) => {
106
+ const getContainerStyle = (pressed) => ({
107
+ backgroundColor: pressed && pressStyle?.backgroundColor ? pressStyle.backgroundColor : backgroundColor,
108
+ borderColor,
109
+ borderWidth,
110
+ borderBottomWidth,
111
+ borderBottomColor,
112
+ borderTopWidth,
113
+ borderTopColor,
114
+ borderLeftWidth,
115
+ borderLeftColor,
116
+ borderRightWidth,
117
+ borderRightColor,
118
+ borderRadius,
119
+ borderStyle,
120
+ overflow,
121
+ opacity: opacity ?? (disabled ? 0.5 : void 0),
122
+ zIndex,
123
+ height,
124
+ width,
125
+ minWidth,
126
+ minHeight,
127
+ maxWidth,
128
+ maxHeight,
129
+ padding,
130
+ paddingHorizontal,
131
+ paddingVertical,
132
+ margin,
133
+ marginTop,
134
+ marginBottom,
135
+ marginLeft,
136
+ marginRight,
137
+ flexDirection,
138
+ flexWrap,
139
+ alignItems,
140
+ justifyContent,
141
+ alignSelf,
142
+ position,
143
+ top,
144
+ bottom,
145
+ left,
146
+ right,
147
+ flex,
148
+ flexShrink,
149
+ gap: toGapValue(gap),
150
+ ...style
151
+ });
152
+ const finalTestID = dataTestId || testID;
153
+ const {
154
+ role,
155
+ tabIndex,
156
+ onKeyDown,
157
+ onKeyUp,
158
+ "aria-label": ariaLabel,
159
+ "aria-labelledby": _ariaLabelledBy,
160
+ "aria-current": _ariaCurrent,
161
+ "aria-disabled": ariaDisabled,
162
+ "aria-live": ariaLive,
163
+ "aria-busy": ariaBusy,
164
+ className,
165
+ "data-testid": _dataTestId,
166
+ cursor: _cursor,
167
+ type: _type,
168
+ ...nativeRest
169
+ } = rest;
170
+ const accessibilityRole = typeof role === "string" && roleToAccessibilityRole[role] || (as === "button" ? "button" : void 0);
171
+ const isDisabled = Boolean(disabled || ariaDisabled);
172
+ const accessibilityState = {
173
+ disabled: isDisabled || void 0,
174
+ busy: Boolean(ariaBusy) || void 0
175
+ };
176
+ const accessibilityLiveRegion = ariaLive === "polite" || ariaLive === "assertive" ? ariaLive : void 0;
177
+ if (as === "img" && src) {
178
+ const imageStyle = {
179
+ width,
180
+ height,
181
+ borderRadius,
182
+ position,
183
+ top,
184
+ bottom,
185
+ left,
186
+ right,
187
+ ...style
188
+ };
189
+ return /* @__PURE__ */ jsx(
190
+ Image,
191
+ {
192
+ source: { uri: src },
193
+ style: imageStyle,
194
+ testID: finalTestID,
195
+ accessibilityLabel: typeof ariaLabel === "string" ? ariaLabel : alt,
196
+ resizeMode: "cover",
197
+ ...nativeRest
198
+ }
199
+ );
200
+ }
201
+ if (onPress || as === "button") {
202
+ return /* @__PURE__ */ jsx(
203
+ Pressable,
204
+ {
205
+ onPress: !isDisabled && onPress ? onPress : void 0,
206
+ disabled: isDisabled,
207
+ onLayout,
208
+ onMoveShouldSetResponder,
209
+ onResponderGrant,
210
+ onResponderMove,
211
+ onResponderRelease,
212
+ onResponderTerminate,
213
+ style: ({ pressed }) => getContainerStyle(pressed),
214
+ testID: finalTestID,
215
+ accessibilityRole,
216
+ accessibilityLabel: typeof ariaLabel === "string" ? ariaLabel : void 0,
217
+ accessibilityState,
218
+ accessibilityLiveRegion,
219
+ ...nativeRest,
220
+ children
221
+ }
222
+ );
223
+ }
224
+ return /* @__PURE__ */ jsx(
225
+ View,
226
+ {
227
+ style: getContainerStyle(),
228
+ testID: finalTestID,
229
+ onLayout,
230
+ onMoveShouldSetResponder,
231
+ onResponderGrant,
232
+ onResponderMove,
233
+ onResponderRelease,
234
+ onResponderTerminate,
235
+ accessibilityRole,
236
+ accessibilityLabel: typeof ariaLabel === "string" ? ariaLabel : void 0,
237
+ accessibilityState,
238
+ accessibilityLiveRegion,
239
+ ...nativeRest,
240
+ children
241
+ }
242
+ );
243
+ };
244
+
245
+ // ../../foundation/primitives-native/src/Text.tsx
246
+ import {
247
+ Text as RNText,
248
+ StyleSheet
249
+ } from "react-native";
250
+ import { jsx as jsx2 } from "react/jsx-runtime";
251
+ var roleMap = {
252
+ alert: "alert",
253
+ heading: "header",
254
+ button: "button",
255
+ link: "link",
256
+ text: "text"
257
+ };
258
+ var parseNumericValue = (value) => {
259
+ if (value === void 0) return void 0;
260
+ if (typeof value === "number") return value;
261
+ const parsed = parseFloat(value);
262
+ return isNaN(parsed) ? void 0 : parsed;
263
+ };
264
+ var Text = ({
265
+ children,
266
+ color,
267
+ fontSize,
268
+ fontWeight,
269
+ fontFamily,
270
+ textAlign,
271
+ lineHeight,
272
+ numberOfLines,
273
+ id,
274
+ role,
275
+ testID,
276
+ "data-testid": dataTestId,
277
+ style: styleProp,
278
+ ...props
279
+ }) => {
280
+ let resolvedFontFamily = fontFamily ? fontFamily.split(",")[0].replace(/['"]/g, "").trim() : void 0;
281
+ if (resolvedFontFamily === "Pilat Wide" || resolvedFontFamily === "Pilat Wide Bold" || resolvedFontFamily === "Aktiv Grotesk") {
282
+ resolvedFontFamily = void 0;
283
+ }
284
+ const incomingStyle = StyleSheet.flatten(styleProp);
285
+ const baseStyle = {
286
+ color: color ?? incomingStyle?.color,
287
+ fontSize: typeof fontSize === "number" ? fontSize : void 0,
288
+ fontWeight,
289
+ fontFamily: resolvedFontFamily,
290
+ textDecorationLine: props.textDecoration,
291
+ textAlign: textAlign ?? incomingStyle?.textAlign,
292
+ lineHeight: parseNumericValue(lineHeight ?? incomingStyle?.lineHeight),
293
+ marginTop: parseNumericValue(
294
+ incomingStyle?.marginTop
295
+ ),
296
+ marginBottom: parseNumericValue(
297
+ incomingStyle?.marginBottom
298
+ )
299
+ };
300
+ const accessibilityRole = role ? roleMap[role] : void 0;
301
+ return /* @__PURE__ */ jsx2(
302
+ RNText,
303
+ {
304
+ style: baseStyle,
305
+ numberOfLines,
306
+ testID: dataTestId || testID || id,
307
+ accessibilityRole,
308
+ children
309
+ }
310
+ );
311
+ };
312
+
313
+ // src/CardSelector.tsx
314
+ import { useResolvedTheme, radiusScale } from "@xsolla/xui-core";
315
+
316
+ // src/CardSelectorGroupContext.tsx
317
+ import { createContext, useContext } from "react";
318
+ var CardSelectorGroupContext = createContext(null);
319
+ var useCardSelectorGroup = () => useContext(CardSelectorGroupContext);
320
+
321
+ // src/CardSelector.tsx
322
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
323
+ var platformMetrics = {
324
+ mobile: {
325
+ controlSize: "md",
326
+ controlBox: 18,
327
+ controlIcon: 12,
328
+ titleFontSize: 14,
329
+ titleLineHeight: 16,
330
+ descriptionFontSize: 12,
331
+ descriptionLineHeight: 16,
332
+ padding: 8,
333
+ gap: 8,
334
+ radius: 8
335
+ },
336
+ desktop: {
337
+ controlSize: "lg",
338
+ controlBox: 20,
339
+ controlIcon: 14,
340
+ titleFontSize: 16,
341
+ titleLineHeight: 20,
342
+ descriptionFontSize: 12,
343
+ descriptionLineHeight: 16,
344
+ padding: 10,
345
+ gap: 8,
346
+ radius: 8
347
+ }
348
+ };
349
+ var CheckGlyph = ({
350
+ size,
351
+ color
352
+ }) => /* @__PURE__ */ jsx3(
353
+ "svg",
354
+ {
355
+ width: size,
356
+ height: size,
357
+ viewBox: "0 0 12 12",
358
+ fill: "none",
359
+ xmlns: "http://www.w3.org/2000/svg",
360
+ "aria-hidden": "true",
361
+ focusable: "false",
362
+ children: /* @__PURE__ */ jsx3(
363
+ "path",
364
+ {
365
+ d: "M10.6699 4.2959L6.00293 8.96387C5.72637 9.24043 5.27761 9.24031 5.00098 8.96387L2 5.96289L3.2959 4.66699L5.50098 6.87207L9.37402 3L10.6699 4.2959Z",
366
+ fill: color
367
+ }
368
+ )
369
+ }
370
+ );
371
+ var SelectionControl = ({ type, checked, disabled, metrics, theme }) => {
372
+ const check = theme.colors.control.check;
373
+ const faint = theme.colors.control.faint;
374
+ const backgroundColor = disabled ? check.bgDisable : checked ? check.bg : faint.bg;
375
+ const borderColor = disabled ? check.borderDisable : checked ? check.border : faint.border;
376
+ const iconColor = disabled ? theme.colors.control.text.disable : check.icon;
377
+ return /* @__PURE__ */ jsx3(
378
+ Box,
379
+ {
380
+ width: metrics.controlBox,
381
+ height: metrics.controlBox,
382
+ flexShrink: 0,
383
+ alignItems: "center",
384
+ justifyContent: "center",
385
+ borderWidth: 1,
386
+ borderStyle: "solid",
387
+ borderColor,
388
+ backgroundColor,
389
+ borderRadius: type === "radio" ? radiusScale["999"] : theme.shape.checkbox[metrics.controlSize].borderRadius,
390
+ "aria-hidden": "true",
391
+ testID: "card-selector-control",
392
+ children: checked && (type === "radio" ? /* @__PURE__ */ jsx3(
393
+ Box,
394
+ {
395
+ width: metrics.controlIcon,
396
+ height: metrics.controlIcon,
397
+ borderRadius: radiusScale["999"],
398
+ backgroundColor: iconColor
399
+ }
400
+ ) : /* @__PURE__ */ jsx3(CheckGlyph, { size: metrics.controlIcon, color: iconColor }))
401
+ }
402
+ );
403
+ };
404
+ var asPlainText = (value) => typeof value === "string" ? value : void 0;
405
+ var CardSelector = ({
406
+ title,
407
+ description,
408
+ imageSrc,
409
+ imageAlt = "",
410
+ value,
411
+ type: typeProp,
412
+ control: controlProp,
413
+ platform: platformProp,
414
+ checked: checkedProp,
415
+ defaultChecked = false,
416
+ onChange,
417
+ state = "default",
418
+ disabled,
419
+ width = "100%",
420
+ "aria-label": ariaLabel,
421
+ testID,
422
+ themeMode,
423
+ themeProductContext = "b2c"
424
+ }) => {
425
+ const { theme } = useResolvedTheme({ themeMode, themeProductContext });
426
+ const group = useCardSelectorGroup();
427
+ const nodeRef = useRef(null);
428
+ const type = typeProp ?? group?.type ?? "radio";
429
+ const control = controlProp ?? group?.control ?? "text";
430
+ const platform = platformProp ?? group?.platform ?? "mobile";
431
+ const metrics = platformMetrics[platform];
432
+ const isDisabled = Boolean(disabled) || state === "disabled" || Boolean(group?.disabled);
433
+ const [internalChecked, setInternalChecked] = useState(defaultChecked);
434
+ const isGroupControlled = group !== null && type === "radio" && value !== void 0;
435
+ const isControlled = checkedProp !== void 0;
436
+ const checked = isControlled ? Boolean(checkedProp) : isGroupControlled ? group?.selectedValue === value : internalChecked;
437
+ const toggle = useCallback(() => {
438
+ if (isDisabled) return;
439
+ if (type === "radio" && checked && isGroupControlled) return;
440
+ const next = type === "radio" ? true : !checked;
441
+ if (!isControlled && !isGroupControlled) {
442
+ setInternalChecked(next);
443
+ }
444
+ if (group && isGroupControlled && value !== void 0) {
445
+ group.select(value);
446
+ }
447
+ onChange?.(next, value);
448
+ }, [
449
+ checked,
450
+ group,
451
+ isControlled,
452
+ isDisabled,
453
+ isGroupControlled,
454
+ onChange,
455
+ type,
456
+ value
457
+ ]);
458
+ const focus = useCallback(() => {
459
+ const node = nodeRef.current;
460
+ if (node && typeof node.focus === "function") node.focus();
461
+ }, []);
462
+ const itemRef = useRef({
463
+ value: value ?? "",
464
+ disabled: Boolean(isDisabled),
465
+ focus
466
+ });
467
+ useEffect(() => {
468
+ itemRef.current.value = value ?? "";
469
+ itemRef.current.disabled = isDisabled;
470
+ itemRef.current.focus = focus;
471
+ });
472
+ const registerItem = group?.register;
473
+ useEffect(() => {
474
+ if (!registerItem) return void 0;
475
+ return registerItem(itemRef.current);
476
+ }, [registerItem]);
477
+ const handleKeyDown = (event) => {
478
+ if (isDisabled) return;
479
+ const key = event.key;
480
+ if (key === " " || key === "Spacebar" || key === "Space" || key === "Enter") {
481
+ event.preventDefault();
482
+ toggle();
483
+ return;
484
+ }
485
+ if (!group || type !== "radio") return;
486
+ const delta = key === "ArrowRight" || key === "ArrowDown" ? 1 : key === "ArrowLeft" || key === "ArrowUp" ? -1 : 0;
487
+ if (delta === 0) return;
488
+ event.preventDefault();
489
+ group.moveFocus(value, delta);
490
+ };
491
+ const showControl = control === "text" || checked;
492
+ const isOnImage = control === "onImage";
493
+ const surfaceBackground = checked ? theme.colors.overlay.brand : state === "press" ? theme.colors.overlay.brand : state === "hover" ? theme.colors.overlay.monoHover : theme.colors.overlay.mono;
494
+ const titleColor = isOnImage && checked ? theme.colors.control.check.bg : theme.colors.content.primary;
495
+ const derivedLabel = [asPlainText(title), asPlainText(description)].filter(Boolean).join(", ");
496
+ const media = /* @__PURE__ */ jsxs(
497
+ Box,
498
+ {
499
+ position: "relative",
500
+ width: "100%",
501
+ overflow: "hidden",
502
+ borderRadius: isOnImage ? metrics.radius : 0,
503
+ style: { aspectRatio: 1 },
504
+ testID: "card-selector-media",
505
+ children: [
506
+ imageSrc && /* @__PURE__ */ jsx3(
507
+ Box,
508
+ {
509
+ as: "img",
510
+ src: imageSrc,
511
+ alt: imageAlt,
512
+ position: "absolute",
513
+ top: 0,
514
+ left: 0,
515
+ width: "100%",
516
+ height: "100%",
517
+ style: { objectFit: "cover", pointerEvents: "none" },
518
+ testID: "card-selector-image"
519
+ }
520
+ ),
521
+ isOnImage && showControl && /* @__PURE__ */ jsx3(
522
+ Box,
523
+ {
524
+ position: "absolute",
525
+ top: 0,
526
+ left: 0,
527
+ right: 0,
528
+ bottom: 0,
529
+ alignItems: "center",
530
+ justifyContent: "center",
531
+ zIndex: 1,
532
+ children: /* @__PURE__ */ jsx3(
533
+ SelectionControl,
534
+ {
535
+ type,
536
+ checked,
537
+ disabled: isDisabled,
538
+ metrics,
539
+ theme
540
+ }
541
+ )
542
+ }
543
+ )
544
+ ]
545
+ }
546
+ );
547
+ const label = /* @__PURE__ */ jsxs(Box, { flexDirection: "column", gap: 2, flexShrink: 1, overflow: "hidden", children: [
548
+ /* @__PURE__ */ jsx3(
549
+ Text,
550
+ {
551
+ color: titleColor,
552
+ fontFamily: theme.fonts.body,
553
+ fontSize: metrics.titleFontSize,
554
+ lineHeight: metrics.titleLineHeight,
555
+ fontWeight: "500",
556
+ numberOfLines: 1,
557
+ testID: "card-selector-title",
558
+ children: title
559
+ }
560
+ ),
561
+ description && /* @__PURE__ */ jsx3(
562
+ Text,
563
+ {
564
+ color: theme.colors.content.tertiary,
565
+ fontFamily: theme.fonts.body,
566
+ fontSize: metrics.descriptionFontSize,
567
+ lineHeight: metrics.descriptionLineHeight,
568
+ fontWeight: "400",
569
+ numberOfLines: 1,
570
+ testID: "card-selector-description",
571
+ children: description
572
+ }
573
+ )
574
+ ] });
575
+ const hasGroupSelection = group?.selectedValue !== void 0;
576
+ const isRovingTarget = checked || !hasGroupSelection && Boolean(group?.isFirstEnabled(value));
577
+ const isRovingRadio = Boolean(group) && type === "radio";
578
+ const tabIndex = isDisabled || isRovingRadio && !isRovingTarget ? -1 : 0;
579
+ return /* @__PURE__ */ jsxs(
580
+ Box,
581
+ {
582
+ ref: nodeRef,
583
+ width,
584
+ flexDirection: "column",
585
+ gap: isOnImage ? metrics.gap : 0,
586
+ opacity: isDisabled ? 0.48 : 1,
587
+ cursor: isDisabled ? "not-allowed" : "pointer",
588
+ role: type === "radio" ? "radio" : "checkbox",
589
+ "aria-checked": checked,
590
+ "aria-disabled": isDisabled || void 0,
591
+ "aria-label": ariaLabel || derivedLabel || void 0,
592
+ tabIndex,
593
+ onPress: isDisabled ? void 0 : toggle,
594
+ onKeyDown: handleKeyDown,
595
+ focusStyle: {
596
+ outlineColor: theme.colors.border.brand,
597
+ outlineWidth: 2,
598
+ outlineOffset: 2,
599
+ outlineStyle: "solid"
600
+ },
601
+ testID: testID || "card-selector",
602
+ children: [
603
+ /* @__PURE__ */ jsxs(
604
+ Box,
605
+ {
606
+ flexDirection: "column",
607
+ overflow: "hidden",
608
+ borderRadius: metrics.radius,
609
+ borderWidth: 2,
610
+ borderStyle: "solid",
611
+ borderColor: checked ? theme.colors.border.brand : "transparent",
612
+ backgroundColor: surfaceBackground,
613
+ hoverStyle: !isDisabled && !checked && !isOnImage ? { backgroundColor: theme.colors.overlay.monoHover } : void 0,
614
+ pressStyle: !isDisabled && !isOnImage ? { backgroundColor: theme.colors.overlay.brand } : void 0,
615
+ testID: "card-selector-surface",
616
+ children: [
617
+ media,
618
+ !isOnImage && /* @__PURE__ */ jsxs(
619
+ Box,
620
+ {
621
+ flexDirection: "row",
622
+ alignItems: "center",
623
+ gap: metrics.gap,
624
+ padding: metrics.padding,
625
+ testID: "card-selector-bar",
626
+ children: [
627
+ /* @__PURE__ */ jsx3(
628
+ SelectionControl,
629
+ {
630
+ type,
631
+ checked,
632
+ disabled: isDisabled,
633
+ metrics,
634
+ theme
635
+ }
636
+ ),
637
+ label
638
+ ]
639
+ }
640
+ )
641
+ ]
642
+ }
643
+ ),
644
+ isOnImage && label
645
+ ]
646
+ }
647
+ );
648
+ };
649
+
650
+ // src/CardSelectorGroup.tsx
651
+ import { useCallback as useCallback2, useMemo, useState as useState2 } from "react";
652
+ import { useResolvedTheme as useResolvedTheme2 } from "@xsolla/xui-core";
653
+ import { jsx as jsx4 } from "react/jsx-runtime";
654
+ var CardSelectorGroup = ({
655
+ children,
656
+ type = "radio",
657
+ value,
658
+ defaultValue,
659
+ onValueChange,
660
+ platform,
661
+ control,
662
+ disabled = false,
663
+ gap = 12,
664
+ "aria-label": ariaLabel,
665
+ "aria-labelledby": ariaLabelledBy,
666
+ testID,
667
+ themeMode,
668
+ themeProductContext = "b2c"
669
+ }) => {
670
+ useResolvedTheme2({ themeMode, themeProductContext });
671
+ const [items, setItems] = useState2([]);
672
+ const [internalValue, setInternalValue] = useState2(defaultValue);
673
+ const isControlled = value !== void 0;
674
+ const selectedValue = isControlled ? value : internalValue;
675
+ const register = useCallback2((item) => {
676
+ setItems((prev) => [...prev, item]);
677
+ return () => setItems((prev) => prev.filter((entry) => entry !== item));
678
+ }, []);
679
+ const select = useCallback2(
680
+ (next) => {
681
+ if (!isControlled) setInternalValue(next);
682
+ onValueChange?.(next);
683
+ },
684
+ [isControlled, onValueChange]
685
+ );
686
+ const moveFocus = useCallback2(
687
+ (fromValue, delta) => {
688
+ const enabled = items.filter((item) => !item.disabled);
689
+ if (enabled.length === 0) return;
690
+ const currentIndex = enabled.findIndex(
691
+ (item) => item.value === fromValue
692
+ );
693
+ const nextIndex = currentIndex === -1 ? 0 : (currentIndex + delta + enabled.length) % enabled.length;
694
+ const next = enabled[nextIndex];
695
+ if (!next) return;
696
+ next.focus();
697
+ if (type === "radio" && next.value) select(next.value);
698
+ },
699
+ [items, select, type]
700
+ );
701
+ const isFirstEnabled = useCallback2(
702
+ (candidate) => {
703
+ const first = items.find((item) => !item.disabled);
704
+ return Boolean(first) && first?.value === candidate;
705
+ },
706
+ [items]
707
+ );
708
+ const contextValue = useMemo(
709
+ () => ({
710
+ type,
711
+ platform,
712
+ control,
713
+ disabled,
714
+ selectedValue,
715
+ select,
716
+ register,
717
+ moveFocus,
718
+ isFirstEnabled
719
+ }),
720
+ [
721
+ control,
722
+ disabled,
723
+ isFirstEnabled,
724
+ moveFocus,
725
+ platform,
726
+ register,
727
+ select,
728
+ selectedValue,
729
+ type
730
+ ]
731
+ );
732
+ return /* @__PURE__ */ jsx4(CardSelectorGroupContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx4(
733
+ Box,
734
+ {
735
+ role: type === "radio" ? "radiogroup" : "group",
736
+ "aria-label": ariaLabelledBy ? void 0 : ariaLabel,
737
+ "aria-labelledby": ariaLabelledBy,
738
+ "aria-disabled": disabled || void 0,
739
+ flexDirection: "row",
740
+ flexWrap: "wrap",
741
+ alignItems: "flex-start",
742
+ gap,
743
+ testID: testID || "card-selector-group",
744
+ children
745
+ }
746
+ ) });
747
+ };
748
+ export {
749
+ CardSelector,
750
+ CardSelectorGroup
751
+ };
752
+ //# sourceMappingURL=index.mjs.map