@popsure/dirty-swan 0.66.19 → 0.66.21
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/dist/cjs/index.js +352 -306
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/lib/components/table/Table.d.ts +2 -1
- package/dist/cjs/lib/components/table/Table.stories.d.ts +9 -1
- package/dist/cjs/lib/components/table/components/TableContents/TableContents.d.ts +2 -1
- package/dist/cjs/lib/components/tabs/Tabs.d.ts +12 -0
- package/dist/cjs/lib/components/tabs/Tabs.stories.d.ts +45 -0
- package/dist/cjs/lib/index.d.ts +3 -2
- package/dist/esm/components/icon/icons/CircleSelected.js +2 -2
- package/dist/esm/components/icon/icons/CircleSelected.js.map +1 -1
- package/dist/esm/components/icon/icons/CircleUnselected.js +2 -2
- package/dist/esm/components/icon/icons/CircleUnselected.js.map +1 -1
- package/dist/esm/components/icon/icons.stories.js +1 -1
- package/dist/esm/components/icon/index.stories.js +1 -1
- package/dist/esm/components/table/Table.js +27 -10
- package/dist/esm/components/table/Table.js.map +1 -1
- package/dist/esm/components/table/Table.stories.js +86 -1
- package/dist/esm/components/table/Table.stories.js.map +1 -1
- package/dist/esm/components/table/Table.test.js +3 -0
- package/dist/esm/components/table/Table.test.js.map +1 -1
- package/dist/esm/components/table/components/TableCell/ButtonCell/ButtonCell.js +9 -8
- package/dist/esm/components/table/components/TableCell/ButtonCell/ButtonCell.js.map +1 -1
- package/dist/esm/components/table/components/TableCell/ButtonCell/ButtonCell.stories.js +4 -2
- package/dist/esm/components/table/components/TableCell/ButtonCell/ButtonCell.stories.js.map +1 -1
- package/dist/esm/components/table/components/TableCell/TableCell.js +2 -0
- package/dist/esm/components/table/components/TableCell/TableCell.js.map +1 -1
- package/dist/esm/components/table/components/TableCell/TableCell.test.js +2 -0
- package/dist/esm/components/table/components/TableCell/TableCell.test.js.map +1 -1
- package/dist/esm/components/table/components/TableContents/TableContents.js +5 -3
- package/dist/esm/components/table/components/TableContents/TableContents.js.map +1 -1
- package/dist/esm/components/table/components/TableContents/TableContents.test.js +15 -0
- package/dist/esm/components/table/components/TableContents/TableContents.test.js.map +1 -1
- package/dist/esm/components/table/components/TableSection/TableSection.js +2 -0
- package/dist/esm/components/table/components/TableSection/TableSection.js.map +1 -1
- package/dist/esm/components/table/components/TableSection/TableSection.test.js +2 -0
- package/dist/esm/components/table/components/TableSection/TableSection.test.js.map +1 -1
- package/dist/esm/components/tabs/Tabs.js +40 -0
- package/dist/esm/components/tabs/Tabs.js.map +1 -0
- package/dist/esm/components/tabs/Tabs.stories.js +77 -0
- package/dist/esm/components/tabs/Tabs.stories.js.map +1 -0
- package/dist/esm/{index-D41W71Hh.js → index-D7G3EuxU.js} +5 -5
- package/dist/esm/{index-D41W71Hh.js.map → index-D7G3EuxU.js.map} +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/components/table/Table.d.ts +2 -1
- package/dist/esm/lib/components/table/Table.stories.d.ts +9 -1
- package/dist/esm/lib/components/table/components/TableContents/TableContents.d.ts +2 -1
- package/dist/esm/lib/components/tabs/Tabs.d.ts +12 -0
- package/dist/esm/lib/components/tabs/Tabs.stories.d.ts +45 -0
- package/dist/esm/lib/index.d.ts +3 -2
- package/dist/esm/util/images/index.stories.js +2 -0
- package/dist/esm/util/images/index.stories.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/components/table/Table.stories.tsx +116 -0
- package/src/lib/components/table/Table.tsx +61 -24
- package/src/lib/components/table/components/TableCell/ButtonCell/ButtonCell.module.scss +27 -21
- package/src/lib/components/table/components/TableCell/ButtonCell/ButtonCell.tsx +19 -16
- package/src/lib/components/table/components/TableContents/TableContents.test.tsx +21 -0
- package/src/lib/components/table/components/TableContents/TableContents.tsx +3 -1
- package/src/lib/components/tabs/Tabs.module.scss +26 -0
- package/src/lib/components/tabs/Tabs.stories.tsx +94 -0
- package/src/lib/components/tabs/Tabs.tsx +65 -0
- package/src/lib/index.tsx +4 -0
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
TableCellData,
|
|
21
21
|
TableData,
|
|
22
22
|
} from './types';
|
|
23
|
+
import { Tabs, TabItem } from '../tabs/Tabs';
|
|
23
24
|
|
|
24
25
|
export type { TableData } from './types';
|
|
25
26
|
|
|
@@ -49,6 +50,7 @@ export interface TableProps {
|
|
|
49
50
|
hideTableNavigation?: boolean;
|
|
50
51
|
hideStickyHeader?: boolean;
|
|
51
52
|
showSelectedColumn?: boolean;
|
|
53
|
+
mobileNavigationMode?: 'arrows' | 'tabs';
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
const defaultTextOverrides = {
|
|
@@ -77,6 +79,7 @@ const Table = ({
|
|
|
77
79
|
hideTableNavigation = false,
|
|
78
80
|
hideStickyHeader = false,
|
|
79
81
|
showSelectedColumn = false,
|
|
82
|
+
mobileNavigationMode = 'arrows',
|
|
80
83
|
}: TableProps) => {
|
|
81
84
|
const textOverrides = { ...defaultTextOverrides, ...definedTextOverrides };
|
|
82
85
|
const isMobile = useMediaQuery('BELOW_MOBILE');
|
|
@@ -89,6 +92,8 @@ const Table = ({
|
|
|
89
92
|
|
|
90
93
|
useScrollSync(headerRef, containerRef, !isMobile);
|
|
91
94
|
|
|
95
|
+
const useTabs = isMobile && mobileNavigationMode === 'tabs';
|
|
96
|
+
|
|
92
97
|
const { activeSection, navigateTable, setActiveSection } = useTableNavigation(
|
|
93
98
|
{
|
|
94
99
|
enabled: isMobile,
|
|
@@ -97,11 +102,25 @@ const Table = ({
|
|
|
97
102
|
}
|
|
98
103
|
);
|
|
99
104
|
|
|
105
|
+
const headerRow = tableData?.[0]?.rows?.[0];
|
|
100
106
|
const titleCell = {
|
|
101
107
|
text: '',
|
|
102
|
-
...(
|
|
108
|
+
...(headerRow?.[0] || {}),
|
|
103
109
|
};
|
|
104
|
-
const currentActiveSection =
|
|
110
|
+
const currentActiveSection = headerRow?.[activeSection];
|
|
111
|
+
|
|
112
|
+
const mobileTabs: TabItem[] = useTabs
|
|
113
|
+
? headerRow
|
|
114
|
+
?.slice(1)
|
|
115
|
+
.map((cell, index) => {
|
|
116
|
+
let label = '';
|
|
117
|
+
if (!cell.type) label = String(cell.text || '');
|
|
118
|
+
else if (cell.type === 'CTA') label = String(cell.title || '');
|
|
119
|
+
else if (cell.type === 'BUTTON') label = String(cell.buttonCaption || '');
|
|
120
|
+
return { id: String(index), label };
|
|
121
|
+
}) ?? []
|
|
122
|
+
: [];
|
|
123
|
+
|
|
105
124
|
const currentActiveSectionReplacements =
|
|
106
125
|
(currentActiveSection?.cellId &&
|
|
107
126
|
cellReplacements?.[currentActiveSection.cellId]) ||
|
|
@@ -127,7 +146,7 @@ const Table = ({
|
|
|
127
146
|
<div className={classNames(styles.wrapper, 'bg-white')}>
|
|
128
147
|
{isMobile ? (
|
|
129
148
|
<>
|
|
130
|
-
{titleCell?.text && (
|
|
149
|
+
{!useTabs && titleCell?.text && (
|
|
131
150
|
<TableCell
|
|
132
151
|
{...titleCell}
|
|
133
152
|
align="left"
|
|
@@ -138,27 +157,44 @@ const Table = ({
|
|
|
138
157
|
)}
|
|
139
158
|
|
|
140
159
|
{!hideTableNavigation && currentActiveSection && (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
useTabs ? (
|
|
161
|
+
<div
|
|
162
|
+
className={classNames(
|
|
163
|
+
'bg-white',
|
|
164
|
+
styles.stickyHeader
|
|
165
|
+
)}
|
|
166
|
+
style={{ top: `${stickyHeaderTopOffset}px` }}
|
|
167
|
+
>
|
|
168
|
+
<Tabs
|
|
169
|
+
tabs={mobileTabs}
|
|
170
|
+
activeIndex={activeSection - 1}
|
|
171
|
+
onTabChange={(index) => setActiveSection(index)}
|
|
172
|
+
className="mb16"
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
) : (
|
|
176
|
+
<TableControls
|
|
177
|
+
activeSection={activeSection}
|
|
178
|
+
columnsLength={columnsLength}
|
|
179
|
+
navigateTable={navigateTable}
|
|
180
|
+
stickyHeaderTopOffset={stickyHeaderTopOffset}
|
|
181
|
+
>
|
|
182
|
+
<TableCell
|
|
183
|
+
{...(isBaseCell(currentActiveSection)
|
|
184
|
+
? {
|
|
185
|
+
openModal: (body: ReactNode) =>
|
|
186
|
+
handleOpenModal({
|
|
187
|
+
body,
|
|
188
|
+
title: currentActiveSection?.text,
|
|
189
|
+
}),
|
|
190
|
+
}
|
|
191
|
+
: {})}
|
|
192
|
+
{...activeCellProps}
|
|
193
|
+
imageComponent={imageComponent}
|
|
194
|
+
isNavigation
|
|
195
|
+
/>
|
|
196
|
+
</TableControls>
|
|
197
|
+
)
|
|
162
198
|
)}
|
|
163
199
|
</>
|
|
164
200
|
) : (
|
|
@@ -203,6 +239,7 @@ const Table = ({
|
|
|
203
239
|
cellReplacements={cellReplacements}
|
|
204
240
|
imageComponent={imageComponent}
|
|
205
241
|
selectedColumn={showSelectedColumn ? activeSection : undefined}
|
|
242
|
+
showHeader={useTabs}
|
|
206
243
|
/>
|
|
207
244
|
</div>
|
|
208
245
|
|
|
@@ -2,21 +2,28 @@
|
|
|
2
2
|
@use "../../../../../scss/public/colors" as *;
|
|
3
3
|
|
|
4
4
|
.buttonCell {
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: space-between;
|
|
8
|
+
width: calc(100% + 40px);
|
|
9
|
+
margin-left: -20px;
|
|
10
|
+
margin-right: -20px;
|
|
11
|
+
padding: 16px 24px;
|
|
12
|
+
border-radius: 12px;
|
|
13
|
+
border: 1px solid $ds-neutral-300;
|
|
14
|
+
background-color: transparent;
|
|
5
15
|
color: $ds-neutral-900;
|
|
6
|
-
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
transition: border-color 0.3s ease-in-out, background-color 0.3s ease-in-out;
|
|
18
|
+
text-align: left;
|
|
7
19
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
border: none;
|
|
12
|
-
background-color: $ds-neutral-100;
|
|
13
|
-
color: $ds-neutral-900;
|
|
14
|
-
opacity: 1;
|
|
15
|
-
cursor: default;
|
|
20
|
+
&:hover:not(:disabled):not(.selected) {
|
|
21
|
+
background-color: $ds-orange-50;
|
|
22
|
+
}
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
&:focus-visible {
|
|
25
|
+
outline: 1px solid $ds-neutral-900;
|
|
26
|
+
outline-offset: 0;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
@include p-size-mobile {
|
|
@@ -24,15 +31,14 @@
|
|
|
24
31
|
}
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.withPrice {
|
|
32
|
-
height: 75px;
|
|
34
|
+
.selected {
|
|
35
|
+
border: 1px solid $ds-neutral-900;
|
|
36
|
+
background-color: $ds-orange-50;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
.
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
.disabled {
|
|
40
|
+
background-color: $ds-neutral-100;
|
|
41
|
+
cursor: default;
|
|
42
|
+
opacity: 1;
|
|
43
|
+
pointer-events: none;
|
|
38
44
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import classNames from 'classnames';
|
|
2
2
|
|
|
3
3
|
import styles from './ButtonCell.module.scss';
|
|
4
|
-
import {
|
|
4
|
+
import { CircleSelectedIcon, CircleUnselectedIcon } from '../../../../icon';
|
|
5
5
|
import { ReactNode } from 'react';
|
|
6
6
|
|
|
7
7
|
export type ButtonCellProps = {
|
|
@@ -27,29 +27,32 @@ export const ButtonCell = ({
|
|
|
27
27
|
}: ButtonCellProps) => {
|
|
28
28
|
return (
|
|
29
29
|
<div
|
|
30
|
-
className={
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
)
|
|
35
|
-
}
|
|
30
|
+
className={classNames(
|
|
31
|
+
"w100 d-flex fd-column ai-start jc-center gap8",
|
|
32
|
+
className,
|
|
33
|
+
)}
|
|
36
34
|
data-cy={dataCy}
|
|
37
35
|
data-testid={dataTestId}
|
|
38
36
|
>
|
|
39
|
-
<
|
|
40
|
-
className={classNames('w100 wmx5
|
|
37
|
+
<button
|
|
38
|
+
className={classNames('w100 wmx5', styles.buttonCell, {
|
|
41
39
|
[styles.selected]: isSelected,
|
|
42
|
-
[styles.
|
|
43
|
-
[styles.withPrice]: !!price,
|
|
40
|
+
[styles.disabled]: disabled,
|
|
44
41
|
})}
|
|
45
|
-
|
|
46
|
-
type="submit"
|
|
42
|
+
type="button"
|
|
47
43
|
onClick={onClick}
|
|
48
44
|
disabled={disabled}
|
|
49
45
|
>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
<div className="d-flex fd-column ai-start">
|
|
47
|
+
<span className="p-p fw-bold">{buttonCaption}</span>
|
|
48
|
+
{price && <span className="p-p">{price}</span>}
|
|
49
|
+
</div>
|
|
50
|
+
{!disabled && (isSelected ? (
|
|
51
|
+
<CircleSelectedIcon size={16} />
|
|
52
|
+
) : (
|
|
53
|
+
<CircleUnselectedIcon size={16} />
|
|
54
|
+
))}
|
|
55
|
+
</button>
|
|
53
56
|
</div>
|
|
54
57
|
);
|
|
55
58
|
};
|
|
@@ -73,4 +73,25 @@ describe('TableContents', () => {
|
|
|
73
73
|
expect(screen.queryByText('Section 2')).not.toBeInTheDocument();
|
|
74
74
|
expect(screen.queryByText('Item 2.1.1')).not.toBeInTheDocument();
|
|
75
75
|
});
|
|
76
|
+
|
|
77
|
+
it('shows the header row only in the first section when showHeader is set', () => {
|
|
78
|
+
const { container } = render(
|
|
79
|
+
<TableContents showHeader tableData={mockData} title="Table" />
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const tableHeaders = container.querySelectorAll('thead');
|
|
83
|
+
expect(tableHeaders).toHaveLength(2);
|
|
84
|
+
expect(tableHeaders[0]).not.toHaveClass('sr-only');
|
|
85
|
+
expect(tableHeaders[1]).toHaveClass('sr-only');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('keeps every header row hidden when showHeader is not set', () => {
|
|
89
|
+
const { container } = render(
|
|
90
|
+
<TableContents tableData={mockData} title="Table" />
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
container.querySelectorAll('thead').forEach((tableHeader) => {
|
|
94
|
+
expect(tableHeader).toHaveClass('sr-only');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
76
97
|
});
|
|
@@ -25,6 +25,7 @@ export interface TableContentsProps {
|
|
|
25
25
|
cellReplacements?: CellReplacements;
|
|
26
26
|
imageComponent?: (args: any) => JSX.Element;
|
|
27
27
|
selectedColumn?: number;
|
|
28
|
+
showHeader?: boolean;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
const TableContents = ({
|
|
@@ -44,6 +45,7 @@ const TableContents = ({
|
|
|
44
45
|
cellReplacements,
|
|
45
46
|
imageComponent,
|
|
46
47
|
selectedColumn,
|
|
48
|
+
showHeader = false,
|
|
47
49
|
}: TableContentsProps) => {
|
|
48
50
|
const [isSectionOpen, setOpenSection] = useState<number | null>(null);
|
|
49
51
|
const lastToggledSection = useRef<number | null>(null);
|
|
@@ -147,7 +149,7 @@ const TableContents = ({
|
|
|
147
149
|
}
|
|
148
150
|
hideColumns={hideColumns}
|
|
149
151
|
hideRows={sectionHideRows}
|
|
150
|
-
hideHeader
|
|
152
|
+
hideHeader={!showHeader || !isFirstSection}
|
|
151
153
|
openModal={openModal}
|
|
152
154
|
title={`${title}${
|
|
153
155
|
section?.title ? ` - ${section.title}` : ''
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@use '../../scss/public/colors' as *;
|
|
2
|
+
@use '../../scss/public/font-weight' as *;
|
|
3
|
+
|
|
4
|
+
.tabs {
|
|
5
|
+
display: flex;
|
|
6
|
+
column-gap: 24px;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.tab {
|
|
10
|
+
border: none;
|
|
11
|
+
border-bottom: 2px solid transparent;
|
|
12
|
+
font-weight: $p-font-weight-bold;
|
|
13
|
+
padding: 0 0 4px;
|
|
14
|
+
color: $ds-neutral-500;
|
|
15
|
+
|
|
16
|
+
&:hover, &Active {
|
|
17
|
+
color: $ds-neutral-900;
|
|
18
|
+
border-bottom-color: $ds-neutral-900;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
&:focus-visible {
|
|
22
|
+
outline: 1px solid $ds-neutral-900;
|
|
23
|
+
outline-offset: 2px;
|
|
24
|
+
border-radius: 2px;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Tabs, TabsProps } from './Tabs';
|
|
3
|
+
|
|
4
|
+
const story = {
|
|
5
|
+
title: 'JSX/Tabs',
|
|
6
|
+
component: Tabs,
|
|
7
|
+
argTypes: {
|
|
8
|
+
tabs: {
|
|
9
|
+
description: 'Array of tab items with id and label',
|
|
10
|
+
},
|
|
11
|
+
activeIndex: {
|
|
12
|
+
description: 'The currently active tab index',
|
|
13
|
+
},
|
|
14
|
+
onTabChange: {
|
|
15
|
+
action: true,
|
|
16
|
+
table: {
|
|
17
|
+
category: 'Callbacks',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
className: {
|
|
21
|
+
description: 'Optional CSS class to apply to the tabs container',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
args: {
|
|
25
|
+
tabs: [
|
|
26
|
+
{ id: 'basic', label: 'Basic' },
|
|
27
|
+
{ id: 'standard', label: 'Standard' },
|
|
28
|
+
{ id: 'premium', label: 'Premium' },
|
|
29
|
+
],
|
|
30
|
+
activeIndex: 0,
|
|
31
|
+
},
|
|
32
|
+
parameters: {
|
|
33
|
+
componentSubtitle:
|
|
34
|
+
'A horizontal tab bar for switching between content sections.',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const Default = {
|
|
39
|
+
render: ({ tabs, activeIndex, onTabChange }: TabsProps) => {
|
|
40
|
+
const [index, setIndex] = useState(activeIndex);
|
|
41
|
+
|
|
42
|
+
const handleChange = (newIndex: number) => {
|
|
43
|
+
onTabChange?.(newIndex);
|
|
44
|
+
setIndex(newIndex);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return <Tabs tabs={tabs} activeIndex={index} onTabChange={handleChange} />;
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
name: 'Default',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const TwoTabs = {
|
|
54
|
+
render: () => {
|
|
55
|
+
const [index, setIndex] = useState(0);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Tabs
|
|
59
|
+
tabs={[
|
|
60
|
+
{ id: 'monthly', label: 'Monthly' },
|
|
61
|
+
{ id: 'yearly', label: 'Yearly' },
|
|
62
|
+
]}
|
|
63
|
+
activeIndex={index}
|
|
64
|
+
onTabChange={setIndex}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
name: 'Two Tabs',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const ManyTabs = {
|
|
73
|
+
render: () => {
|
|
74
|
+
const [index, setIndex] = useState(0);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Tabs
|
|
78
|
+
tabs={[
|
|
79
|
+
{ id: 'overview', label: 'Overview' },
|
|
80
|
+
{ id: 'coverage', label: 'Coverage' },
|
|
81
|
+
{ id: 'pricing', label: 'Pricing' },
|
|
82
|
+
{ id: 'reviews', label: 'Reviews' },
|
|
83
|
+
{ id: 'faq', label: 'FAQ' },
|
|
84
|
+
]}
|
|
85
|
+
activeIndex={index}
|
|
86
|
+
onTabChange={setIndex}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
name: 'Many Tabs',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export default story;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import { KeyboardEvent, useCallback, useRef } from 'react';
|
|
3
|
+
|
|
4
|
+
import styles from './Tabs.module.scss';
|
|
5
|
+
|
|
6
|
+
export interface TabItem {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TabsProps {
|
|
12
|
+
tabs: TabItem[];
|
|
13
|
+
activeIndex: number;
|
|
14
|
+
onTabChange: (index: number) => void;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const Tabs = ({ tabs, activeIndex, onTabChange, className }: TabsProps) => {
|
|
19
|
+
const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
|
20
|
+
|
|
21
|
+
const setTabRef = useCallback(
|
|
22
|
+
(index: number) => (el: HTMLButtonElement | null) => {
|
|
23
|
+
tabRefs.current[index] = el;
|
|
24
|
+
},
|
|
25
|
+
[]
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const handleKeyDown = (e: KeyboardEvent, index: number) => {
|
|
29
|
+
const count = tabs.length;
|
|
30
|
+
let nextIndex: number | null = null;
|
|
31
|
+
|
|
32
|
+
if (e.key === 'ArrowRight') nextIndex = (index + 1) % count;
|
|
33
|
+
if (e.key === 'ArrowLeft') nextIndex = (index - 1 + count) % count;
|
|
34
|
+
|
|
35
|
+
if (nextIndex !== null) {
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
onTabChange(nextIndex);
|
|
38
|
+
tabRefs.current[nextIndex]?.focus({ preventScroll: true });
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className={classNames(styles.tabs, className)} role="tablist">
|
|
44
|
+
{tabs.map((tab, index) => (
|
|
45
|
+
<button
|
|
46
|
+
ref={setTabRef(index)}
|
|
47
|
+
key={tab.id}
|
|
48
|
+
type="button"
|
|
49
|
+
role="tab"
|
|
50
|
+
aria-selected={activeIndex === index}
|
|
51
|
+
tabIndex={activeIndex === index ? 0 : -1}
|
|
52
|
+
className={classNames('p-p bg-transparent c-pointer', styles.tab, {
|
|
53
|
+
[styles.tabActive]: activeIndex === index,
|
|
54
|
+
})}
|
|
55
|
+
onClick={() => onTabChange(index)}
|
|
56
|
+
onKeyDown={(e) => handleKeyDown(e, index)}
|
|
57
|
+
>
|
|
58
|
+
{tab.label}
|
|
59
|
+
</button>
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export { Tabs };
|
package/src/lib/index.tsx
CHANGED
|
@@ -54,6 +54,7 @@ import { Toaster, toast } from './components/toast';
|
|
|
54
54
|
import { IconWrapperProps } from './components/icon/IconWrapper';
|
|
55
55
|
import { Accordion, AccordionProps } from './components/accordion';
|
|
56
56
|
import { Table, TableData, TableProps } from './components/table/Table';
|
|
57
|
+
import { Tabs, TabsProps, TabItem } from './components/tabs/Tabs';
|
|
57
58
|
import { useEscapeKey } from './hooks/useEscapeKey';
|
|
58
59
|
import { useFocusWithin } from './hooks/useFocusWithin';
|
|
59
60
|
import { useMediaQuery } from './hooks/useMediaQuery';
|
|
@@ -107,6 +108,7 @@ export {
|
|
|
107
108
|
illustrations,
|
|
108
109
|
Spinner,
|
|
109
110
|
Table,
|
|
111
|
+
Tabs,
|
|
110
112
|
Toggle,
|
|
111
113
|
Toaster,
|
|
112
114
|
toast,
|
|
@@ -139,6 +141,8 @@ export type {
|
|
|
139
141
|
UploadStatus,
|
|
140
142
|
CardProps,
|
|
141
143
|
IconWrapperProps,
|
|
144
|
+
TabItem,
|
|
145
|
+
TabsProps,
|
|
142
146
|
TableData,
|
|
143
147
|
TableProps,
|
|
144
148
|
};
|