@toptal/picasso-tabs 5.0.15-alpha-allow-to-pass-class-names-in-modal-package-7c2827b69.2 → 5.0.15-alpha-ff-7-tabs-17eb872bb.13
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-package/src/Tab/Tab.d.ts +5 -8
- package/dist-package/src/Tab/Tab.d.ts.map +1 -1
- package/dist-package/src/Tab/Tab.js +61 -16
- package/dist-package/src/Tab/Tab.js.map +1 -1
- package/dist-package/src/Tabs/Tabs.d.ts +12 -9
- package/dist-package/src/Tabs/Tabs.d.ts.map +1 -1
- package/dist-package/src/Tabs/Tabs.js +68 -16
- package/dist-package/src/Tabs/Tabs.js.map +1 -1
- package/dist-package/src/Tabs/TabsContext.d.ts +11 -0
- package/dist-package/src/Tabs/TabsContext.d.ts.map +1 -0
- package/dist-package/src/Tabs/TabsContext.js +15 -0
- package/dist-package/src/Tabs/TabsContext.js.map +1 -0
- package/dist-package/src/Tabs/index.d.ts +3 -2
- package/dist-package/src/Tabs/index.d.ts.map +1 -1
- package/dist-package/src/Tabs/index.js.map +1 -1
- package/dist-package/src/TabsCompound/index.d.ts +2 -4
- package/dist-package/src/TabsCompound/index.d.ts.map +1 -1
- package/dist-package/src/index.d.ts +0 -1
- package/dist-package/src/index.d.ts.map +1 -1
- package/dist-package/src/index.js +0 -1
- package/dist-package/src/index.js.map +1 -1
- package/package.json +13 -13
- package/src/Tab/Tab.tsx +121 -51
- package/src/Tab/__snapshots__/test.tsx.snap +29 -45
- package/src/Tab/story/IconOrBadge.example.tsx +8 -3
- package/src/Tabs/Tabs.tsx +125 -42
- package/src/Tabs/TabsContext.tsx +27 -0
- package/src/Tabs/__snapshots__/test.tsx.snap +45 -76
- package/src/Tabs/index.ts +3 -2
- package/src/Tabs/story/Default.example.tsx +4 -2
- package/src/Tabs/test.tsx +4 -4
- package/src/index.ts +0 -1
- package/dist-package/src/Tab/styles.d.ts +0 -4
- package/dist-package/src/Tab/styles.d.ts.map +0 -1
- package/dist-package/src/Tab/styles.js +0 -95
- package/dist-package/src/Tab/styles.js.map +0 -1
- package/dist-package/src/TabScrollButton/TabScrollButton.d.ts +0 -12
- package/dist-package/src/TabScrollButton/TabScrollButton.d.ts.map +0 -1
- package/dist-package/src/TabScrollButton/TabScrollButton.js +0 -30
- package/dist-package/src/TabScrollButton/TabScrollButton.js.map +0 -1
- package/dist-package/src/TabScrollButton/index.d.ts +0 -5
- package/dist-package/src/TabScrollButton/index.d.ts.map +0 -1
- package/dist-package/src/TabScrollButton/index.js +0 -2
- package/dist-package/src/TabScrollButton/index.js.map +0 -1
- package/dist-package/src/Tabs/styles.d.ts +0 -4
- package/dist-package/src/Tabs/styles.d.ts.map +0 -1
- package/dist-package/src/Tabs/styles.js +0 -41
- package/dist-package/src/Tabs/styles.js.map +0 -1
- package/dist-package/src/Tabs/use-tab-action.d.ts +0 -5
- package/dist-package/src/Tabs/use-tab-action.d.ts.map +0 -1
- package/dist-package/src/Tabs/use-tab-action.js +0 -21
- package/dist-package/src/Tabs/use-tab-action.js.map +0 -1
- package/src/Tab/styles.ts +0 -107
- package/src/TabScrollButton/TabScrollButton.tsx +0 -59
- package/src/TabScrollButton/index.ts +0 -6
- package/src/Tabs/styles.ts +0 -45
- package/src/Tabs/use-tab-action.ts +0 -27
package/src/Tab/Tab.tsx
CHANGED
@@ -1,30 +1,26 @@
|
|
1
1
|
import type { ReactNode, HTMLAttributes, ReactElement } from 'react'
|
2
|
-
import React, { forwardRef
|
3
|
-
import type { Theme } from '@material-ui/core/styles'
|
4
|
-
import { makeStyles } from '@material-ui/core/styles'
|
5
|
-
import type { TabProps } from '@material-ui/core'
|
6
|
-
import { Tab as MUITab } from '@material-ui/core'
|
2
|
+
import React, { forwardRef } from 'react'
|
7
3
|
import type { BaseProps, TextLabelProps } from '@toptal/picasso-shared'
|
8
4
|
import { useTitleCase } from '@toptal/picasso-shared'
|
9
5
|
import { UserBadge } from '@toptal/picasso-user-badge'
|
6
|
+
import { twJoin, twMerge } from '@toptal/picasso-tailwind-merge'
|
10
7
|
|
11
|
-
import
|
12
|
-
import { TabsOrientationContext } from '../Tabs/Tabs'
|
8
|
+
import { useTabsContext, type TabsValueType } from '../Tabs/TabsContext'
|
13
9
|
import { TabLabel } from '../TabLabel'
|
14
10
|
import { TabDescription } from '../TabDescription'
|
15
11
|
|
16
12
|
export interface Props
|
17
13
|
extends BaseProps,
|
18
14
|
TextLabelProps,
|
19
|
-
Omit<HTMLAttributes<
|
15
|
+
Omit<HTMLAttributes<HTMLButtonElement>, 'onChange'> {
|
20
16
|
/**
|
21
17
|
* If true, the tab will be disabled
|
22
18
|
* @default false
|
23
19
|
*/
|
24
20
|
disabled?: boolean
|
25
21
|
|
26
|
-
/**
|
27
|
-
value?:
|
22
|
+
/** The value of the tab */
|
23
|
+
value?: TabsValueType
|
28
24
|
|
29
25
|
/** The label element */
|
30
26
|
label?: ReactNode
|
@@ -40,32 +36,97 @@ export interface Props
|
|
40
36
|
|
41
37
|
// Properties below are managed by Tabs component
|
42
38
|
|
43
|
-
selected?: boolean
|
44
|
-
onChange?: TabProps['onChange']
|
45
|
-
onClick?: TabProps['onClick']
|
39
|
+
// selected?: boolean
|
40
|
+
// onChange?: TabProps['onChange']
|
41
|
+
// onClick?: TabProps['onClick']
|
46
42
|
}
|
47
43
|
|
48
|
-
const
|
44
|
+
const getOpacityClass = (
|
45
|
+
selected: boolean,
|
46
|
+
disabled: boolean,
|
47
|
+
orientation: 'horizontal' | 'vertical'
|
48
|
+
) => {
|
49
|
+
if (disabled) {
|
50
|
+
return 'opacity-50'
|
51
|
+
}
|
52
|
+
if (selected || orientation === 'vertical') {
|
53
|
+
return 'opacity-100 '
|
54
|
+
}
|
55
|
+
|
56
|
+
return 'opacity-70'
|
57
|
+
}
|
58
|
+
|
59
|
+
const wrapperClassesByOrientation = {
|
60
|
+
horizontal: 'inline-flex flex-row items-center justify-center',
|
61
|
+
vertical: 'block',
|
62
|
+
}
|
63
|
+
|
64
|
+
const rootClassesByOrientation = (selected: boolean) => ({
|
65
|
+
horizontal: [
|
66
|
+
'm-0 [&:not(:last-child)]:mr-8 pt-0 pb-[0.4375rem] px-0',
|
67
|
+
'text-center bg-transparent transition-shadow z-10 rounded-none',
|
68
|
+
'text-black',
|
69
|
+
selected && 'shadow-blue-500 shadow-[inset_0_-2px_0]',
|
70
|
+
],
|
71
|
+
vertical: [
|
72
|
+
' first:mt-4 last:mb-4 my-1 mx-0 py-2 px-4',
|
73
|
+
'text-left rounded-l-sm rounded-r-none transition-all',
|
74
|
+
'w-full overflow-hidden',
|
75
|
+
selected && 'shadow-1',
|
76
|
+
selected && [
|
77
|
+
'before:absolute',
|
78
|
+
'before:content-[""]',
|
79
|
+
'before:bottom-0',
|
80
|
+
'before:left-0',
|
81
|
+
'before:top-0',
|
82
|
+
'before:w-[3px]',
|
83
|
+
'before:bg-blue-500',
|
84
|
+
],
|
85
|
+
selected
|
86
|
+
? 'bg-gray-50 text-black'
|
87
|
+
: 'bg-gray-100 hover:bg-gray-200 text-graphite-700 hover:text-black',
|
88
|
+
],
|
89
|
+
})
|
90
|
+
|
91
|
+
const classesByVariant = {
|
92
|
+
scrollable: 'shrink-0 max-w-[264px]',
|
93
|
+
fullWidth: 'shrink flex-grow basis-0',
|
94
|
+
}
|
49
95
|
|
50
|
-
export const Tab = forwardRef<
|
96
|
+
export const Tab = forwardRef<HTMLButtonElement, Props>(function Tab(
|
97
|
+
props,
|
98
|
+
ref
|
99
|
+
) {
|
51
100
|
const {
|
52
101
|
disabled,
|
53
102
|
value,
|
54
103
|
label,
|
55
104
|
icon,
|
56
|
-
selected,
|
57
|
-
onChange,
|
58
|
-
onClick,
|
59
105
|
titleCase: propsTitleCase,
|
60
106
|
description,
|
61
107
|
avatar,
|
108
|
+
className,
|
109
|
+
onClick,
|
62
110
|
...rest
|
63
111
|
} = props
|
64
|
-
const classes = useStyles()
|
65
112
|
const titleCase = useTitleCase(propsTitleCase)
|
66
|
-
const
|
113
|
+
const {
|
114
|
+
value: selectedValue,
|
115
|
+
onChange,
|
116
|
+
orientation,
|
117
|
+
variant,
|
118
|
+
} = useTabsContext()
|
119
|
+
const isHorizontal = orientation === 'horizontal'
|
120
|
+
const selected = value === selectedValue
|
121
|
+
|
122
|
+
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
123
|
+
if (!disabled && onChange) {
|
124
|
+
onChange(event, value as TabsValueType)
|
125
|
+
}
|
126
|
+
onClick?.(event)
|
127
|
+
}
|
67
128
|
|
68
|
-
const
|
129
|
+
const renderLabel = getLabelComponent({
|
69
130
|
avatar,
|
70
131
|
description,
|
71
132
|
disabled,
|
@@ -75,39 +136,44 @@ export const Tab = forwardRef<HTMLDivElement, Props>(function Tab(props, ref) {
|
|
75
136
|
})
|
76
137
|
|
77
138
|
return (
|
78
|
-
<
|
79
|
-
className=
|
80
|
-
|
139
|
+
<button
|
140
|
+
className={twMerge(
|
141
|
+
getOpacityClass(selected, !!disabled, orientation),
|
142
|
+
rootClassesByOrientation(selected)[orientation],
|
143
|
+
classesByVariant[variant],
|
144
|
+
disabled ? 'cursor-default text-gray-500' : 'cursor-pointer',
|
145
|
+
disabled && 'pointer-events-none',
|
146
|
+
icon && isHorizontal && 'min-h-0 pt-0 pr-6',
|
147
|
+
'min-w-0 sm:min-w-[160px] md:min-w-[auto]',
|
148
|
+
'border-0 cursor-pointer inline-flex outline-none',
|
149
|
+
'items-center select-none align-middle appearance-none',
|
150
|
+
'justify-center no-underline [-webkit-tap-highlight-color:transparent]',
|
151
|
+
'normal-case whitespace-normal leading-4',
|
152
|
+
'relative ',
|
153
|
+
className
|
154
|
+
)}
|
81
155
|
ref={ref}
|
82
|
-
tabIndex={0}
|
156
|
+
tabIndex={disabled ? -1 : 0}
|
83
157
|
disabled={disabled}
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
158
|
+
onClick={handleClick}
|
159
|
+
role='tab'
|
160
|
+
aria-selected={selected}
|
161
|
+
aria-disabled={disabled}
|
162
|
+
type='button'
|
163
|
+
{...rest}
|
164
|
+
>
|
165
|
+
<span
|
166
|
+
className={twJoin('w-full', wrapperClassesByOrientation[orientation])}
|
167
|
+
>
|
168
|
+
{renderLabel}
|
169
|
+
{icon && <span className='absolute right-0 mb-0'>{icon}</span>}
|
170
|
+
</span>
|
171
|
+
</button>
|
96
172
|
)
|
97
173
|
})
|
98
174
|
|
99
|
-
Tab.defaultProps = {}
|
100
|
-
|
101
175
|
Tab.displayName = 'Tab'
|
102
176
|
|
103
|
-
type GetLabelComponentProps = {
|
104
|
-
avatar?: string | null
|
105
|
-
description?: string
|
106
|
-
disabled?: boolean
|
107
|
-
label?: React.ReactNode
|
108
|
-
orientation: 'horizontal' | 'vertical'
|
109
|
-
titleCase?: boolean
|
110
|
-
}
|
111
177
|
const getLabelComponent = ({
|
112
178
|
avatar,
|
113
179
|
description,
|
@@ -115,14 +181,19 @@ const getLabelComponent = ({
|
|
115
181
|
label,
|
116
182
|
orientation,
|
117
183
|
titleCase,
|
118
|
-
}:
|
184
|
+
}: {
|
185
|
+
avatar?: string | null
|
186
|
+
description?: string
|
187
|
+
disabled?: boolean
|
188
|
+
label?: React.ReactNode
|
189
|
+
orientation: 'horizontal' | 'vertical'
|
190
|
+
titleCase?: boolean
|
191
|
+
}): React.ReactNode => {
|
119
192
|
if (!label) {
|
120
193
|
return null
|
121
194
|
}
|
122
|
-
|
123
195
|
const isHorizontal = orientation === 'horizontal'
|
124
196
|
const isCustomLabel = typeof label !== 'string'
|
125
|
-
|
126
197
|
const Label = () => (
|
127
198
|
<TabLabel titleCase={titleCase} label={label} orientation={orientation} />
|
128
199
|
)
|
@@ -130,7 +201,6 @@ const getLabelComponent = ({
|
|
130
201
|
if (isHorizontal || isCustomLabel) {
|
131
202
|
return <Label />
|
132
203
|
}
|
133
|
-
|
134
204
|
if (typeof avatar === 'undefined') {
|
135
205
|
return (
|
136
206
|
<>
|
@@ -6,31 +6,29 @@ exports[`Tab Tab disabled tab 1`] = `
|
|
6
6
|
class="Picasso-root"
|
7
7
|
>
|
8
8
|
<div
|
9
|
-
|
9
|
+
aria-orientation="horizontal"
|
10
|
+
class="relative min-h flex overflow-hidden overflow-x"
|
10
11
|
data-component-type="tabs"
|
11
12
|
>
|
12
13
|
<div
|
13
|
-
class="
|
14
|
-
style="width: 99px; height: 99px; position: absolute; top: -9999px; overflow: scroll;"
|
15
|
-
/>
|
16
|
-
<div
|
17
|
-
class="MuiTabs-scroller MuiTabs-scrollable"
|
18
|
-
style="margin-bottom: 0px;"
|
14
|
+
class="after:absolute after:content-[""] after:bottom-0 after:left-0 after:right-0 after:h-[1px] after:bg-gray after:z-0 flex-auto inline-block relative whitespace-nowrap"
|
19
15
|
>
|
20
16
|
<div
|
21
|
-
class="
|
17
|
+
class="flex"
|
22
18
|
role="tablist"
|
19
|
+
tabindex="-1"
|
23
20
|
>
|
24
21
|
<button
|
22
|
+
aria-disabled="true"
|
25
23
|
aria-selected="false"
|
26
|
-
class="
|
24
|
+
class="opacity-50 m-0 [&:not(:last-child)]:mr-8 pt-0 pb-[0.4375rem] px-0 text-center bg-transparent transition-shadow z-10 rounded-none shrink-0 max-w text-gray pointer-events min-w sm:min-w md:min-w border-0 cursor-pointer inline-flex outline-none items-center select-none align-middle appearance-none justify-center no-underline [-webkit-tap-highlight normal-case whitespace-normal leading-4 relative"
|
27
25
|
disabled=""
|
28
26
|
role="tab"
|
29
27
|
tabindex="-1"
|
30
28
|
type="button"
|
31
29
|
>
|
32
30
|
<span
|
33
|
-
class="
|
31
|
+
class="w-full inline-flex flex-row items-center justify-center"
|
34
32
|
>
|
35
33
|
<div
|
36
34
|
class="m-0 text-sm text-inherit font-semibold leading-[1.1rem]"
|
@@ -40,10 +38,6 @@ exports[`Tab Tab disabled tab 1`] = `
|
|
40
38
|
</span>
|
41
39
|
</button>
|
42
40
|
</div>
|
43
|
-
<span
|
44
|
-
class="PrivateTabIndicator-root PrivateTabIndicator-colorSecondary MuiTabs-indicator"
|
45
|
-
style="left: 0px; width: 0px;"
|
46
|
-
/>
|
47
41
|
</div>
|
48
42
|
</div>
|
49
43
|
</div>
|
@@ -56,30 +50,27 @@ exports[`Tab Tab renders 1`] = `
|
|
56
50
|
class="Picasso-root"
|
57
51
|
>
|
58
52
|
<div
|
59
|
-
|
53
|
+
aria-orientation="horizontal"
|
54
|
+
class="relative min-h flex overflow-hidden overflow-x"
|
60
55
|
data-component-type="tabs"
|
61
56
|
>
|
62
57
|
<div
|
63
|
-
class="
|
64
|
-
style="width: 99px; height: 99px; position: absolute; top: -9999px; overflow: scroll;"
|
65
|
-
/>
|
66
|
-
<div
|
67
|
-
class="MuiTabs-scroller MuiTabs-scrollable"
|
68
|
-
style="margin-bottom: 0px;"
|
58
|
+
class="after:absolute after:content-[""] after:bottom-0 after:left-0 after:right-0 after:h-[1px] after:bg-gray after:z-0 flex-auto inline-block relative whitespace-nowrap"
|
69
59
|
>
|
70
60
|
<div
|
71
|
-
class="
|
61
|
+
class="flex"
|
72
62
|
role="tablist"
|
63
|
+
tabindex="-1"
|
73
64
|
>
|
74
65
|
<button
|
75
66
|
aria-selected="false"
|
76
|
-
class="
|
67
|
+
class="opacity-70 m-0 [&:not(:last-child)]:mr-8 pt-0 pb-[0.4375rem] px-0 text-center bg-transparent transition-shadow z-10 rounded-none text-black shrink-0 max-w min-w sm:min-w md:min-w border-0 cursor-pointer inline-flex outline-none items-center select-none align-middle appearance-none justify-center no-underline [-webkit-tap-highlight normal-case whitespace-normal leading-4 relative"
|
77
68
|
role="tab"
|
78
69
|
tabindex="0"
|
79
70
|
type="button"
|
80
71
|
>
|
81
72
|
<span
|
82
|
-
class="
|
73
|
+
class="w-full inline-flex flex-row items-center justify-center"
|
83
74
|
>
|
84
75
|
<div
|
85
76
|
class="m-0 text-sm text-inherit font-semibold leading-[1.1rem]"
|
@@ -89,10 +80,6 @@ exports[`Tab Tab renders 1`] = `
|
|
89
80
|
</span>
|
90
81
|
</button>
|
91
82
|
</div>
|
92
|
-
<span
|
93
|
-
class="PrivateTabIndicator-root PrivateTabIndicator-colorSecondary MuiTabs-indicator"
|
94
|
-
style="left: 0px; width: 0px;"
|
95
|
-
/>
|
96
83
|
</div>
|
97
84
|
</div>
|
98
85
|
</div>
|
@@ -105,46 +92,43 @@ exports[`Tab Tab tab with icon 1`] = `
|
|
105
92
|
class="Picasso-root"
|
106
93
|
>
|
107
94
|
<div
|
108
|
-
|
95
|
+
aria-orientation="horizontal"
|
96
|
+
class="relative min-h flex overflow-hidden overflow-x"
|
109
97
|
data-component-type="tabs"
|
110
98
|
>
|
111
99
|
<div
|
112
|
-
class="
|
113
|
-
style="width: 99px; height: 99px; position: absolute; top: -9999px; overflow: scroll;"
|
114
|
-
/>
|
115
|
-
<div
|
116
|
-
class="MuiTabs-scroller MuiTabs-scrollable"
|
117
|
-
style="margin-bottom: 0px;"
|
100
|
+
class="after:absolute after:content-[""] after:bottom-0 after:left-0 after:right-0 after:h-[1px] after:bg-gray after:z-0 flex-auto inline-block relative whitespace-nowrap"
|
118
101
|
>
|
119
102
|
<div
|
120
|
-
class="
|
103
|
+
class="flex"
|
121
104
|
role="tablist"
|
105
|
+
tabindex="-1"
|
122
106
|
>
|
123
107
|
<button
|
124
108
|
aria-selected="false"
|
125
|
-
class="
|
109
|
+
class="opacity-70 m-0 [&:not(:last-child)]:mr-8 pb-[0.4375rem] px-0 text-center bg-transparent transition-shadow z-10 rounded-none text-black shrink-0 max-w min-h pt-0 pr-6 min-w sm:min-w md:min-w border-0 cursor-pointer inline-flex outline-none items-center select-none align-middle appearance-none justify-center no-underline [-webkit-tap-highlight normal-case whitespace-normal leading-4 relative"
|
126
110
|
role="tab"
|
127
111
|
tabindex="0"
|
128
112
|
type="button"
|
129
113
|
>
|
130
114
|
<span
|
131
|
-
class="
|
115
|
+
class="w-full inline-flex flex-row items-center justify-center"
|
132
116
|
>
|
133
|
-
<div
|
134
|
-
id="Icon"
|
135
|
-
/>
|
136
117
|
<div
|
137
118
|
class="m-0 text-sm text-inherit font-semibold leading-[1.1rem]"
|
138
119
|
>
|
139
120
|
Tab Label
|
140
121
|
</div>
|
122
|
+
<span
|
123
|
+
class="absolute right-0 mb-0"
|
124
|
+
>
|
125
|
+
<div
|
126
|
+
id="Icon"
|
127
|
+
/>
|
128
|
+
</span>
|
141
129
|
</span>
|
142
130
|
</button>
|
143
131
|
</div>
|
144
|
-
<span
|
145
|
-
class="PrivateTabIndicator-root PrivateTabIndicator-colorSecondary MuiTabs-indicator"
|
146
|
-
style="left: 0px; width: 0px;"
|
147
|
-
/>
|
148
132
|
</div>
|
149
133
|
</div>
|
150
134
|
</div>
|
@@ -3,10 +3,12 @@ import { Container, Tabs, Tooltip, Badge } from '@toptal/picasso'
|
|
3
3
|
import { SPACING_4 } from '@toptal/picasso-utils'
|
4
4
|
import { Exclamation16 } from '@toptal/picasso-icons'
|
5
5
|
|
6
|
+
import type { TabsValueType } from '../../Tabs/TabsContext'
|
7
|
+
|
6
8
|
const Example = () => {
|
7
|
-
const [value, setValue] = React.useState(0)
|
9
|
+
const [value, setValue] = React.useState<TabsValueType>(0)
|
8
10
|
|
9
|
-
const handleChange = (_: React.ChangeEvent<{}>, newValue:
|
11
|
+
const handleChange = (_: React.ChangeEvent<{}>, newValue: TabsValueType) => {
|
10
12
|
setValue(newValue)
|
11
13
|
}
|
12
14
|
|
@@ -24,7 +26,10 @@ const Example = () => {
|
|
24
26
|
}
|
25
27
|
/>
|
26
28
|
<Tabs.Tab label='Label' />
|
27
|
-
<Tabs.Tab
|
29
|
+
<Tabs.Tab
|
30
|
+
label='Label'
|
31
|
+
icon={<Badge content={10} variant='white' className='mt-[1px]' />}
|
32
|
+
/>
|
28
33
|
</Tabs>
|
29
34
|
|
30
35
|
{value === 0 && (
|
package/src/Tabs/Tabs.tsx
CHANGED
@@ -1,24 +1,21 @@
|
|
1
|
-
import type {
|
2
|
-
import React, { forwardRef } from 'react'
|
3
|
-
import type { Theme } from '@material-ui/core/styles'
|
4
|
-
import { makeStyles } from '@material-ui/core/styles'
|
5
|
-
import { Tabs as MUITabs } from '@material-ui/core'
|
1
|
+
import type { ReactNode, ChangeEvent } from 'react'
|
2
|
+
import React, { forwardRef, useMemo, useCallback } from 'react'
|
6
3
|
import type { BaseProps } from '@toptal/picasso-shared'
|
4
|
+
import { twJoin, twMerge } from '@toptal/picasso-tailwind-merge'
|
7
5
|
|
8
|
-
import {
|
9
|
-
import styles from './styles'
|
10
|
-
import useTabAction from './use-tab-action'
|
11
|
-
|
12
|
-
export type TabsValueType = string | number | false
|
6
|
+
import { TabsContext, type TabsValueType } from './TabsContext'
|
13
7
|
|
14
8
|
export interface Props<V extends TabsValueType> extends BaseProps {
|
15
9
|
/** Tabs content containing Tab components */
|
16
10
|
children: ReactNode
|
17
11
|
|
18
12
|
/** Callback fired when the value changes. */
|
19
|
-
onChange?: (event:
|
13
|
+
onChange?: (event: ChangeEvent<{}>, value: V) => void
|
20
14
|
|
21
|
-
/**
|
15
|
+
/**
|
16
|
+
* The value of the currently selected Tab.
|
17
|
+
* If you don't want any selected Tab, you can set this property to null.
|
18
|
+
*/
|
22
19
|
value: V
|
23
20
|
|
24
21
|
/** The tabs orientation (layout flow direction). */
|
@@ -26,54 +23,140 @@ export interface Props<V extends TabsValueType> extends BaseProps {
|
|
26
23
|
|
27
24
|
/** Determines additional display behavior of the tabs */
|
28
25
|
variant?: 'scrollable' | 'fullWidth'
|
26
|
+
|
27
|
+
/** The default value. Use when the component is not controlled. */
|
28
|
+
defaultValue?: V
|
29
|
+
|
30
|
+
/** The direction of the text. */
|
31
|
+
direction?: 'ltr' | 'rtl'
|
29
32
|
}
|
30
33
|
|
31
|
-
const
|
32
|
-
|
33
|
-
|
34
|
+
const indicatorClasses = [
|
35
|
+
'after:absolute',
|
36
|
+
'after:content-[""]',
|
37
|
+
'after:bottom-0',
|
38
|
+
'after:left-0',
|
39
|
+
'after:right-0',
|
40
|
+
'after:h-[1px]',
|
41
|
+
'after:bg-gray-500',
|
42
|
+
'after:z-0',
|
43
|
+
]
|
44
|
+
|
45
|
+
const classesByOrientation = {
|
46
|
+
vertical: {
|
47
|
+
root: 'w-[200px] m-0 flex-col',
|
48
|
+
scroller: 'pl-2',
|
49
|
+
},
|
50
|
+
horizontal: {
|
51
|
+
root: '',
|
52
|
+
scroller: indicatorClasses,
|
53
|
+
},
|
54
|
+
}
|
34
55
|
|
35
|
-
|
36
|
-
|
37
|
-
|
56
|
+
const classesByVariant = {
|
57
|
+
scrollable: {
|
58
|
+
root: 'overflow-x-auto',
|
59
|
+
scroller: '',
|
60
|
+
},
|
61
|
+
fullWidth: {
|
62
|
+
root: '',
|
63
|
+
scroller: 'w-full overflow-hidden',
|
64
|
+
},
|
65
|
+
}
|
38
66
|
|
39
|
-
export const Tabs = forwardRef(
|
40
|
-
|
41
|
-
props: Props<V>,
|
42
|
-
ref: ForwardedRef<HTMLButtonElement>
|
43
|
-
) => {
|
67
|
+
export const Tabs = forwardRef<HTMLDivElement, Props<TabsValueType>>(
|
68
|
+
function Tabs(props, ref) {
|
44
69
|
const {
|
45
70
|
children,
|
46
71
|
orientation = 'horizontal',
|
47
72
|
onChange,
|
48
|
-
value,
|
73
|
+
value: valueProp,
|
74
|
+
defaultValue,
|
49
75
|
variant = 'scrollable',
|
76
|
+
direction = 'ltr',
|
77
|
+
className,
|
50
78
|
...rest
|
51
79
|
} = props
|
52
|
-
|
53
|
-
const
|
80
|
+
|
81
|
+
const [value, setValue] = React.useState<TabsValueType>(
|
82
|
+
defaultValue ?? null
|
83
|
+
)
|
84
|
+
const isControlled = valueProp !== undefined
|
85
|
+
const currentValue = isControlled ? valueProp : value
|
86
|
+
|
87
|
+
const handleChange = useCallback(
|
88
|
+
(event: ChangeEvent<{}>, newValue: TabsValueType) => {
|
89
|
+
if (!isControlled) {
|
90
|
+
setValue(newValue)
|
91
|
+
}
|
92
|
+
onChange?.(event, newValue as TabsValueType)
|
93
|
+
},
|
94
|
+
[isControlled, onChange]
|
95
|
+
)
|
96
|
+
|
97
|
+
const contextValue = useMemo(
|
98
|
+
() => ({
|
99
|
+
value: currentValue,
|
100
|
+
onChange: handleChange,
|
101
|
+
orientation,
|
102
|
+
variant,
|
103
|
+
direction,
|
104
|
+
}),
|
105
|
+
[currentValue, handleChange, orientation, variant, direction]
|
106
|
+
)
|
107
|
+
|
108
|
+
const isVertical = orientation === 'vertical'
|
109
|
+
|
110
|
+
const childrenWithIndex = React.Children.map(children, (child, idx) => {
|
111
|
+
if (
|
112
|
+
React.isValidElement(child) &&
|
113
|
+
// @ts-expect-error: type check for Picasso Tab
|
114
|
+
(child.type.displayName === 'Tab' || child.type.name === 'Tab') &&
|
115
|
+
child.props.value === undefined
|
116
|
+
) {
|
117
|
+
return React.cloneElement(child as React.ReactElement<any>, {
|
118
|
+
value: idx,
|
119
|
+
})
|
120
|
+
}
|
121
|
+
|
122
|
+
return child
|
123
|
+
})
|
54
124
|
|
55
125
|
return (
|
56
|
-
<
|
57
|
-
<
|
126
|
+
<TabsContext.Provider value={contextValue}>
|
127
|
+
<div
|
58
128
|
{...rest}
|
59
|
-
classes={{ root: classes[orientation] }}
|
60
129
|
ref={ref}
|
61
|
-
onChange={onChange}
|
62
|
-
value={value}
|
63
|
-
action={action}
|
64
|
-
scrollButtons='auto'
|
65
|
-
ScrollButtonComponent={TabScrollButton}
|
66
|
-
orientation={orientation}
|
67
|
-
variant={variant}
|
68
130
|
data-component-type='tabs'
|
131
|
+
className={twMerge(
|
132
|
+
'relative min-h-0 flex overflow-hidden',
|
133
|
+
classesByOrientation[orientation].root,
|
134
|
+
classesByVariant[variant].root,
|
135
|
+
className
|
136
|
+
)}
|
137
|
+
aria-orientation={orientation}
|
69
138
|
>
|
70
|
-
|
71
|
-
|
72
|
-
|
139
|
+
<div
|
140
|
+
className={twJoin(
|
141
|
+
classesByVariant[variant].scroller,
|
142
|
+
classesByOrientation[orientation].scroller,
|
143
|
+
'flex-auto inline-block relative whitespace-nowrap'
|
144
|
+
)}
|
145
|
+
>
|
146
|
+
<div
|
147
|
+
className={twJoin('flex', isVertical && 'flex-col')}
|
148
|
+
role='tablist'
|
149
|
+
tabIndex={-1}
|
150
|
+
>
|
151
|
+
{childrenWithIndex}
|
152
|
+
</div>
|
153
|
+
</div>
|
154
|
+
</div>
|
155
|
+
</TabsContext.Provider>
|
73
156
|
)
|
74
157
|
}
|
75
|
-
)
|
76
|
-
|
77
|
-
|
158
|
+
)
|
159
|
+
|
160
|
+
Tabs.displayName = 'Tabs'
|
78
161
|
|
79
162
|
export default Tabs
|
@@ -0,0 +1,27 @@
|
|
1
|
+
import React from 'react'
|
2
|
+
|
3
|
+
export type TabsValueType = string | number | null
|
4
|
+
|
5
|
+
export interface TabsContextValue {
|
6
|
+
value: TabsValueType
|
7
|
+
onChange: (event: React.ChangeEvent<{}>, value: TabsValueType) => void
|
8
|
+
orientation: 'horizontal' | 'vertical'
|
9
|
+
variant: 'scrollable' | 'fullWidth'
|
10
|
+
}
|
11
|
+
|
12
|
+
export const TabsContext = React.createContext<TabsContextValue>({
|
13
|
+
value: null,
|
14
|
+
onChange: () => {},
|
15
|
+
orientation: 'horizontal',
|
16
|
+
variant: 'scrollable',
|
17
|
+
})
|
18
|
+
|
19
|
+
export const useTabsContext = () => {
|
20
|
+
const context = React.useContext(TabsContext)
|
21
|
+
|
22
|
+
if (!context) {
|
23
|
+
throw new Error('useTabsContext must be used within a TabsProvider')
|
24
|
+
}
|
25
|
+
|
26
|
+
return context
|
27
|
+
}
|