@xsolla/xui-tabs 0.150.0 → 0.152.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 (2) hide show
  1. package/README.md +125 -322
  2. package/package.json +4 -4
package/README.md CHANGED
@@ -1,437 +1,240 @@
1
1
  # Tabs
2
2
 
3
- A cross-platform React tabs component for organizing content into multiple panels that users can switch between. Implements WAI-ARIA tablist pattern for accessibility.
4
-
5
- **Variants:**
6
- - **Line** (default): Traditional underlined tabs with bottom border indicator
7
- - **Segmented**: Button-group style segmented control with sliding active indicator
3
+ A cross-platform React tab list with two visual variants `line` (default underlined) and `segmented` (button-group with sliding indicator on web). Implements the WAI-ARIA tablist pattern with full keyboard navigation.
8
4
 
9
5
  ## Installation
10
6
 
11
7
  ```bash
12
8
  npm install @xsolla/xui-tabs
13
- # or
14
- yarn add @xsolla/xui-tabs
15
9
  ```
16
10
 
17
- ## Demo
11
+ ## Imports
12
+
13
+ ```tsx
14
+ import {
15
+ Tabs,
16
+ TabPanel,
17
+ type TabItemType,
18
+ type TabsProps,
19
+ type TabPanelProps,
20
+ } from '@xsolla/xui-tabs';
21
+ ```
18
22
 
19
- ### Basic Tabs
23
+ ## Quick start
20
24
 
21
25
  ```tsx
22
26
  import * as React from 'react';
23
27
  import { Tabs, TabPanel } from '@xsolla/xui-tabs';
24
28
 
25
- export default function BasicTabs() {
26
- const [activeTab, setActiveTab] = React.useState('tab1');
27
-
29
+ export default function QuickStart() {
30
+ const [active, setActive] = React.useState('overview');
28
31
  const tabs = [
29
- { id: 'tab1', label: 'Overview' },
30
- { id: 'tab2', label: 'Features' },
31
- { id: 'tab3', label: 'Pricing' },
32
+ { id: 'overview', label: 'Overview' },
33
+ { id: 'pricing', label: 'Pricing' },
32
34
  ];
33
-
34
35
  return (
35
36
  <div>
36
- <Tabs
37
- id="basic-tabs"
38
- tabs={tabs}
39
- activeTabId={activeTab}
40
- onChange={setActiveTab}
41
- />
42
- <TabPanel id="tab1" tabsId="basic-tabs" hidden={activeTab !== 'tab1'}>
43
- <p>Overview content goes here.</p>
44
- </TabPanel>
45
- <TabPanel id="tab2" tabsId="basic-tabs" hidden={activeTab !== 'tab2'}>
46
- <p>Features content goes here.</p>
47
- </TabPanel>
48
- <TabPanel id="tab3" tabsId="basic-tabs" hidden={activeTab !== 'tab3'}>
49
- <p>Pricing content goes here.</p>
50
- </TabPanel>
37
+ <Tabs id="qs" tabs={tabs} activeTabId={active} onChange={setActive} />
38
+ <TabPanel id="overview" tabsId="qs" hidden={active !== 'overview'}>Overview content</TabPanel>
39
+ <TabPanel id="pricing" tabsId="qs" hidden={active !== 'pricing'}>Pricing content</TabPanel>
51
40
  </div>
52
41
  );
53
42
  }
54
43
  ```
55
44
 
56
- ### Tabs with Icons
45
+ ## API Reference
57
46
 
