@xsolla/xui-tabs 0.74.0 → 0.75.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.
@@ -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,146 @@ 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
+ focusStyle: {
721
+ outlineColor: theme.colors.border.brand,
722
+ outlineWidth: 2,
723
+ outlineOffset: -2
724
+ },
725
+ children: [
726
+ tab.icon && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
727
+ Icon,
728
+ {
729
+ size: segmentedStyles.iconSize,
730
+ color: textColor,
731
+ "aria-hidden": true,
732
+ children: tab.icon
733
+ }
734
+ ),
735
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
736
+ Text,
737
+ {
738
+ color: textColor,
739
+ fontSize: segmentedStyles.fontSize,
740
+ fontWeight: "400",
741
+ textAlign: "center",
742
+ whiteSpace: "nowrap",
743
+ overflow: "hidden",
744
+ textOverflow: "ellipsis",
745
+ children: tab.label
746
+ }
747
+ ),
748
+ tab.counter !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Box, { marginLeft: 2, "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
749
+ Text,
750
+ {
751
+ color: textColor,
752
+ fontSize: segmentedStyles.fontSize,
753
+ fontWeight: "400",
754
+ children: tab.counter
755
+ }
756
+ ) }),
757
+ 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 }) })
758
+ ]
759
+ },
760
+ tab.id
761
+ );
762
+ })
763
+ ]
764
+ }
765
+ );
766
+ }
767
+ const lineStyles = theme.sizing.tabs(size);
608
768
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
609
769
  Box,
610
770
  {
@@ -618,8 +778,8 @@ var Tabs = ({
618
778
  flexDirection: "row",
619
779
  alignItems: "flex-end",
620
780
  justifyContent: alignLeft ? "flex-start" : "center",
621
- width: "100%",
622
- height: sizeStyles.height,
781
+ width: stretched ? "100%" : "fit-content",
782
+ height: lineStyles.height,
623
783
  borderBottomWidth: 1,
624
784
  borderBottomColor: theme.colors.border.secondary,
625
785
  borderStyle: "solid",
@@ -657,12 +817,12 @@ var Tabs = ({
657
817
  onPress: handlePress,
658
818
  onFocus: handleFocus,
659
819
  onKeyDown: (e) => handleKeyDown(e, index),
660
- height: sizeStyles.height,
661
- paddingHorizontal: sizeStyles.paddingHorizontal,
820
+ height: lineStyles.height,
821
+ paddingHorizontal: lineStyles.paddingHorizontal,
662
822
  flexDirection: "row",
663
823
  alignItems: "center",
664
824
  justifyContent: "center",
665
- gap: sizeStyles.gap,
825
+ gap: lineStyles.gap,
666
826
  position: "relative",
667
827
  borderBottomWidth,
668
828
  borderBottomColor,
@@ -678,12 +838,12 @@ var Tabs = ({
678
838
  outlineOffset: -2
679
839
  },
680
840
  children: [
681
- tab.icon && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Icon, { size: sizeStyles.iconSize, color: textColor, "aria-hidden": true, children: tab.icon }),
841
+ tab.icon && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Icon, { size: lineStyles.iconSize, color: textColor, "aria-hidden": true, children: tab.icon }),
682
842
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
683
843
  Text,
684
844
  {
685
845
  color: textColor,
686
- fontSize: sizeStyles.fontSize,
846
+ fontSize: lineStyles.fontSize,
687
847
  fontWeight: isActive ? "600" : "500",
688
848
  children: tab.label
689
849
  }
@@ -692,7 +852,7 @@ var Tabs = ({
692
852
  Text,
693
853
  {
694
854
  color: theme.colors.content.brand.primary,
695
- fontSize: sizeStyles.fontSize,
855
+ fontSize: lineStyles.fontSize,
696
856
  fontWeight: "500",
697
857
  "aria-label": `${tab.counter} items`,
698
858
  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 };