@xsolla/xui-tabs 0.141.1 → 0.147.1-pr265.1777528265
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.
- package/README.md +437 -0
- package/native/index.d.mts +3 -1
- package/native/index.d.ts +3 -1
- package/native/index.js +29 -16
- package/native/index.js.map +1 -1
- package/native/index.mjs +29 -16
- package/native/index.mjs.map +1 -1
- package/package.json +4 -4
- package/web/index.d.mts +3 -1
- package/web/index.d.ts +3 -1
- package/web/index.js +29 -16
- package/web/index.js.map +1 -1
- package/web/index.mjs +29 -16
- package/web/index.mjs.map +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# Tabs
|
|
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
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @xsolla/xui-tabs
|
|
13
|
+
# or
|
|
14
|
+
yarn add @xsolla/xui-tabs
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Demo
|
|
18
|
+
|
|
19
|
+
### Basic Tabs
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import * as React from 'react';
|
|
23
|
+
import { Tabs, TabPanel } from '@xsolla/xui-tabs';
|
|
24
|
+
|
|
25
|
+
export default function BasicTabs() {
|
|
26
|
+
const [activeTab, setActiveTab] = React.useState('tab1');
|
|
27
|
+
|
|
28
|
+
const tabs = [
|
|
29
|
+
{ id: 'tab1', label: 'Overview' },
|
|
30
|
+
{ id: 'tab2', label: 'Features' },
|
|
31
|
+
{ id: 'tab3', label: 'Pricing' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<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>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Tabs with Icons
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import * as React from 'react';
|
|
60
|
+
import { Tabs } from '@xsolla/xui-tabs';
|
|
61
|
+
import { Home, User, Settings } from '@xsolla/xui-icons';
|
|
62
|
+
|
|
63
|
+
export default function TabsWithIcons() {
|
|
64
|
+
const [activeTab, setActiveTab] = React.useState('home');
|
|
65
|
+
|
|
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
|
+
];
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<Tabs
|
|
74
|
+
tabs={tabs}
|
|
75
|
+
activeTabId={activeTab}
|
|
76
|
+
onChange={setActiveTab}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Tabs with Counter
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import * as React from 'react';
|
|
86
|
+
import { Tabs } from '@xsolla/xui-tabs';
|
|
87
|
+
|
|
88
|
+
export default function TabsWithCounter() {
|
|
89
|
+
const [activeTab, setActiveTab] = React.useState('inbox');
|
|
90
|
+
|
|
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
|
+
];
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Tabs
|
|
100
|
+
tabs={tabs}
|
|
101
|
+
activeTabId={activeTab}
|
|
102
|
+
onChange={setActiveTab}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Tabs Sizes
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import * as React from 'react';
|
|
112
|
+
import { Tabs } from '@xsolla/xui-tabs';
|
|
113
|
+
|
|
114
|
+
export default function TabsSizes() {
|
|
115
|
+
const tabs = [
|
|
116
|
+
{ id: 'a', label: 'Tab A' },
|
|
117
|
+
{ id: 'b', label: 'Tab B' },
|
|
118
|
+
{ id: 'c', label: 'Tab C' },
|
|
119
|
+
];
|
|
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
|
+
);
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Segmented Variant
|
|
133
|
+
|
|
134
|
+
A button-group style segmented control with a sliding active indicator animation.
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import * as React from 'react';
|
|
138
|
+
import { Tabs } from '@xsolla/xui-tabs';
|
|
139
|
+
|
|
140
|
+
export default function SegmentedTabs() {
|
|
141
|
+
const [activeTab, setActiveTab] = React.useState('daily');
|
|
142
|
+
|
|
143
|
+
const tabs = [
|
|
144
|
+
{ id: 'daily', label: 'Daily' },
|
|
145
|
+
{ id: 'weekly', label: 'Weekly' },
|
|
146
|
+
{ id: 'monthly', label: 'Monthly' },
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<Tabs
|
|
151
|
+
tabs={tabs}
|
|
152
|
+
activeTabId={activeTab}
|
|
153
|
+
onChange={setActiveTab}
|
|
154
|
+
variant="segmented"
|
|
155
|
+
/>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
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.
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
import * as React from 'react';
|
|
166
|
+
import { Tabs } from '@xsolla/xui-tabs';
|
|
167
|
+
|
|
168
|
+
export default function StretchedTabs() {
|
|
169
|
+
const [activeTab, setActiveTab] = React.useState('tab1');
|
|
170
|
+
|
|
171
|
+
const tabs = [
|
|
172
|
+
{ id: 'tab1', label: 'Log in' },
|
|
173
|
+
{ id: 'tab2', label: 'Sign up' },
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<Tabs
|
|
178
|
+
tabs={tabs}
|
|
179
|
+
activeTabId={activeTab}
|
|
180
|
+
onChange={setActiveTab}
|
|
181
|
+
variant="segmented"
|
|
182
|
+
stretched
|
|
183
|
+
/>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
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
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
import * as React from 'react';
|
|
224
|
+
import { Tabs } from '@xsolla/xui-tabs';
|
|
225
|
+
|
|
226
|
+
export default function DisabledTab() {
|
|
227
|
+
const [activeTab, setActiveTab] = React.useState('active');
|
|
228
|
+
|
|
229
|
+
const tabs = [
|
|
230
|
+
{ id: 'active', label: 'Active Tab' },
|
|
231
|
+
{ id: 'disabled', label: 'Disabled Tab', disabled: true },
|
|
232
|
+
{ id: 'another', label: 'Another Tab' },
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<Tabs
|
|
237
|
+
tabs={tabs}
|
|
238
|
+
activeTabId={activeTab}
|
|
239
|
+
onChange={setActiveTab}
|
|
240
|
+
/>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Manual Activation
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
import * as React from 'react';
|
|
249
|
+
import { Tabs } from '@xsolla/xui-tabs';
|
|
250
|
+
|
|
251
|
+
export default function ManualActivation() {
|
|
252
|
+
const [activeTab, setActiveTab] = React.useState('tab1');
|
|
253
|
+
|
|
254
|
+
const tabs = [
|
|
255
|
+
{ id: 'tab1', label: 'First' },
|
|
256
|
+
{ id: 'tab2', label: 'Second' },
|
|
257
|
+
{ id: 'tab3', label: 'Third' },
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
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>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Full Example with Content
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
import * as React from 'react';
|
|
278
|
+
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
|
+
|
|
285
|
+
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} /> },
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
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
|
+
|
|
302
|
+
<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>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
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
|
+
## Accessibility
|
|
422
|
+
|
|
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`
|
|
431
|
+
|
|
432
|
+
## Platform Support
|
|
433
|
+
|
|
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
|
package/native/index.d.mts
CHANGED
|
@@ -10,6 +10,8 @@ interface TabItemType {
|
|
|
10
10
|
icon?: React.ReactNode;
|
|
11
11
|
/** Optional counter to display after the label */
|
|
12
12
|
counter?: string | number;
|
|
13
|
+
/** Counter colour palette (line variant only). Defaults to "brand". */
|
|
14
|
+
counterPalette?: "brand" | "tertiary" | "default";
|
|
13
15
|
/** Optional badge to display */
|
|
14
16
|
badge?: boolean | string | number;
|
|
15
17
|
/** Whether the tab is disabled */
|
|
@@ -30,7 +32,7 @@ interface TabsProps extends ThemeOverrideProps {
|
|
|
30
32
|
variant?: "line" | "segmented";
|
|
31
33
|
/** Whether to align tabs to the left (only for line variant) */
|
|
32
34
|
alignLeft?: boolean;
|
|
33
|
-
/**
|
|
35
|
+
/** Segmented variant only: distribute items at equal width across the container */
|
|
34
36
|
stretched?: boolean;
|
|
35
37
|
/** Accessible label for the tab list */
|
|
36
38
|
"aria-label"?: string;
|
package/native/index.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ interface TabItemType {
|
|
|
10
10
|
icon?: React.ReactNode;
|
|
11
11
|
/** Optional counter to display after the label */
|
|
12
12
|
counter?: string | number;
|
|
13
|
+
/** Counter colour palette (line variant only). Defaults to "brand". */
|
|
14
|
+
counterPalette?: "brand" | "tertiary" | "default";
|
|
13
15
|
/** Optional badge to display */
|
|
14
16
|
badge?: boolean | string | number;
|
|
15
17
|
/** Whether the tab is disabled */
|
|
@@ -30,7 +32,7 @@ interface TabsProps extends ThemeOverrideProps {
|
|
|
30
32
|
variant?: "line" | "segmented";
|
|
31
33
|
/** Whether to align tabs to the left (only for line variant) */
|
|
32
34
|
alignLeft?: boolean;
|
|
33
|
-
/**
|
|
35
|
+
/** Segmented variant only: distribute items at equal width across the container */
|
|
34
36
|
stretched?: boolean;
|
|
35
37
|
/** Accessible label for the tab list */
|
|
36
38
|
"aria-label"?: string;
|
package/native/index.js
CHANGED
|
@@ -331,18 +331,26 @@ var Tabs = ({
|
|
|
331
331
|
const [indicatorStyle, setIndicatorStyle] = (0, import_react2.useState)({ left: 0, width: 0, initialized: false });
|
|
332
332
|
(0, import_react2.useEffect)(() => {
|
|
333
333
|
if (!isSegmented || !isWeb) return;
|
|
334
|
-
const
|
|
335
|
-
|
|
334
|
+
const updateIndicator = () => {
|
|
335
|
+
const activeIndex = tabs.findIndex((tab) => tab.id === activeTabId);
|
|
336
|
+
const activeTabEl = tabRefs.current[activeIndex];
|
|
337
|
+
const containerEl2 = containerRef.current;
|
|
338
|
+
if (activeTabEl && containerEl2) {
|
|
339
|
+
const containerRect = containerEl2.getBoundingClientRect();
|
|
340
|
+
const tabRect = activeTabEl.getBoundingClientRect();
|
|
341
|
+
setIndicatorStyle({
|
|
342
|
+
left: tabRect.left - containerRect.left,
|
|
343
|
+
width: tabRect.width,
|
|
344
|
+
initialized: true
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
updateIndicator();
|
|
336
349
|
const containerEl = containerRef.current;
|
|
337
|
-
if (
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
left: tabRect.left - containerRect.left,
|
|
342
|
-
width: tabRect.width,
|
|
343
|
-
initialized: true
|
|
344
|
-
});
|
|
345
|
-
}
|
|
350
|
+
if (!containerEl || typeof ResizeObserver === "undefined") return;
|
|
351
|
+
const observer = new ResizeObserver(updateIndicator);
|
|
352
|
+
observer.observe(containerEl);
|
|
353
|
+
return () => observer.disconnect();
|
|
346
354
|
}, [activeTabId, tabs, isSegmented]);
|
|
347
355
|
const enabledIndices = tabs.map((tab, index) => !tab.disabled ? index : -1).filter((i) => i !== -1);
|
|
348
356
|
const focusTab = (0, import_react2.useCallback)((index) => {
|
|
@@ -565,8 +573,7 @@ var Tabs = ({
|
|
|
565
573
|
flexDirection: "row",
|
|
566
574
|
alignItems: "flex-end",
|
|
567
575
|
justifyContent: alignLeft ? "flex-start" : "center",
|
|
568
|
-
width:
|
|
569
|
-
alignSelf: !stretched && !isWeb ? "flex-start" : void 0,
|
|
576
|
+
width: "100%",
|
|
570
577
|
height: lineStyles.height,
|
|
571
578
|
borderBottomWidth: 1,
|
|
572
579
|
borderBottomColor: theme.colors.border.secondary,
|
|
@@ -584,7 +591,9 @@ var Tabs = ({
|
|
|
584
591
|
const handleFocus = () => {
|
|
585
592
|
setFocusedIndex(index);
|
|
586
593
|
};
|
|
587
|
-
const textColor = isDisabled ? theme.colors.
|
|
594
|
+
const textColor = isDisabled ? theme.colors.control.text.disable : theme.colors.content.primary;
|
|
595
|
+
const counterPaletteColor = tab.counterPalette === "default" ? theme.colors.content.primary : tab.counterPalette === "tertiary" ? theme.colors.content.tertiary : theme.colors.content.brand.primary;
|
|
596
|
+
const counterColor = isDisabled ? theme.colors.control.text.disable : counterPaletteColor;
|
|
588
597
|
const borderBottomColor = isActive ? theme.colors.border.primary : "transparent";
|
|
589
598
|
const borderBottomWidth = isActive ? 2 : 0;
|
|
590
599
|
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
@@ -612,6 +621,8 @@ var Tabs = ({
|
|
|
612
621
|
justifyContent: "center",
|
|
613
622
|
gap: lineStyles.gap,
|
|
614
623
|
position: "relative",
|
|
624
|
+
borderTopLeftRadius: 2,
|
|
625
|
+
borderTopRightRadius: 2,
|
|
615
626
|
borderBottomWidth,
|
|
616
627
|
borderBottomColor,
|
|
617
628
|
borderStyle: isActive ? "solid" : "none",
|
|
@@ -632,15 +643,17 @@ var Tabs = ({
|
|
|
632
643
|
{
|
|
633
644
|
color: textColor,
|
|
634
645
|
fontSize: lineStyles.fontSize,
|
|
635
|
-
|
|
646
|
+
lineHeight: lineStyles.lineHeight,
|
|
647
|
+
fontWeight: "500",
|
|
636
648
|
children: tab.label
|
|
637
649
|
}
|
|
638
650
|
),
|
|
639
651
|
tab.counter !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Box, { marginLeft: 2, "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
640
652
|
Text,
|
|
641
653
|
{
|
|
642
|
-
color:
|
|
654
|
+
color: counterColor,
|
|
643
655
|
fontSize: lineStyles.fontSize,
|
|
656
|
+
lineHeight: lineStyles.lineHeight,
|
|
644
657
|
fontWeight: "500",
|
|
645
658
|
"aria-label": `${tab.counter} items`,
|
|
646
659
|
children: tab.counter
|