@popsure/dirty-swan 0.66.18 → 0.66.20
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/comparisonTable/components/AccordionItem/AccordionItem.js +2 -2
- package/dist/esm/components/comparisonTable/components/AccordionItem/AccordionItem.js.map +1 -1
- 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 +81 -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 +6 -4
- package/dist/esm/components/table/components/TableContents/TableContents.js.map +1 -1
- package/dist/esm/components/table/components/TableContents/TableContents.test.js +2 -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/comparisonTable/components/AccordionItem/AccordionItem.module.scss +4 -0
- package/src/lib/components/comparisonTable/components/AccordionItem/AccordionItem.tsx +1 -0
- package/src/lib/components/table/Table.stories.tsx +104 -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.module.scss +4 -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% + 48px);
|
|
9
|
+
margin-left: -24px;
|
|
10
|
+
margin-right: -24px;
|
|
11
|
+
padding: 16px 24px;
|
|
12
|
+
border-radius: 8px;
|
|
13
|
+
border: 1px solid transparent;
|
|
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
|
};
|
|
@@ -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}
|
|
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
|
};
|