-
Notifications
You must be signed in to change notification settings - Fork 264
Expand file tree
/
Copy pathTabsItem.tsx
More file actions
240 lines (224 loc) · 8.26 KB
/
Copy pathTabsItem.tsx
File metadata and controls
240 lines (224 loc) · 8.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
import * as React from 'react';
import {
focusRing,
slugify,
createElemPropsHook,
composeHooks,
ExtractProps,
EllipsisText,
createSubcomponent,
useModalityType,
createComponent,
} from '@workday/canvas-kit-react/common';
import {Box, FlexProps, mergeStyles} from '@workday/canvas-kit-react/layout';
import {OverflowTooltip} from '@workday/canvas-kit-react/tooltip';
import {SystemIcon, systemIconStencil} from '@workday/canvas-kit-react/icon';
import {
useListItemRegister,
useListItemRovingFocus,
isSelected,
useListItemSelect,
useOverflowListItemMeasure,
ListRenderItemContext,
} from '@workday/canvas-kit-react/collection';
import {calc, createStencil, px2rem} from '@workday/canvas-kit-styling';
import {useTabsModel} from './useTabsModel';
import {buttonStencil} from '@workday/canvas-kit-react/button';
import {system, brand, base} from '@workday/canvas-tokens-web';
export interface TabsItemProps
extends ExtractProps<typeof Box, never>,
Partial<Pick<FlexProps, 'gap'>> {
/**
* Optionally pass index to tab item. This should be done if `Tabs.Item` components were created
* via a `Array::map` function. This index will ensure keyboard navigation works even if items are
* inserted out of order.
*/
index?: number;
/**
* The contents of the tab item. This will be the accessible name of the tab for screen readers.
* Often, this is text. Icons are also supported. Using `Tabs.Icon` will render an icon that is
* not visible to screen readers and therefore the icon should not be necessary to understand the
* tab. In most circumstances, `aria-label` should not be used.
*
* ```tsx
* <Tabs.Item>First Tab</Tabs.Item>
* <Tabs.Item>
* <Tabs.Icon icon={canvasIcon} />
* <Tabs.Text>Second Tab</Tabs.Text>
* </Tabs.Item>
* ```
*/
children: React.ReactNode;
/**
* The identifier of the tab. This identifier will be used in change events and for `initialTab`.
* Must match the `data-id` of the associated tab panel. If this property is not provided, it will
* default to a string representation of the the zero-based index of the Tab when it was
* initialized.
*/
'data-id'?: string;
/**
* Optional id. If not set, it will inherit the ID passed to the `Tabs` component and append the
* index at the end. Only set this for advanced cases.
*/
id?: string;
/**
* Part of the ARIA specification for tabs. This attributes links a `role=tab` to a
* `role=tabpanel`. This value must be the same as the associated `id` attribute of the tab panel.
* This is automatically set by the component and should only be used in advanced cases.
*/
'aria-controls'?: string;
/**
* Part of the ARIA specification for tabs. Lets screen readers know which tab is active. This
* should either be `true` or `undefined` and never `false`. This is automatically set by the
* component and should only be used in advanced cases.
*/
'aria-selected'?: boolean;
/**
* Part of the ARIA specification for tabs. The currently active tab should not have a `tabIndex`
* set while all inactive tabs should have a `tabIndex={-1}`
*/
tabIndex?: number;
}
const tabItemStencil = createStencil({
base: {
...system.type.subtext.large,
fontFamily: `${system.fontFamily.default}, Helvetica Neue, Helvetica, Arial, sans-serif`,
fontWeight: system.fontWeight.medium,
border: 'none',
backgroundColor: 'transparent',
flex: '0 0 auto',
minWidth: system.space.zero,
alignItems: 'center',
padding: `${system.space.x3} ${system.space.x4}`,
height: px2rem(52),
cursor: 'pointer',
color: system.color.fg.muted.default,
position: 'relative',
borderRadius: `${system.space.x1} ${system.space.x1} ${system.space.zero} ${system.space.zero}`,
transition: 'background 150ms ease, color 150ms ease',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
[systemIconStencil.vars.color]: system.color.fg.muted.soft,
'&:has(span)': {
display: 'flex',
gap: system.space.x2,
},
'&:hover, &.hover, &:focus-visible, &.focus': {
backgroundColor: base.soap200,
color: base.blackPepper400,
[systemIconStencil.vars.color]: system.color.icon.strong,
},
'&:focus-visible, &.focus': {
// focus outline for Windows high contrast theme
outline: `${px2rem(2)} solid transparent`,
...focusRing({inset: 'outer', width: 0, separation: 2}),
[buttonStencil.vars.boxShadowInner]: system.color.border.inverse,
[buttonStencil.vars.boxShadowOuter]: brand.common.focusOutline,
[systemIconStencil.vars.color]: system.color.icon.strong,
},
'&:disabled, &.disabled, &[aria-disabled]': {
color: system.color.text.disabled,
[systemIconStencil.vars.color]: system.color.fg.disabled,
'&:hover': {
cursor: 'auto',
backgroundColor: system.color.bg.transparent,
[systemIconStencil.vars.color]: system.color.fg.disabled,
},
},
'&[aria-selected=true]': {
color: brand.primary.base,
cursor: 'default',
[systemIconStencil.vars.color]: brand.primary.base,
'&:after': {
position: 'absolute',
// selected state for Windows high contrast theme
borderBottom: `${system.space.x1} solid transparent`,
borderRadius: `${system.shape.x1} ${system.shape.x1} ${system.shape.zero} ${system.shape.zero}`,
backgroundColor: brand.primary.base,
bottom: system.space.zero,
content: `''`,
left: system.space.zero,
marginBlockStart: `${calc.negate(calc.divide(system.space.x2, system.space.x1))}`,
width: '100%',
},
'&:hover, &.hover, &:focus-visible, &.focus': {
backgroundColor: system.color.bg.transparent,
color: brand.primary.base,
},
},
},
});
export const StyledTabItem = createComponent('button')<TabsItemProps>({
displayName: 'StyledTabItem',
Component: ({children, ...elemProps}, ref, Element) => {
return (
<Element ref={ref} {...mergeStyles(elemProps, tabItemStencil())}>
{children}
</Element>
);
},
});
/**
* When the selected tab receives ArrowDown, move focus to its tab panel.
* Only applies to the selected tab; returning null from onKeyDown prevents the roving
* focus handler from also handling the key. Tabs.OverflowButton does not use this hook.
* Uses ListRenderItemContext for the item id since this hook runs before useListItemRegister
* merges in data-id.
*/
const useTabsItemFocusPanelOnArrowDown = createElemPropsHook(useTabsModel)(
({state}, _, elemProps: {'data-id'?: string} = {}) => {
const {item} = React.useContext(ListRenderItemContext);
const name = elemProps['data-id'] || item?.id || '';
const selected = !!name && isSelected(name, state);
return {
onKeyDown(event: React.KeyboardEvent<HTMLElement>) {
if (!selected || event.key !== 'ArrowDown') {
return;
}
event.preventDefault();
const panelId = slugify(`tabpanel-${state.id}-${name}`);
const panel = document.getElementById(panelId);
if (panel) {
(panel as HTMLElement).focus();
}
return null as unknown as void; // prevent roving focus from handling this key
},
};
}
);
export const useTabsItem = composeHooks(
createElemPropsHook(useTabsModel)(({state}, _, elemProps: {'data-id'?: string} = {}) => {
const name = elemProps['data-id'] || '';
const selected = !!elemProps['data-id'] && isSelected(name, state);
return {
type: 'button' as const,
role: 'tab' as const,
'aria-selected': selected,
'aria-controls': slugify(`tabpanel-${state.id}-${name}`),
};
}),
useListItemSelect,
useOverflowListItemMeasure,
useListItemRovingFocus,
useListItemRegister,
useTabsItemFocusPanelOnArrowDown
);
export const TabsItem = createSubcomponent('button')({
displayName: 'Tabs.Item',
modelHook: useTabsModel,
elemPropsHook: useTabsItem,
subComponents: {
Icon: SystemIcon,
Text: EllipsisText,
},
})<TabsItemProps>(({children, ...elemProps}, Element) => {
const modality = useModalityType();
return (
<OverflowTooltip>
<StyledTabItem as={Element} maxWidth={modality === 'touch' ? undefined : 280} {...elemProps}>
{children}
</StyledTabItem>
</OverflowTooltip>
);
});