58
- ```tsx
59
- import * as React from 'react';
60
- import { Tabs } from '@xsolla/xui-tabs';
61
- import { Home, User, Settings } from '@xsolla/xui-icons';
47
+ ### `<Tabs>`
62
48
 
63
- export default function TabsWithIcons() {
64
- const [activeTab, setActiveTab] = React.useState('home');
49
+ | Prop | Type | Default | Description |
50
+ | --- | --- | --- | --- |
51
+ | `tabs` | `TabItemType[]` | — | **Required.** Tab definitions. |
52
+ | `activeTabId` | `string` | — | Currently active tab. |
53
+ | `onChange` | `(id: string) => void` | — | Called when tab selection changes. |
54
+ | `size` | `"xl" \| "lg" \| "md" \| "sm"` | `"md"` | Size variant. |
55
+ | `variant` | `"line" \| "segmented"` | `"line"` | Visual style. |
56
+ | `alignLeft` | `boolean` | `true` | Line variant only — left vs centre alignment. |
57
+ | `stretched` | `boolean` | `false` | Segmented variant only — distribute items at equal width. |
58
+ | `activateOnFocus` | `boolean` | `true` | Activate the focused tab automatically; otherwise require Enter/Space. |
59
+ | `id` | `string` | — | Root id used to derive `*-tablist`, `*-tab-{id}`, and `*-tabpanel-{id}` ids. |
60
+ | `aria-label` | `string` | — | Accessible label for the tab list. |
61
+ | `aria-labelledby` | `string` | — | ID of an element labelling the tab list. |
62
+ | `testID` | `string` | — | Test identifier. |
63
+
64
+ Inherits `ThemeOverrideProps` (`themeMode`, `themeProductContext`).
65
+
66
+ ### `<TabPanel>`
65
67
 
66
- const tabs = [
67
- { id: 'home', label: 'Home', icon: <Home size={16} /> },
68
- { id: 'profile', label: 'Profile', icon: <User size={16} /> },
69
- { id: 'settings', label: 'Settings', icon: <Settings size={16} /> },
70
- ];
68
+ | Prop | Type | Default | Description |
69
+ | --- | --- | --- | --- |
70
+ | `id` | `string` | | **Required.** Matches the corresponding `tab.id`. |
71
+ | `tabsId` | `string` | | **Required.** Parent `Tabs` id. |
72
+ | `hidden` | `boolean` | `false` | Whether the panel is hidden. |
73
+ | `children` | `ReactNode` | — | **Required.** Panel content. |
74
+ | `aria-label` | `string` | — | Accessible label for the panel. |
75
+ | `testID` | `string` | — | Test identifier. |
71
76
 
72
- return (
73
- <Tabs
74
- tabs={tabs}
75
- activeTabId={activeTab}
76
- onChange={setActiveTab}
77
- />
78
- );
77
+ ### `TabItemType`
78
+
79
+ ```typescript
80
+ interface TabItemType {
81
+ id: string;
82
+ label: string;
83
+ icon?: React.ReactNode;
84
+ counter?: string | number;
85
+ counterPalette?: "brand" | "tertiary" | "default"; // line variant only; default "brand"
86
+ badge?: boolean | string | number;
87
+ disabled?: boolean;
88
+ "aria-label"?: string;
79
89
  }
80
90
  ```
81
91
 
82
- ### Tabs with Counter
92
+ ### Exported types
83
93
 
84
- ```tsx
85
- import * as React from 'react';
86
- import { Tabs } from '@xsolla/xui-tabs';
94
+ | Type | Description |
95
+ | --- | --- |
96
+ | `TabsProps` | Props for `<Tabs>`. |
97
+ | `TabPanelProps` | Props for `<TabPanel>`. |
98
+ | `TabItemType` | Tab definition. |
87
99
 
88
- export default function TabsWithCounter() {
89
- const [activeTab, setActiveTab] = React.useState('inbox');
100
+ ### Keyboard navigation
90
101
 
91
- const tabs = [
92
- { id: 'inbox', label: 'Inbox', counter: 12 },
93
- { id: 'sent', label: 'Sent', counter: 5 },
94
- { id: 'drafts', label: 'Drafts', counter: 3 },
95
- { id: 'spam', label: 'Spam', badge: true },
96
- ];
102
+ | Key | Action |
103
+ | --- | --- |
104
+ | Arrow Right / Down | Move to next enabled tab. |
105
+ | Arrow Left / Up | Move to previous enabled tab. |
106
+ | Home | Jump to first enabled tab. |
107
+ | End | Jump to last enabled tab. |
108
+ | Enter / Space | Activate the focused tab when `activateOnFocus={false}`. |
97
109
 
98
- return (
99
- <Tabs
100
- tabs={tabs}
101
- activeTabId={activeTab}
102
- onChange={setActiveTab}
103
- />
104
- );
105
- }
106
- ```
110
+ ## Examples
107
111
 
108
- ### Tabs Sizes
112
+ ### Tabs with icons and counters
109
113
 
110
114
  ```tsx
