@xsolla/xui-tabs 0.73.0 → 0.74.0-pr112.1769643402

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.
@@ -25,8 +25,12 @@ interface TabsProps {
25
25
  onChange?: (id: string) => void;
26
26
  /** Size variant of the tabs */
27
27
  size?: "xl" | "lg" | "md" | "sm";
28
- /** Whether to align tabs to the left */
28
+ /** Visual variant of the tabs */
29
+ variant?: "line" | "segmented";
30
+ /** Whether to align tabs to the left (only for line variant) */
29
31
  alignLeft?: boolean;
32
+ /** Whether the component should stretch to fill its container */
33
+ stretched?: boolean;
30
34
  /** Accessible label for the tab list */
31
35
  "aria-label"?: string;
32
36
  /** ID of element that labels this tab list */
@@ -47,6 +51,9 @@ interface TabsProps {
47
51
  * - End: Jump to last tab
48
52
  * - Enter/Space: Activate focused tab (when activateOnFocus is false)
49
53
  *
54
+ * Variants:
55
+ * - "line" (default): Traditional underlined tabs
56
+ * - "segmented": Button-group style segmented control
50
57
  */
51
58
  declare const Tabs: React.FC<TabsProps>;
52
59
  /**
package/native/index.d.ts CHANGED
@@ -25,8 +25,12 @@ interface TabsProps {
25
25
  onChange?: (id: string) => void;
26
26
  /** Size variant of the tabs */
27
27
  size?: "xl" | "lg" | "md" | "sm";
28
- /** Whether to align tabs to the left */
28
+ /** Visual variant of the tabs */
29
+ variant?: "line" | "segmented";
30
+ /** Whether to align tabs to the left (only for line variant) */
29
31
  alignLeft?: boolean;
32
+ /** Whether the component should stretch to fill its container */
33
+ stretched?: boolean;
30
34
  /** Accessible label for the tab list */
31
35
  "aria-label"?: string;
32
36
  /** ID of element that labels this tab list */
@@ -47,6 +51,9 @@ interface TabsProps {
47
51
  * - End: Jump to last tab
48
52
  * - Enter/Space: Activate focused tab (when activateOnFocus is false)
49
53
  *
54
+ * Variants:
55
+ * - "line" (default): Traditional underlined tabs
56
+ * - "segmented": Button-group style segmented control
50
57
  */
51
58
  declare const Tabs: React.FC<TabsProps>;
52
59
  /**
package/native/index.js CHANGED
@@ -521,12 +521,15 @@ TextAreaPrimitive.displayName = "TextAreaPrimitive";
521
521
  var import_xui_core = require("@xsolla/xui-core");
522
522
  var import_xui_badge = require("@xsolla/xui-badge");
523
523
  var import_jsx_runtime8 = require("react/jsx-runtime");
524
+ var isWeb = typeof document !== "undefined";
524
525
  var Tabs = ({
525
526
  tabs,
526
527
  activeTabId,
527
528
  onChange,
528
529
  size = "md",
530
+ variant = "line",
529
531
  alignLeft = true,
532
+ stretched = false,
530
533
  "aria-label": ariaLabel,
531
534
  "aria-labelledby": ariaLabelledBy,
532
535
  activateOnFocus = true,
@@ -534,10 +537,27 @@ var Tabs = ({
534
537
  testID
535
538
  }) => {
536
539
  const { theme } = (0, import_xui_core.useDesignSystem)();
537
- const sizeStyles = theme.sizing.tabs(size);
540
+ const isSegmented = variant === "segmented";
538
541
  const tabListId = id ? `${id}-tablist` : void 0;
539
542
  const [_focusedIndex, setFocusedIndex] = (0, import_react4.useState)(-1);
540
543
  const tabRefs = (0, import_react4.useRef)([]);
544
+ const containerRef = (0, import_react4.useRef)(null);
545
+ const [indicatorStyle, setIndicatorStyle] = (0, import_react4.useState)({ left: 0, width: 0, initialized: false });
546
+ (0, import_react4.useEffect)(() => {
547
+ if (!isSegmented || !isWeb) return;
548
+ const activeIndex = tabs.findIndex((tab) => tab.id === activeTabId);
549
+ const activeTabEl = tabRefs.current[activeIndex];
550
+ const containerEl = containerRef.current;
551
+ if (activeTabEl && containerEl) {
552
+ const containerRect = containerEl.getBoundingClientRect();
553
+ const tabRect = activeTabEl.getBoundingClientRect();
554
+ setIndicatorStyle({
555
+ left: tabRect.left - containerRect.left,
556
+ width: tabRect.width,
557
+ initialized: true
558
+ });
559
+ }
560
+ }, [activeTabId, tabs, isSegmented]);
541
561
  const enabledIndices = tabs.map((tab, index) => !tab.disabled ? index : -1).filter((i) => i !== -1);
542
562
  const focusTab = (0, import_react4.useCallback)((index) => {
543
563
  const tabElement = tabRefs.current[index];
@@ -605,6 +625,141 @@ var Tabs = ({
605
625
  },
606
626
  [enabledIndices, focusTab, activateOnFocus, onChange, tabs]
607
627
  );
628
+ if (isSegmented) {
629
+ const segmentedStyles = theme.sizing.tabsSegmented(size);
630
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
631
+ Box,
632
+ {
633
+ as: "nav",
634
+ role: "tablist",
635
+ id: tabListId,
636
+ "aria-label": ariaLabel,
637
+ "aria-labelledby": ariaLabelledBy,
638
+ "aria-orientation": "horizontal",
639
+ testID,
640
+ ref: (el) => {
641
+ containerRef.current = el;
642
+ },
643
+ flexDirection: "row",
644
+ alignItems: "center",
645
+ flexShrink: 0,
646
+ position: "relative",
647
+ width: stretched ? "100%" : "fit-content",
648
+ height: segmentedStyles.height,
649
+ backgroundColor: theme.colors.control.segmented.bg,
650
+ borderRadius: segmentedStyles.containerRadius,
651
+ padding: segmentedStyles.containerPadding,
652
+ overflow: "hidden",
653
+ children: [
654
+ isWeb && indicatorStyle.initialized && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
655
+ Box,
656
+ {
657
+ position: "absolute",
658
+ zIndex: 0,
659
+ height: `calc(100% - ${segmentedStyles.containerPadding * 2}px)`,
660
+ backgroundColor: theme.colors.control.segmented.bgActive,
661
+ borderRadius: segmentedStyles.itemRadius,
662
+ style: {
663
+ left: indicatorStyle.left,
664
+ width: indicatorStyle.width,
665
+ transition: "left 200ms ease-out, width 200ms ease-out",
666
+ pointerEvents: "none"
667
+ },
668
+ "aria-hidden": true
669
+ }
670
+ ),
671
+ tabs.map((tab, index) => {
672
+ const isActive = tab.id === activeTabId;
673
+ const isDisabled = tab.disabled;
674
+ const tabId = id ? `${id}-tab-${tab.id}` : void 0;
675
+ const tabPanelId = id ? `${id}-tabpanel-${tab.id}` : void 0;
676
+ const handlePress = () => {
677
+ if (!isDisabled && onChange) {
678
+ onChange(tab.id);
679
+ }
680
+ };
681
+ const handleFocus = () => {
682
+ setFocusedIndex(index);
683
+ };
684
+ const textColor = isDisabled ? theme.colors.control.text.disable : isActive ? theme.colors.control.segmented.textActive : theme.colors.control.text.primary;
685
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
686
+ Box,
687
+ {
688
+ as: "button",
689
+ role: "tab",
690
+ id: tabId,
691
+ "aria-selected": isActive,
692
+ "aria-disabled": isDisabled || void 0,
693
+ "aria-controls": tabPanelId,
694
+ "aria-label": tab["aria-label"],
695
+ tabIndex: isActive ? 0 : -1,
696
+ disabled: isDisabled,
697
+ ref: (el) => {
698
+ tabRefs.current[index] = el;
699
+ },
700
+ onPress: handlePress,
701
+ onFocus: handleFocus,
702
+ onKeyDown: (e) => handleKeyDown(e, index),
703
+ flex: stretched ? 1 : void 0,
704
+ flexShrink: 0,
705
+ position: "relative",
706
+ zIndex: 1,
707
+ height: "100%",
708
+ paddingHorizontal: segmentedStyles.itemPaddingHorizontal,
709
+ paddingVertical: segmentedStyles.itemPaddingVertical,
710
+ flexDirection: "row",
711
+ alignItems: "center",
712
+ justifyContent: "center",
713
+ gap: segmentedStyles.gap,
714
+ backgroundColor: !isWeb && isActive ? theme.colors.control.segmented.bgActive : "transparent",
715
+ borderRadius: segmentedStyles.itemRadius,
716
+ cursor: isDisabled ? "not-allowed" : "pointer",
717
+ hoverStyle: !isDisabled && !isActive ? {
718
+ backgroundColor: theme.colors.control.segmented.bgHover
719
+ } : void 0,
720
+ children: [
721
+ tab.icon && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
722
+ Icon,
723
+ {
724
+ size: segmentedStyles.iconSize,
725
+ color: textColor,
726
+ "aria-hidden": true,
727
+ children: tab.icon
728
+ }
729
+ ),
730
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
731
+ Text,
732
+ {
733
+ color: textColor,
734
+ fontSize: segmentedStyles.fontSize,
735
+ fontWeight: "400",
736
+ textAlign: "center",
737
+ whiteSpace: "nowrap",
738
+ overflow: "hidden",
739
+ textOverflow: "ellipsis",
740
+ children: tab.label
741
+ }
742
+ ),
743
+ tab.counter !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Box, { marginLeft: 2, "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
744
+ Text,
745
+ {
746
+ color: textColor,
747
+ fontSize: segmentedStyles.fontSize,
748
+ fontWeight: "400",
749
+ children: tab.counter
750
+ }
751
+ ) }),
752
+ tab.badge && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Box, { marginLeft: 2, "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_xui_badge.Badge, { size: "sm", children: typeof tab.badge === "string" || typeof tab.badge === "number" ? tab.badge : void 0 }) })
753
+ ]
754
+ },
755
+ tab.id
756
+ );
757
+ })
758
+ ]
759
+ }
760
+ );
761
+ }
762
+ const lineStyles = theme.sizing.tabs(size);
608
763
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
609
764
  Box,
610
765
  {
@@ -618,8 +773,8 @@ var Tabs = ({
618
773
  flexDirection: "row",
619
774
  alignItems: "flex-end",
620
775
  justifyContent: alignLeft ? "flex-start" : "center",
621
- width: "100%",
622
- height: sizeStyles.height,
776
+ width: stretched ? "100%" : "fit-content",
777
+ height: lineStyles.height,
623
778
  borderBottomWidth: 1,
624
779
  borderBottomColor: theme.colors.border.secondary,
625
780
  borderStyle: "solid",
@@ -657,12 +812,12 @@ var Tabs = ({
657
812
  onPress: handlePress,
658
813
  onFocus: handleFocus,
659
814
  onKeyDown: (e) => handleKeyDown(e, index),
660
- height: sizeStyles.height,
661
- paddingHorizontal: sizeStyles.paddingHorizontal,
815
+ height: lineStyles.height,
816
+ paddingHorizontal: lineStyles.paddingHorizontal,
662
817
  flexDirection: "row",
663
818
  alignItems: "center",
664
819
  justifyContent: "center",
665
- gap: sizeStyles.gap,
820
+ gap: lineStyles.gap,
666
821
  position: "relative",
667
822
  borderBottomWidth,
668
823
  borderBottomColor,
@@ -678,12 +833,12 @@ var Tabs = ({
678
833
  outlineOffset: -2
679
834
  },
680
835
  children: [
681
- tab.icon && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Icon, { size: sizeStyles.iconSize, color: textColor, "aria-hidden": true, children: tab.icon }),
836
+ tab.icon && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Icon, { size: lineStyles.iconSize, color: textColor, "aria-hidden": true, children: tab.icon }),
682
837
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
683
838
  Text,
684
839
  {
685
840
  color: textColor,
686
- fontSize: sizeStyles.fontSize,
841
+ fontSize: lineStyles.fontSize,
687
842
  fontWeight: isActive ? "600" : "500",
688
843
  children: tab.label
689
844
  }
@@ -692,7 +847,7 @@ var Tabs = ({
692
847
  Text,
693
848
  {
694
849
  color: theme.colors.content.brand.primary,
695
- fontSize: sizeStyles.fontSize,
850
+ fontSize: lineStyles.fontSize,
696
851
  fontWeight: "500",
697
852
  "aria-label": `${tab.counter} items`,
698
853
  children: tab.counter
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Flowtype definitions for index
3
+ * Generated by Flowgen from a Typescript Definition
4
+ * Flowgen v1.21.0
5
+ * @flow
6
+ */
7
+
8
+ import React from "react";
9
+ declare interface TabItemType {
10
+ /**
11
+ * Unique identifier for the tab
12
+ */
13
+ id: string;
14
+
15
+ /**
16
+ * Display label for the tab
17
+ */
18
+ label: string;
19
+
20
+ /**
21
+ * Optional icon to display before the label
22
+ */
23
+ icon?: React.ReactNode;
24
+
25
+ /**
26
+ * Optional counter to display after the label
27
+ */
28
+ counter?: string | number;
29
+
30
+ /**
31
+ * Optional badge to display
32
+ */
33
+ badge?: boolean | string | number;
34
+
35
+ /**
36
+ * Whether the tab is disabled
37
+ */
38
+ disabled?: boolean;
39
+
40
+ /**
41
+ * Accessible label for screen readers (defaults to label)
42
+ */
43
+ "aria-label"?: string;
44
+ }
45
+ declare interface TabsProps {
46
+ /**
47
+ * Array of tab items
48
+ */
49
+ tabs: TabItemType[];
50
+
51
+ /**
52
+ * ID of the currently active tab
53
+ */
54
+ activeTabId?: string;
55
+
56
+ /**
57
+ * Callback when a tab is selected
58
+ */
59
+ onChange?: (id: string) => void;
60
+
61
+ /**
62
+ * Size variant of the tabs
63
+ */
64
+ size?: "xl" | "lg" | "md" | "sm";
65
+
66
+ /**
67
+ * Visual variant of the tabs
68
+ */
69
+ variant?: "line" | "segmented";
70
+
71
+ /**
72
+ * Whether to align tabs to the left (only for line variant)
73
+ */
74
+ alignLeft?: boolean;
75
+
76
+ /**
77
+ * Whether the component should stretch to fill its container
78
+ */
79
+ stretched?: boolean;
80
+
81
+ /**
82
+ * Accessible label for the tab list
83
+ */
84
+ "aria-label"?: string;
85
+
86
+ /**
87
+ * ID of element that labels this tab list
88
+ */
89
+ "aria-labelledby"?: string;
90
+
91
+ /**
92
+ * Whether keyboard navigation should automatically activate tabs
93
+ */
94
+ activateOnFocus?: boolean;
95
+
96
+ /**
97
+ * HTML id attribute
98
+ */
99
+ id?: string;
100
+
101
+ /**
102
+ * Test ID for testing frameworks
103
+ */
104
+ testID?: string;
105
+ }
106
+ /**
107
+ * Tabs - An accessible tabbed interface component
108
+ *
109
+ * Implements WAI-ARIA Tabs pattern with proper keyboard navigation:
110
+ * - Arrow Left/Right: Navigate between tabs
111
+ * - Home: Jump to first tab
112
+ * - End: Jump to last tab
113
+ * - Enter/Space: Activate focused tab (when activateOnFocus is false)
114
+ *
115
+ * Variants:
116
+ * - "line" (default): Traditional underlined tabs
117
+ * - "segmented": Button-group style segmented control
118
+ */
119
+ declare var Tabs: React.FC<TabsProps>;
120
+ /**
121
+ * TabPanel - Container for tab content with proper accessibility attributes
122
+ * @example <TabPanel id="tab1" tabsId="my-tabs" hidden={activeTab !== 'tab1'}>
123
+ * <p>Content for tab 1</p>
124
+ * </TabPanel>
125
+ */
126
+ declare interface TabPanelProps {
127
+ /**
128
+ * ID matching the tab's id
129
+ */
130
+ id: string;
131
+
132
+ /**
133
+ * ID of the parent Tabs component
134
+ */
135
+ tabsId: string;
136
+
137
+ /**
138
+ * Whether the panel is hidden
139
+ */
140
+ hidden?: boolean;
141
+
142
+ /**
143
+ * Panel content
144
+ */
145
+ children: React.ReactNode;
146
+
147
+ /**
148
+ * Accessible label for the panel
149
+ */
150
+ "aria-label"?: string;
151
+
152
+ /**
153
+ * Test ID for testing frameworks
154
+ */
155
+ testID?: string;
156
+ }
157
+ declare var TabPanel: React.FC<TabPanelProps>;
158
+ export type { TabItemType, TabPanelProps, TabsProps };
159
+ declare export { TabPanel, Tabs };