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