111
115
  import * as React from 'react';
112
116
  import { Tabs } from '@xsolla/xui-tabs';
117
+ import { Home, User, Settings } from '@xsolla/xui-icons-base';
113
118
 
114
- export default function TabsSizes() {
119
+ export default function IconTabs() {
120
+ const [active, setActive] = React.useState('home');
115
121
  const tabs = [
116
- { id: 'a', label: 'Tab A' },
117
- { id: 'b', label: 'Tab B' },
118
- { id: 'c', label: 'Tab C' },
122
+ { id: 'home', label: 'Home', icon: <Home size={16} /> },
123
+ { id: 'profile', label: 'Profile', icon: <User size={16} />, counter: 5 },
124
+ { id: 'settings', label: 'Settings', icon: <Settings size={16} /> },
119
125
  ];
120
-
121
- return (
122
- <div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
123
- <Tabs tabs={tabs} activeTabId="a" size="sm" />
124
- <Tabs tabs={tabs} activeTabId="a" size="md" />
125
- <Tabs tabs={tabs} activeTabId="a" size="lg" />
126
- <Tabs tabs={tabs} activeTabId="a" size="xl" />
127
- </div>
128
- );
126
+ return <Tabs tabs={tabs} activeTabId={active} onChange={setActive} />;
129
127
  }
130
128
  ```
131
129
 
132
- ### Segmented Variant
133
-
134
- A button-group style segmented control with a sliding active indicator animation.
130
+ ### Segmented variant
135
131
 
136
132
  ```tsx
137
133
  import * as React from 'react';
138
134
  import { Tabs } from '@xsolla/xui-tabs';
139
135
 
140
136
  export default function SegmentedTabs() {
141
- const [activeTab, setActiveTab] = React.useState('daily');
142
-
137
+ const [active, setActive] = React.useState('daily');
143
138
  const tabs = [
144
139
  { id: 'daily', label: 'Daily' },
145
140
  { id: 'weekly', label: 'Weekly' },
146
141
  { id: 'monthly', label: 'Monthly' },
147
142
  ];
148
-
149
- return (
150
- <Tabs
151
- tabs={tabs}
152
- activeTabId={activeTab}
153
- onChange={setActiveTab}
154
- variant="segmented"
155
- />
156
- );
143
+ return <Tabs tabs={tabs} activeTabId={active} onChange={setActive} variant="segmented" />;
157
144
  }
158
145
  ```
159
146
 
160
- ### Stretched Segmented Tabs
161
-
162
- For the segmented variant, the `stretched` prop distributes items at equal width across the container. Has no effect on the line variant — line tabs always span the container width and rely on `alignLeft` for positioning.
147
+ ### Stretched segmented
163
148
 
164
149
  ```tsx
165
150
  import * as React from 'react';
166
151
  import { Tabs } from '@xsolla/xui-tabs';
167
152
 
168
153
  export default function StretchedTabs() {
169
- const [activeTab, setActiveTab] = React.useState('tab1');
170
-
154
+ const [active, setActive] = React.useState('login');
171
155
  const tabs = [
172
- { id: 'tab1', label: 'Log in' },
173
- { id: 'tab2', label: 'Sign up' },
156
+ { id: 'login', label: 'Log in' },
157
+ { id: 'signup', label: 'Sign up' },
174
158
  ];
175
-
176
- return (
177
- <Tabs
178
- tabs={tabs}
179
- activeTabId={activeTab}
180
- onChange={setActiveTab}
181
- variant="segmented"
182
- stretched
183
- />
184
- );
159
+ return <Tabs tabs={tabs} activeTabId={active} onChange={setActive} variant="segmented" stretched />;
185
160
  }
186
161
  ```
187
162
 
188
- ## Anatomy
189
-
190
- Import the components and compose them:
191
-
192
- ```jsx
193
- import { Tabs, TabPanel } from '@xsolla/xui-tabs';
194
-
195
- // Tab list
196
- <Tabs
197
- id="my-tabs" // Unique ID for ARIA
198
- tabs={tabItems} // Array of tab definitions
199
- activeTabId={activeId} // Currently active tab
200
- onChange={handleChange} // Tab selection handler
201
- size="md" // Size variant
202
- variant="line" // Visual variant: "line" | "segmented"
203
- stretched={false} // Equal-width items (segmented variant only)
204
- alignLeft={true} // Left-anchor vs centre (line variant only)
205
- activateOnFocus={true} // Auto-activate on focus
206
- />
207
-
208
- // Tab panels
209
- <TabPanel
210
- id="tab-id" // Matches tab.id
211
- tabsId="my-tabs" // Parent Tabs id
212
- hidden={!isActive} // Visibility control
213
- >
214
- Panel content
215
- </TabPanel>
216
- ```
217
-
218
- ## Examples
219
-
220
- ### Disabled Tab
163
+ ### Disabled tab
221
164
 
222
165
  ```tsx
223
166
  import * as React from 'react';
224
167
  import { Tabs } from '@xsolla/xui-tabs';
225
168
 
226
169
  export default function DisabledTab() {
227
- const [activeTab, setActiveTab] = React.useState('active');
228
-
170
+ const [active, setActive] = React.useState('a');
229
171
  const tabs = [
230
- { id: 'active', label: 'Active Tab' },
231
- { id: 'disabled', label: 'Disabled Tab', disabled: true },
232
- { id: 'another', label: 'Another Tab' },
172
+ { id: 'a', label: 'Active' },
173
+ { id: 'b', label: 'Disabled', disabled: true },
174
+ { id: 'c', label: 'Another' },
233
175
  ];
234
-
235
- return (
236
- <Tabs
237
- tabs={tabs}
238
- activeTabId={activeTab}
239
- onChange={setActiveTab}
240
- />
241
- );
176
+ return <Tabs tabs={tabs} activeTabId={active} onChange={setActive} />;
242
177
  }
243
178
  ```
244
179
 
245
- ### Manual Activation
180
+ ### Manual activation
246
181
 
247
182
  ```tsx
248
183
  import * as React from 'react';
249
184
  import { Tabs } from '@xsolla/xui-tabs';
250
185
 
251
186
  export default function ManualActivation() {
252
- const [activeTab, setActiveTab] = React.useState('tab1');
253
-
187
+ const [active, setActive] = React.useState('one');
254
188
  const tabs = [
255
- { id: 'tab1', label: 'First' },
256
- { id: 'tab2', label: 'Second' },
257
- { id: 'tab3', label: 'Third' },
189
+ { id: 'one', label: 'One' },
190
+ { id: 'two', label: 'Two' },
191
+ { id: 'three', label: 'Three' },
258
192
  ];
259
-
260
193
  return (
261
- <div>
262
- <p>Use Arrow keys to navigate, Enter/Space to activate</p>
263
- <Tabs
264
- tabs={tabs}
265
- activeTabId={activeTab}
266
- onChange={setActiveTab}
267
- activateOnFocus={false}
268
- />
269
- </div>
194
+ <Tabs
195
+ tabs={tabs}
196
+ activeTabId={active}
197
+ onChange={setActive}
198
+ activateOnFocus={false}
199
+ />
270
200
  );
271
201
  }
272
202
  ```
273
203
 
274
- ### Full Example with Content
204
+ ### Full panels
275
205
 
276
206
  ```tsx
277
207
  import * as React from 'react';
278
208
  import { Tabs, TabPanel } from '@xsolla/xui-tabs';
279
- import { Settings } from '@xsolla/xui-icons';
280
- import { LayoutDashboard, BarChart3, FileText } from '@xsolla/xui-icons-base';
281
-
282
- export default function FullTabsExample() {
283
- const [activeTab, setActiveTab] = React.useState('dashboard');
284
209
 
210
+ export default function FullTabs() {
211
+ const [active, setActive] = React.useState('dashboard');
285
212
  const tabs = [
286
- { id: 'dashboard', label: 'Dashboard', icon: <LayoutDashboard size={16} /> },
287
- { id: 'analytics', label: 'Analytics', icon: <BarChart3 size={16} />, counter: 5 },
288
- { id: 'reports', label: 'Reports', icon: <FileText size={16} /> },
289
- { id: 'settings', label: 'Settings', icon: <Settings size={16} /> },
213
+ { id: 'dashboard', label: 'Dashboard' },
214
+ { id: 'analytics', label: 'Analytics', counter: 5 },
215
+ { id: 'reports', label: 'Reports' },
290
216
  ];
291
-
292
217
  return (
293
- <div style={{ width: '100%' }}>
294
- <Tabs
295
- id="main-tabs"
296
- tabs={tabs}
297
- activeTabId={activeTab}
298
- onChange={setActiveTab}
299
- size="md"
300
- />
301
-
218
+ <div>
219
+ <Tabs id="main" tabs={tabs} activeTabId={active} onChange={setActive} />
302
220
  <div style={{ padding: 24, border: '1px solid #eee', borderTop: 'none' }}>
303
- <TabPanel id="dashboard" tabsId="main-tabs" hidden={activeTab !== 'dashboard'}>
304
- <h3>Dashboard</h3>
305
- <p>Welcome to your dashboard overview.</p>
306
- </TabPanel>
307
-
308
- <TabPanel id="analytics" tabsId="main-tabs" hidden={activeTab !== 'analytics'}>
309
- <h3>Analytics</h3>
310
- <p>View your analytics and metrics here.</p>
311
- </TabPanel>
312
-
313
- <TabPanel id="reports" tabsId="main-tabs" hidden={activeTab !== 'reports'}>
314
- <h3>Reports</h3>
315
- <p>Generate and view reports.</p>
316
- </TabPanel>
317
-
318
- <TabPanel id="settings" tabsId="main-tabs" hidden={activeTab !== 'settings'}>
319
- <h3>Settings</h3>
320
- <p>Configure your preferences.</p>
321
- </TabPanel>
221
+ <TabPanel id="dashboard" tabsId="main" hidden={active !== 'dashboard'}>Dashboard content.</TabPanel>
222
+ <TabPanel id="analytics" tabsId="main" hidden={active !== 'analytics'}>Analytics content.</TabPanel>
223
+ <TabPanel id="reports" tabsId="main" hidden={active !== 'reports'}>Reports content.</TabPanel>
322
224
  </div>
323
225
  </div>
324
226
  );
325
227
  }
326
228
  ```
327
229
 
328
- ## API Reference
329
-
330
- ### Tabs
331
-
332
- The tab list component.
333
-
334
- **Tabs Props:**
335
-
336
- | Prop | Type | Default | Description |
337
- | :--- | :--- | :------ | :---------- |
338
- | tabs | `TabItemType[]` | - | **Required.** Array of tab definitions. |
339
- | activeTabId | `string` | - | Currently active tab ID. |
340
- | onChange | `(id: string) => void` | - | Callback when tab selection changes. |
341
- | size | `"xl" \| "lg" \| "md" \| "sm"` | `"md"` | Size of the tabs. |
342
- | variant | `"line" \| "segmented"` | `"line"` | Visual variant. Line shows underlined tabs; segmented shows button-group style. |
343
- | stretched | `boolean` | `false` | Segmented variant only: distribute items at equal width. No effect on line variant. |
344
- | alignLeft | `boolean` | `true` | Left vs center alignment (line variant only). |
345
- | activateOnFocus | `boolean` | `true` | Auto-activate tab on focus. |
346
- | id | `string` | - | Unique ID for ARIA association. |
347
- | aria-label | `string` | - | Accessible label for tab list. |
348
- | aria-labelledby | `string` | - | ID of labeling element. |
349
- | testID | `string` | - | Test identifier. |
350
-
351
- **TabItemType:**
352
-
353
- ```typescript
354
- interface TabItemType {
355
- id: string; // Unique tab identifier
356
- label: string; // Display label
357
- icon?: ReactNode; // Optional icon
358
- counter?: string | number; // Optional counter badge
359
- counterPalette?: "brand" | "tertiary" | "default"; // Counter colour (line variant). Default "brand".
360
- badge?: boolean | string | number; // Optional badge
361
- disabled?: boolean; // Whether tab is disabled
362
- "aria-label"?: string; // Custom accessible label
363
- }
364
- ```
365
-
366
- ---
367
-
368
- ### TabPanel
369
-
370
- Container for tab content.
371
-
372
- **TabPanel Props:**
373
-
374
- | Prop | Type | Default | Description |
375
- | :--- | :--- | :------ | :---------- |
376
- | id | `string` | - | **Required.** Matches corresponding tab.id. |
377
- | tabsId | `string` | - | **Required.** Parent Tabs component id. |
378
- | hidden | `boolean` | - | Whether panel is hidden. |
379
- | children | `ReactNode` | - | Panel content. |
380
- | aria-label | `string` | - | Accessible label. |
381
- | testID | `string` | - | Test identifier. |
382
-
383
- ## Keyboard Navigation
384
-
385
- | Key | Action |
386
- | :-- | :----- |
387
- | Arrow Right/Down | Move to next enabled tab |
388
- | Arrow Left/Up | Move to previous enabled tab |
389
- | Home | Jump to first enabled tab |
390
- | End | Jump to last enabled tab |
391
- | Enter/Space | Activate tab (when activateOnFocus is false) |
392
-
393
- ## Theming
394
-
395
- Tabs uses the design system theme for colors:
396
-
397
- ### Line Variant
398
-
399
- ```typescript
400
- // Colors accessed via theme
401
- theme.colors.content.primary // Tab text (default, hover, active); counterPalette="default"
402
- theme.colors.content.tertiary // counterPalette="tertiary"
403
- theme.colors.control.text.disable // Disabled tab text and disabled counter
404
- theme.colors.content.brand.primary // counterPalette="brand" (default)
405
- theme.colors.border.primary // Active tab indicator (bottom border)
406
- theme.colors.border.secondary // Container bottom border
407
- theme.colors.overlay.mono // Hover background
408
- ```
409
-
410
- ### Segmented Variant
411
-
412
- ```typescript
413
- // Colors accessed via theme
414
- theme.colors.control.segmented.bg // Container background
415
- theme.colors.control.segmented.bgHover // Tab hover background
416
- theme.colors.control.segmented.bgActive // Active tab indicator background
417
- theme.colors.control.segmented.text // Tab text
418
- theme.colors.control.segmented.textDisable // Disabled tab text
419
- ```
420
-
421
230
  ## Accessibility
422
231
 
423
- - Implements WAI-ARIA tablist pattern
424
- - Uses `role="tablist"`, `role="tab"`, `role="tabpanel"`
425
- - `aria-selected` indicates active tab
426
- - `aria-controls` links tabs to panels
427
- - `aria-labelledby` links panels to tabs
428
- - Full keyboard navigation support
429
- - Focus management follows ARIA best practices
430
- - Segmented variant sliding indicator is marked with `aria-hidden`
232
+ - Implements the WAI-ARIA tablist pattern with `role="tablist"`, `role="tab"`, and `role="tabpanel"`.
233
+ - `aria-selected`, `aria-controls`, and `aria-labelledby` link tabs and panels.
234
+ - Roving `tabIndex` moves focus to the active tab; arrow keys cycle through enabled tabs.
235
+ - The segmented variant's sliding indicator is marked `aria-hidden`.
431
236
 
432
237
  ## Platform Support
433
238
 
434
- Both variants support web and React Native platforms:
435
-
436
- - **Web**: Segmented variant includes smooth sliding animation for the active indicator
437
- - **React Native**: Falls back to static background without animation
239
+ - **Web** segmented variant uses a smooth sliding active indicator backed by `ResizeObserver`.
240
+ - **React Native** — segmented variant falls back to per-tab background highlighting (no animation).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xsolla/xui-tabs",
3
- "version": "0.150.0",
3
+ "version": "0.152.0",
4
4
  "main": "./web/index.js",
5
5
  "module": "./web/index.mjs",
6
6
  "types": "./web/index.d.ts",
@@ -12,9 +12,9 @@
12
12
  "test:watch": "vitest"
13
13
  },
14
14
  "dependencies": {
15
- "@xsolla/xui-badge": "0.150.0",
16
- "@xsolla/xui-core": "0.150.0",
17
- "@xsolla/xui-primitives-core": "0.150.0"
15
+ "@xsolla/xui-badge": "0.152.0",
16
+ "@xsolla/xui-core": "0.152.0",
17
+ "@xsolla/xui-primitives-core": "0.152.0"
18
18
  },
19
19
  "peerDependencies": {
20
20
  "react": ">=16.8.0",