@react-spectrum/s2 3.0.0-nightly-e228ed814-250129 → 3.0.0-nightly-016590a4a-250131

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.
Files changed (283) hide show
  1. package/dist/Accordion.cjs +3 -3
  2. package/dist/Accordion.css +2 -2
  3. package/dist/Accordion.mjs +3 -3
  4. package/dist/ActionBar.cjs +47 -47
  5. package/dist/ActionBar.css +51 -51
  6. package/dist/ActionBar.mjs +47 -47
  7. package/dist/ActionButton.cjs +97 -97
  8. package/dist/ActionButton.css +96 -96
  9. package/dist/ActionButton.mjs +97 -97
  10. package/dist/ActionButtonGroup.cjs +11 -11
  11. package/dist/ActionButtonGroup.css +9 -9
  12. package/dist/ActionButtonGroup.mjs +11 -11
  13. package/dist/AlertDialog.cjs +3 -3
  14. package/dist/AlertDialog.css +3 -3
  15. package/dist/AlertDialog.mjs +3 -3
  16. package/dist/Avatar.cjs +17 -17
  17. package/dist/Avatar.css +14 -14
  18. package/dist/Avatar.mjs +17 -17
  19. package/dist/AvatarGroup.cjs +100 -100
  20. package/dist/AvatarGroup.css +34 -34
  21. package/dist/AvatarGroup.mjs +100 -100
  22. package/dist/Badge.cjs +92 -67
  23. package/dist/Badge.cjs.map +1 -1
  24. package/dist/Badge.css +168 -92
  25. package/dist/Badge.css.map +1 -1
  26. package/dist/Badge.mjs +92 -67
  27. package/dist/Badge.mjs.map +1 -1
  28. package/dist/Breadcrumbs.cjs +115 -115
  29. package/dist/Breadcrumbs.css +89 -89
  30. package/dist/Breadcrumbs.mjs +115 -115
  31. package/dist/Button.cjs +236 -152
  32. package/dist/Button.cjs.map +1 -1
  33. package/dist/Button.css +309 -165
  34. package/dist/Button.css.map +1 -1
  35. package/dist/Button.mjs +236 -152
  36. package/dist/Button.mjs.map +1 -1
  37. package/dist/ButtonGroup.cjs +19 -19
  38. package/dist/ButtonGroup.css +15 -15
  39. package/dist/ButtonGroup.mjs +19 -19
  40. package/dist/Card.cjs +264 -261
  41. package/dist/Card.cjs.map +1 -1
  42. package/dist/Card.css +210 -198
  43. package/dist/Card.css.map +1 -1
  44. package/dist/Card.mjs +264 -261
  45. package/dist/Card.mjs.map +1 -1
  46. package/dist/CardView.cjs +15 -15
  47. package/dist/CardView.css +18 -18
  48. package/dist/CardView.mjs +15 -15
  49. package/dist/CenterBaseline.cjs +1 -1
  50. package/dist/CenterBaseline.css +2 -2
  51. package/dist/CenterBaseline.mjs +1 -1
  52. package/dist/Checkbox.cjs +124 -73
  53. package/dist/Checkbox.cjs.map +1 -1
  54. package/dist/Checkbox.css +201 -81
  55. package/dist/Checkbox.css.map +1 -1
  56. package/dist/Checkbox.mjs +124 -73
  57. package/dist/Checkbox.mjs.map +1 -1
  58. package/dist/CheckboxGroup.cjs +49 -49
  59. package/dist/CheckboxGroup.css +41 -41
  60. package/dist/CheckboxGroup.mjs +49 -49
  61. package/dist/ClearButton.cjs +15 -15
  62. package/dist/ClearButton.css +17 -17
  63. package/dist/ClearButton.mjs +15 -15
  64. package/dist/CloseButton.cjs +35 -35
  65. package/dist/CloseButton.css +33 -33
  66. package/dist/CloseButton.mjs +35 -35
  67. package/dist/ColorArea.cjs +22 -22
  68. package/dist/ColorArea.css +15 -15
  69. package/dist/ColorArea.mjs +22 -22
  70. package/dist/ColorField.cjs +38 -38
  71. package/dist/ColorField.css +32 -32
  72. package/dist/ColorField.mjs +38 -38
  73. package/dist/ColorHandle.cjs +27 -21
  74. package/dist/ColorHandle.cjs.map +1 -1
  75. package/dist/ColorHandle.css +93 -45
  76. package/dist/ColorHandle.css.map +1 -1
  77. package/dist/ColorHandle.mjs +27 -21
  78. package/dist/ColorHandle.mjs.map +1 -1
  79. package/dist/ColorSlider.cjs +52 -52
  80. package/dist/ColorSlider.css +51 -51
  81. package/dist/ColorSlider.mjs +52 -52
  82. package/dist/ColorSwatch.cjs +27 -24
  83. package/dist/ColorSwatch.cjs.map +1 -1
  84. package/dist/ColorSwatch.css +41 -29
  85. package/dist/ColorSwatch.css.map +1 -1
  86. package/dist/ColorSwatch.mjs +27 -24
  87. package/dist/ColorSwatch.mjs.map +1 -1
  88. package/dist/ColorSwatchPicker.cjs +23 -23
  89. package/dist/ColorSwatchPicker.css +60 -48
  90. package/dist/ColorSwatchPicker.css.map +1 -1
  91. package/dist/ColorSwatchPicker.mjs +23 -23
  92. package/dist/ColorWheel.cjs +22 -22
  93. package/dist/ColorWheel.css +16 -16
  94. package/dist/ColorWheel.mjs +22 -22
  95. package/dist/ComboBox.cjs +80 -80
  96. package/dist/ComboBox.css +88 -88
  97. package/dist/ComboBox.mjs +80 -80
  98. package/dist/Content.cjs.map +1 -1
  99. package/dist/Content.mjs.map +1 -1
  100. package/dist/ContextualHelp.cjs +5 -5
  101. package/dist/ContextualHelp.css +38 -38
  102. package/dist/ContextualHelp.mjs +5 -5
  103. package/dist/CustomDialog.cjs +31 -31
  104. package/dist/CustomDialog.css +25 -25
  105. package/dist/CustomDialog.mjs +31 -31
  106. package/dist/Dialog.cjs +17 -17
  107. package/dist/Dialog.css +64 -64
  108. package/dist/Dialog.mjs +17 -17
  109. package/dist/Disclosure.cjs +111 -108
  110. package/dist/Disclosure.cjs.map +1 -1
  111. package/dist/Disclosure.css +124 -112
  112. package/dist/Disclosure.css.map +1 -1
  113. package/dist/Disclosure.mjs +111 -108
  114. package/dist/Disclosure.mjs.map +1 -1
  115. package/dist/Divider.cjs +26 -26
  116. package/dist/Divider.css +16 -16
  117. package/dist/Divider.mjs +26 -26
  118. package/dist/DropZone.cjs +56 -47
  119. package/dist/DropZone.cjs.map +1 -1
  120. package/dist/DropZone.css +80 -56
  121. package/dist/DropZone.css.map +1 -1
  122. package/dist/DropZone.mjs +56 -47
  123. package/dist/DropZone.mjs.map +1 -1
  124. package/dist/Field.cjs +204 -150
  125. package/dist/Field.cjs.map +1 -1
  126. package/dist/Field.css +246 -150
  127. package/dist/Field.css.map +1 -1
  128. package/dist/Field.mjs +204 -150
  129. package/dist/Field.mjs.map +1 -1
  130. package/dist/Form.cjs +10 -10
  131. package/dist/Form.css +9 -9
  132. package/dist/Form.mjs +10 -10
  133. package/dist/FullscreenDialog.cjs +5 -5
  134. package/dist/FullscreenDialog.css +72 -72
  135. package/dist/FullscreenDialog.mjs +5 -5
  136. package/dist/IllustratedMessage.cjs +134 -134
  137. package/dist/IllustratedMessage.css +69 -69
  138. package/dist/IllustratedMessage.mjs +134 -134
  139. package/dist/Image.cjs +12 -12
  140. package/dist/Image.css +13 -13
  141. package/dist/Image.mjs +12 -12
  142. package/dist/InlineAlert.cjs +104 -77
  143. package/dist/InlineAlert.cjs.map +1 -1
  144. package/dist/InlineAlert.css +149 -77
  145. package/dist/InlineAlert.css.map +1 -1
  146. package/dist/InlineAlert.mjs +104 -77
  147. package/dist/InlineAlert.mjs.map +1 -1
  148. package/dist/Link.cjs +31 -31
  149. package/dist/Link.css +30 -30
  150. package/dist/Link.mjs +31 -31
  151. package/dist/Menu.cjs +269 -264
  152. package/dist/Menu.cjs.map +1 -1
  153. package/dist/Menu.css +152 -152
  154. package/dist/Menu.css.map +1 -1
  155. package/dist/Menu.mjs +270 -265
  156. package/dist/Menu.mjs.map +1 -1
  157. package/dist/Meter.cjs +85 -85
  158. package/dist/Meter.css +81 -81
  159. package/dist/Meter.mjs +85 -85
  160. package/dist/Modal.cjs +48 -48
  161. package/dist/Modal.css +46 -46
  162. package/dist/Modal.mjs +48 -48
  163. package/dist/NumberField.cjs +115 -115
  164. package/dist/NumberField.css +114 -114
  165. package/dist/NumberField.mjs +115 -115
  166. package/dist/Picker.cjs +193 -175
  167. package/dist/Picker.cjs.map +1 -1
  168. package/dist/Picker.css +223 -163
  169. package/dist/Picker.css.map +1 -1
  170. package/dist/Picker.mjs +193 -175
  171. package/dist/Picker.mjs.map +1 -1
  172. package/dist/Popover.cjs +84 -78
  173. package/dist/Popover.cjs.map +1 -1
  174. package/dist/Popover.css +89 -65
  175. package/dist/Popover.css.map +1 -1
  176. package/dist/Popover.mjs +84 -78
  177. package/dist/Popover.mjs.map +1 -1
  178. package/dist/ProgressBar.cjs +98 -98
  179. package/dist/ProgressBar.css +92 -92
  180. package/dist/ProgressBar.mjs +98 -98
  181. package/dist/ProgressCircle.cjs +17 -17
  182. package/dist/ProgressCircle.css +15 -15
  183. package/dist/ProgressCircle.mjs +17 -17
  184. package/dist/Provider.cjs +4 -4
  185. package/dist/Provider.css +5 -5
  186. package/dist/Provider.mjs +4 -4
  187. package/dist/Radio.cjs +152 -80
  188. package/dist/Radio.cjs.map +1 -1
  189. package/dist/Radio.css +240 -96
  190. package/dist/Radio.css.map +1 -1
  191. package/dist/Radio.mjs +152 -80
  192. package/dist/Radio.mjs.map +1 -1
  193. package/dist/RadioGroup.cjs +47 -47
  194. package/dist/RadioGroup.css +41 -41
  195. package/dist/RadioGroup.mjs +47 -47
  196. package/dist/SearchField.cjs +42 -42
  197. package/dist/SearchField.css +47 -47
  198. package/dist/SearchField.mjs +42 -42
  199. package/dist/SegmentedControl.cjs +101 -89
  200. package/dist/SegmentedControl.cjs.map +1 -1
  201. package/dist/SegmentedControl.css +140 -92
  202. package/dist/SegmentedControl.css.map +1 -1
  203. package/dist/SegmentedControl.mjs +101 -89
  204. package/dist/SegmentedControl.mjs.map +1 -1
  205. package/dist/Slider.cjs +229 -196
  206. package/dist/Slider.cjs.map +1 -1
  207. package/dist/Slider.css +221 -149
  208. package/dist/Slider.css.map +1 -1
  209. package/dist/Slider.mjs +229 -196
  210. package/dist/Slider.mjs.map +1 -1
  211. package/dist/StatusLight.cjs +56 -56
  212. package/dist/StatusLight.css +59 -59
  213. package/dist/StatusLight.mjs +56 -56
  214. package/dist/Switch.cjs +107 -74
  215. package/dist/Switch.cjs.map +1 -1
  216. package/dist/Switch.css +141 -69
  217. package/dist/Switch.css.map +1 -1
  218. package/dist/Switch.mjs +107 -74
  219. package/dist/Switch.mjs.map +1 -1
  220. package/dist/TableView.cjs +280 -253
  221. package/dist/TableView.cjs.map +1 -1
  222. package/dist/TableView.css +199 -163
  223. package/dist/TableView.css.map +1 -1
  224. package/dist/TableView.mjs +280 -253
  225. package/dist/TableView.mjs.map +1 -1
  226. package/dist/Tabs.cjs +493 -197
  227. package/dist/Tabs.cjs.map +1 -1
  228. package/dist/Tabs.css +250 -134
  229. package/dist/Tabs.css.map +1 -1
  230. package/dist/Tabs.mjs +494 -198
  231. package/dist/Tabs.mjs.map +1 -1
  232. package/dist/TabsPicker.cjs +415 -0
  233. package/dist/TabsPicker.cjs.map +1 -0
  234. package/dist/TabsPicker.css +482 -0
  235. package/dist/TabsPicker.css.map +1 -0
  236. package/dist/TabsPicker.mjs +409 -0
  237. package/dist/TabsPicker.mjs.map +1 -0
  238. package/dist/TagGroup.cjs +148 -148
  239. package/dist/TagGroup.css +134 -134
  240. package/dist/TagGroup.mjs +148 -148
  241. package/dist/TextField.cjs +59 -59
  242. package/dist/TextField.css +62 -62
  243. package/dist/TextField.mjs +59 -59
  244. package/dist/ToggleButton.cjs +3 -3
  245. package/dist/ToggleButton.css +12 -12
  246. package/dist/ToggleButton.mjs +3 -3
  247. package/dist/Tooltip.cjs +60 -57
  248. package/dist/Tooltip.cjs.map +1 -1
  249. package/dist/Tooltip.css +83 -71
  250. package/dist/Tooltip.css.map +1 -1
  251. package/dist/Tooltip.mjs +60 -57
  252. package/dist/Tooltip.mjs.map +1 -1
  253. package/dist/TreeView.cjs +465 -0
  254. package/dist/TreeView.cjs.map +1 -0
  255. package/dist/TreeView.css +632 -0
  256. package/dist/TreeView.css.map +1 -0
  257. package/dist/TreeView.mjs +455 -0
  258. package/dist/TreeView.mjs.map +1 -0
  259. package/dist/main.cjs +4 -0
  260. package/dist/main.cjs.map +1 -1
  261. package/dist/module.mjs +3 -1
  262. package/dist/module.mjs.map +1 -1
  263. package/dist/types.d.ts +33 -8
  264. package/dist/types.d.ts.map +1 -1
  265. package/icons/Skeleton.cjs +2 -2
  266. package/icons/Skeleton.css +5 -5
  267. package/icons/Skeleton.mjs +2 -2
  268. package/package.json +21 -19
  269. package/src/Badge.tsx +4 -1
  270. package/src/Content.tsx +2 -1
  271. package/src/Menu.tsx +2 -0
  272. package/src/Tabs.tsx +450 -144
  273. package/src/TabsPicker.tsx +350 -0
  274. package/src/TreeView.tsx +497 -0
  275. package/src/index.ts +2 -0
  276. package/style/__tests__/style-macro.test.js +18 -18
  277. package/style/dist/spectrum-theme.cjs +20 -10
  278. package/style/dist/spectrum-theme.cjs.map +1 -1
  279. package/style/dist/spectrum-theme.mjs +20 -10
  280. package/style/dist/spectrum-theme.mjs.map +1 -1
  281. package/style/dist/types.d.ts +4 -0
  282. package/style/dist/types.d.ts.map +1 -1
  283. package/style/spectrum-theme.ts +18 -11
package/src/Tabs.tsx CHANGED
@@ -11,28 +11,32 @@
11
11
  */
12
12
 
13
13
  import {
14
- TabListProps as AriaTabListProps,
15
- TabPanel as AriaTabPanel,
16
- TabPanelProps as AriaTabPanelProps,
17
- TabProps as AriaTabProps,
18
- TabsProps as AriaTabsProps,
19
- ContextValue,
20
- Provider,
21
- Tab as RACTab,
22
- TabList as RACTabList,
23
- Tabs as RACTabs,
24
- TabListStateContext,
25
- useSlottedContext
26
- } from 'react-aria-components';
14
+ TabListProps as AriaTabListProps,
15
+ TabPanel as AriaTabPanel,
16
+ TabPanelProps as AriaTabPanelProps,
17
+ TabProps as AriaTabProps,
18
+ TabsProps as AriaTabsProps,
19
+ ContextValue,
20
+ Group,
21
+ Provider,
22
+ Tab as RACTab,
23
+ TabList as RACTabList,
24
+ Tabs as RACTabs,
25
+ TabListStateContext
26
+ } from 'react-aria-components';
27
27
  import {centerBaseline} from './CenterBaseline';
28
- import {Collection, DOMRef, DOMRefValue, Key, Node, Orientation} from '@react-types/shared';
29
- import {createContext, forwardRef, ReactNode, useCallback, useContext, useEffect, useRef, useState} from 'react';
30
- import {focusRing, style} from '../style' with {type: 'macro'};
28
+ import {Collection, DOMRef, DOMRefValue, Key, Node, Orientation, RefObject} from '@react-types/shared';
29
+ import {CollectionBuilder} from '@react-aria/collections';
30
+ import {createContext, forwardRef, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
31
+ import {focusRing, size, style} from '../style' with {type: 'macro'};
31
32
  import {getAllowedOverrides, StyleProps, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'};
32
33
  import {IconContext} from './Icon';
34
+ import {Picker, PickerItem} from './TabsPicker';
33
35
  import {Text, TextContext} from './Content';
36
+ import {useControlledState} from '@react-stately/utils';
34
37
  import {useDOMRef} from '@react-spectrum/utils';
35
- import {useLayoutEffect} from '@react-aria/utils';
38
+ import {useEffectEvent, useId, useLabels, useLayoutEffect, useResizeObserver} from '@react-aria/utils';
39
+ import {useHasTabbableChild} from '@react-aria/focus';
36
40
  import {useLocale} from '@react-aria/i18n';
37
41
  import {useSpectrumContextProps} from './useSpectrumContextProps';
38
42
 
@@ -45,18 +49,21 @@ export interface TabsProps extends Omit<AriaTabsProps, 'className' | 'style' | '
45
49
  * The amount of space between the tabs.
46
50
  * @default 'regular'
47
51
  */
48
- density?: 'compact' | 'regular'
52
+ density?: 'compact' | 'regular',
53
+ /**
54
+ * Defines if the text within the tabs should be hidden and only the icon should be shown.
55
+ * The text is always visible when the item is collapsed into a picker.
56
+ * @default 'show'
57
+ */
58
+ labelBehavior?: 'show' | 'hide'
49
59
  }
50
60
 
51
61
  export interface TabProps extends Omit<AriaTabProps, 'children' | 'style' | 'className'>, StyleProps {
52
62
  /** The content to display in the tab. */
53
- children?: ReactNode
63
+ children: ReactNode
54
64
  }
55
65
 
56
- export interface TabListProps<T> extends Omit<AriaTabListProps<T>, 'children' | 'style' | 'className'>, StyleProps {
57
- /** The content to display in the tablist. */
58
- children?: ReactNode
59
- }
66
+ export interface TabListProps<T> extends Omit<AriaTabListProps<T>, 'style' | 'className' | 'aria-label' | 'aria-labelledby'>, StyleProps {}
60
67
 
61
68
  export interface TabPanelProps extends Omit<AriaTabPanelProps, 'children' | 'style' | 'className'>, UnsafeStyles {
62
69
  /** Spectrum-defined styles, returned by the `style()` macro. */
@@ -66,82 +73,72 @@ export interface TabPanelProps extends Omit<AriaTabPanelProps, 'children' | 'sty
66
73
  }
67
74
 
68
75
  export const TabsContext = createContext<ContextValue<TabsProps, DOMRefValue<HTMLDivElement>>>(null);
76
+ const InternalTabsContext = createContext<TabsProps>({});
77
+ const CollapseContext = createContext({
78
+ showTabs: true,
79
+ menuId: '',
80
+ valueId: ''
81
+ });
69
82
 
70
- const tabPanel = style({
71
- marginTop: 4,
72
- color: 'gray-800',
73
- flexGrow: 1,
74
- flexBasis: '[0%]',
75
- minHeight: 0,
76
- minWidth: 0
77
- }, getAllowedOverrides({height: true}));
78
-
79
- export function TabPanel(props: TabPanelProps) {
80
- return (
81
- <AriaTabPanel
82
- {...props}
83
- style={props.UNSAFE_style}
84
- className={(props.UNSAFE_className || '') + tabPanel(null, props.styles)} />
85
- );
86
- }
87
-
88
- const tab = style({
89
- ...focusRing(),
90
- display: 'flex',
91
- color: {
92
- default: 'neutral-subdued',
93
- isSelected: 'neutral',
94
- isHovered: 'neutral-subdued',
95
- isDisabled: 'disabled',
96
- forcedColors: {
97
- isSelected: 'Highlight',
98
- isDisabled: 'GrayText'
99
- }
100
- },
101
- borderRadius: 'sm',
102
- gap: 'text-to-visual',
103
- height: {
104
- density: {
105
- compact: 32,
106
- regular: 48
107
- }
108
- },
109
- alignItems: 'center',
83
+ const tabs = style({
110
84
  position: 'relative',
111
- cursor: 'default',
112
- flexShrink: 0,
113
- transition: 'default'
114
- }, getAllowedOverrides());
115
-
116
- const icon = style({
85
+ display: 'flex',
117
86
  flexShrink: 0,
118
- '--iconPrimary': {
119
- type: 'fill',
120
- value: 'currentColor'
87
+ font: 'ui',
88
+ flexDirection: {
89
+ orientation: {
90
+ horizontal: 'column'
91
+ }
121
92
  }
122
- });
93
+ }, getAllowedOverrides({height: true}));
123
94
 
124
- export function Tab(props: TabProps) {
125
- let {density} = useSlottedContext(TabsContext) ?? {};
95
+ /**
96
+ * Tabs organize content into multiple sections and allow users to navigate between them. The content under the set of tabs should be related and form a coherent unit.
97
+ */
98
+ export const Tabs = forwardRef(function Tabs(props: TabsProps, ref: DOMRef<HTMLDivElement>) {
99
+ [props, ref] = useSpectrumContextProps(props, ref, TabsContext);
100
+ let {
101
+ density = 'regular',
102
+ isDisabled,
103
+ disabledKeys,
104
+ orientation = 'horizontal',
105
+ labelBehavior = 'show'
106
+ } = props;
107
+ let domRef = useDOMRef(ref);
108
+ let [value, setValue] = useControlledState(props.selectedKey, props.defaultSelectedKey ?? null!, props.onSelectionChange);
109
+
110
+ if (!props['aria-label'] && !props['aria-labelledby']) {
111
+ throw new Error('An aria-label or aria-labelledby prop is required on Tabs for accessibility.');
112
+ }
126
113
 
127
114
  return (
128
- <RACTab
129
- {...props}
130
- style={props.UNSAFE_style}
131
- className={renderProps => (props.UNSAFE_className || '') + tab({...renderProps, density}, props.styles)}>
132
- <Provider
133
- values={[
134
- [TextContext, {styles: style({order: 1})}],
135
- [IconContext, {
136
- render: centerBaseline({slot: 'icon', styles: style({order: 0})}),
137
- styles: icon
138
- }]
139
- ]}>
140
- {typeof props.children === 'string' ? <Text>{props.children}</Text> : props.children}
141
- </Provider>
142
- </RACTab>
115
+ <Provider
116
+ values={[
117
+ [InternalTabsContext, {
118
+ density,
119
+ isDisabled,
120
+ orientation,
121
+ disabledKeys,
122
+ selectedKey: value,
123
+ onSelectionChange: setValue,
124
+ labelBehavior,
125
+ 'aria-label': props['aria-label'],
126
+ 'aria-labelledby': props['aria-labelledby']
127
+ }]
128
+ ]}>
129
+ <CollectionBuilder content={props.children}>
130
+ {collection => (
131
+ <CollapsingTabs
132
+ {...props}
133
+ selectedKey={value}
134
+ onSelectionChange={setValue}
135
+ collection={collection}
136
+ containerRef={domRef} />
137
+ )}
138
+ </CollectionBuilder>
139
+ </Provider>
143
140
  );
144
- }
141
+ });
145
142
 
146
143
  const tablist = style({
147
144
  display: 'flex',
@@ -151,6 +148,14 @@ const tablist = style({
151
148
  density: {
152
149
  compact: 24,
153
150
  regular: 32
151
+ },
152
+ labelBehavior: {
153
+ hide: {
154
+ density: {
155
+ compact: 16,
156
+ regular: 24
157
+ }
158
+ }
154
159
  }
155
160
  }
156
161
  }
@@ -175,7 +180,15 @@ const tablist = style({
175
180
  });
176
181
 
177
182
  export function TabList<T extends object>(props: TabListProps<T>) {
178
- let {density, isDisabled, disabledKeys, orientation} = useSlottedContext(TabsContext) ?? {};
183
+ let {showTabs} = useContext(CollapseContext) ?? {};
184
+
185
+ if (showTabs) {
186
+ return <TabListInner {...props} />;
187
+ }
188
+ }
189
+
190
+ function TabListInner<T extends object>(props: TabListProps<T>) {
191
+ let {density, isDisabled, disabledKeys, orientation, labelBehavior, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy} = useContext(InternalTabsContext) ?? {};
179
192
  let state = useContext(TabListStateContext);
180
193
  let [selectedTab, setSelectedTab] = useState<HTMLElement | undefined>(undefined);
181
194
  let tablistRef = useRef<HTMLDivElement>(null);
@@ -198,34 +211,16 @@ export function TabList<T extends object>(props: TabListProps<T>) {
198
211
  <TabLine disabledKeys={disabledKeys} isDisabled={isDisabled} selectedTab={selectedTab} orientation={orientation} density={density} />}
199
212
  <RACTabList
200
213
  {...props}
214
+ aria-label={ariaLabel}
215
+ aria-labelledby={ariaLabelledBy}
201
216
  ref={tablistRef}
202
- className={renderProps => tablist({...renderProps, density})} />
217
+ className={renderProps => tablist({...renderProps, labelBehavior, density})} />
203
218
  {orientation === 'horizontal' &&
204
219
  <TabLine disabledKeys={disabledKeys} isDisabled={isDisabled} selectedTab={selectedTab} orientation={orientation} density={density} />}
205
220
  </div>
206
221
  );
207
222
  }
208
223
 
209
- function isAllTabsDisabled<T>(collection: Collection<Node<T>> | null, disabledKeys: Set<Key>) {
210
- let testKey: Key | null = null;
211
- if (collection && collection.size > 0) {
212
- testKey = collection.getFirstKey();
213
-
214
- let index = 0;
215
- while (testKey && index < collection.size) {
216
- // We have to check if the item in the collection has a key in disabledKeys or has the isDisabled prop set directly on it
217
- if (!disabledKeys.has(testKey) && !collection.getItem(testKey)?.props?.isDisabled) {
218
- return false;
219
- }
220
-
221
- testKey = collection.getKeyAfter(testKey);
222
- index++;
223
- }
224
- return true;
225
- }
226
- return false;
227
- }
228
-
229
224
  interface TabLineProps {
230
225
  disabledKeys: Iterable<Key> | undefined,
231
226
  isDisabled: boolean | undefined,
@@ -279,7 +274,7 @@ function TabLine(props: TabLineProps) {
279
274
  // We want to add disabled styling to the selection indicator only if all the Tabs are disabled
280
275
  let [isDisabled, setIsDisabled] = useState<boolean>(false);
281
276
  useEffect(() => {
282
- let isDisabled = isTabsDisabled || isAllTabsDisabled(state?.collection || null, disabledKeys ? new Set(disabledKeys) : new Set(null));
277
+ let isDisabled = isTabsDisabled || isAllTabsDisabled(state?.collection, disabledKeys ? new Set(disabledKeys) : new Set(null));
283
278
  setIsDisabled(isDisabled);
284
279
  }, [state?.collection, disabledKeys, isTabsDisabled, setIsDisabled]);
285
280
 
@@ -314,50 +309,361 @@ function TabLine(props: TabLineProps) {
314
309
 
315
310
  useLayoutEffect(() => {
316
311
  onResize();
317
- }, [onResize, state?.selectedItem?.key, direction, orientation, density]);
312
+ }, [onResize, state?.selectedItem?.key, density, direction, orientation]);
318
313
 
319
314
  return (
320
315
  <div style={{...style}} className={selectedIndicator({isDisabled, orientation})} />
321
316
  );
322
317
  }
323
318
 
324
- const tabs = style({
319
+ const tab = style({
320
+ ...focusRing(),
325
321
  display: 'flex',
322
+ color: {
323
+ default: 'neutral-subdued',
324
+ isSelected: 'neutral',
325
+ isHovered: 'neutral-subdued',
326
+ isDisabled: 'disabled',
327
+ forcedColors: {
328
+ isSelected: 'Highlight',
329
+ isDisabled: 'GrayText'
330
+ }
331
+ },
332
+ borderRadius: 'sm',
333
+ gap: 'text-to-visual',
334
+ height: {
335
+ density: {
336
+ compact: 32,
337
+ regular: 48
338
+ }
339
+ },
340
+ alignItems: 'center',
341
+ position: 'relative',
342
+ cursor: 'default',
326
343
  flexShrink: 0,
327
- fontFamily: 'sans',
328
- fontWeight: 'normal',
329
- flexDirection: {
330
- orientation: {
331
- horizontal: 'column'
344
+ transition: 'default',
345
+ paddingX: {
346
+ labelBehavior: {
347
+ hide: size(6)
332
348
  }
333
349
  }
334
- }, getAllowedOverrides({height: true}));
350
+ }, getAllowedOverrides());
335
351
 
336
- /**
337
- * Tabs organize content into multiple sections and allow users to navigate between them. The content under the set of tabs should be related and form a coherent unit.
338
- */
339
- export const Tabs = forwardRef(function Tabs(props: TabsProps, ref: DOMRef<HTMLDivElement>) {
340
- [props, ref] = useSpectrumContextProps(props, ref, TabsContext);
341
- let {
342
- density = 'regular',
343
- isDisabled,
344
- disabledKeys,
345
- orientation = 'horizontal'
346
- } = props;
347
- let domRef = useDOMRef(ref);
352
+ const icon = style({
353
+ display: 'block',
354
+ flexShrink: 0,
355
+ '--iconPrimary': {
356
+ type: 'fill',
357
+ value: 'currentColor'
358
+ }
359
+ });
360
+
361
+ export function Tab(props: TabProps) {
362
+ let {density, labelBehavior} = useContext(InternalTabsContext) ?? {};
348
363
 
364
+ let contentId = useId();
365
+ let ariaLabelledBy = props['aria-labelledby'] || '';
349
366
  return (
350
- <RACTabs
367
+ <RACTab
351
368
  {...props}
352
- ref={domRef}
369
+ // @ts-ignore
370
+ originalProps={props}
371
+ aria-labelledby={`${labelBehavior === 'hide' ? contentId : ''} ${ariaLabelledBy}`}
353
372
  style={props.UNSAFE_style}
354
- className={renderProps => (props.UNSAFE_className || '') + tabs({...renderProps}, props.styles)}>
355
- <Provider
356
- values={[
357
- [TabsContext, {density, isDisabled, disabledKeys, orientation}]
358
- ]}>
373
+ className={renderProps => (props.UNSAFE_className || '') + tab({...renderProps, density, labelBehavior}, props.styles)}>
374
+ {({
375
+ // @ts-ignore
376
+ isMenu
377
+ }) => {
378
+ if (isMenu) {
379
+ return props.children;
380
+ } else {
381
+ return (
382
+ <Provider
383
+ values={[
384
+ [TextContext, {
385
+ id: contentId,
386
+ styles:
387
+ style({
388
+ order: 1,
389
+ display: {
390
+ labelBehavior: {
391
+ hide: 'none'
392
+ }
393
+ }
394
+ })({labelBehavior})
395
+ }],
396
+ [IconContext, {
397
+ render: centerBaseline({slot: 'icon', styles: style({order: 0})}),
398
+ styles: icon
399
+ }]
400
+ ]}>
401
+ {typeof props.children === 'string' ? <Text>{props.children}</Text> : props.children}
402
+ </Provider>
403
+ );
404
+ }
405
+ }}
406
+ </RACTab>
407
+ );
408
+ }
409
+
410
+ const tabPanel = style({
411
+ ...focusRing(),
412
+ display: 'flex',
413
+ marginTop: 4,
414
+ marginX: -4,
415
+ paddingX: 4,
416
+ color: 'gray-800',
417
+ flexGrow: 1,
418
+ flexShrink: 1,
419
+ flexBasis: '[0%]',
420
+ minHeight: 0,
421
+ minWidth: 0,
422
+ overflow: 'auto'
423
+ }, getAllowedOverrides({height: true}));
424
+
425
+ export function TabPanel(props: TabPanelProps) {
426
+ let {showTabs} = useContext(CollapseContext);
427
+ let {selectedKey} = useContext(InternalTabsContext);
428
+ if (showTabs) {
429
+ return (
430
+ <AriaTabPanel
431
+ {...props}
432
+ style={props.UNSAFE_style}
433
+ className={renderProps => (props.UNSAFE_className ?? '') + tabPanel(renderProps, props.styles)} />
434
+ );
435
+ }
436
+
437
+ if (props.id !== selectedKey) {
438
+ return null;
439
+ }
440
+
441
+ return <CollapsedTabPanel {...props} />;
442
+ }
443
+
444
+ function CollapsedTabPanel(props: TabPanelProps) {
445
+ let {UNSAFE_style, UNSAFE_className = '', ...otherProps} = props;
446
+ let {menuId, valueId} = useContext(CollapseContext);
447
+ let ref = useRef(null);
448
+ let tabIndex = useHasTabbableChild(ref) ? undefined : 0;
449
+
450
+ return (
451
+ <Group
452
+ {...otherProps}
453
+ ref={ref}
454
+ aria-labelledby={menuId + ' ' + valueId}
455
+ tabIndex={tabIndex}
456
+ style={UNSAFE_style}
457
+ className={renderProps => UNSAFE_className + tabPanel(renderProps, props.styles)} />
458
+ );
459
+ }
460
+
461
+ function isAllTabsDisabled<T>(collection: Collection<Node<T>> | undefined, disabledKeys: Set<Key>) {
462
+ let testKey: Key | null = null;
463
+ if (collection && collection.size > 0) {
464
+ testKey = collection.getFirstKey();
465
+
466
+ let index = 0;
467
+ while (testKey && index < collection.size) {
468
+ // We have to check if the item in the collection has a key in disabledKeys or has the isDisabled prop set directly on it
469
+ if (!disabledKeys.has(testKey) && !collection.getItem(testKey)?.props?.isDisabled) {
470
+ return false;
471
+ }
472
+
473
+ testKey = collection.getKeyAfter(testKey);
474
+ index++;
475
+ }
476
+ return true;
477
+ }
478
+ return false;
479
+ }
480
+
481
+ let HiddenTabs = function (props: {
482
+ listRef: RefObject<HTMLDivElement | null>,
483
+ items: Array<Node<any>>,
484
+ size?: string,
485
+ density?: 'compact' | 'regular'
486
+ }) {
487
+ let {listRef, items, size, density} = props;
488
+
489
+ return (
490
+ <div
491
+ // @ts-ignore
492
+ inert="true"
493
+ ref={listRef}
494
+ className={style({
495
+ display: '[inherit]',
496
+ flexDirection: '[inherit]',
497
+ gap: '[inherit]',
498
+ flexWrap: '[inherit]',
499
+ position: 'absolute',
500
+ inset: 0,
501
+ visibility: 'hidden',
502
+ overflow: 'hidden',
503
+ opacity: 0
504
+ })}>
505
+ {items.map((item) => {
506
+ // pull off individual props as an allow list, don't want refs or other props getting through
507
+ return (
508
+ <div
509
+ data-hidden-tab
510
+ style={item.props.UNSAFE_style}
511
+ key={item.key}
512
+ className={item.props.className({size, density})}>
513
+ {item.props.children({size, density})}
514
+ </div>
515
+ );
516
+ })}
517
+ </div>
518
+ );
519
+ };
520
+
521
+ let TabsMenu = (props: {valueId: string, items: Array<Node<any>>, onSelectionChange: TabsProps['onSelectionChange']} & TabsProps) => {
522
+ let {id, items, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, valueId} = props;
523
+ let {density, onSelectionChange, selectedKey, isDisabled, disabledKeys, labelBehavior} = useContext(InternalTabsContext);
524
+ let state = useContext(TabListStateContext);
525
+ let allKeysDisabled = useMemo(() => {
526
+ return isAllTabsDisabled(state?.collection, disabledKeys ? new Set(disabledKeys) : new Set());
527
+ }, [state?.collection, disabledKeys]);
528
+ let labelProps = useLabels({
529
+ id,
530
+ 'aria-label': ariaLabel,
531
+ 'aria-labelledby': ariaLabelledBy
532
+ });
533
+
534
+ return (
535
+ <div
536
+ className={style({
537
+ display: 'flex',
538
+ flexShrink: 0,
539
+ alignItems: 'center',
540
+ height: {
541
+ density: {
542
+ compact: 32,
543
+ regular: 48
544
+ }
545
+ }})({density})}>
546
+ <Picker
547
+ id={id}
548
+ valueId={valueId}
549
+ {...labelProps}
550
+ aria-describedby={props['aria-describedby']}
551
+ aria-details={props['aria-details']}
552
+ isDisabled={isDisabled || allKeysDisabled}
553
+ density={density!}
554
+ labelBehavior={labelBehavior}
555
+ items={items}
556
+ disabledKeys={disabledKeys}
557
+ selectedKey={selectedKey}
558
+ onSelectionChange={onSelectionChange}>
559
+ {(item: Node<any>) => {
560
+ return (
561
+ <PickerItem
562
+ {...item.props.originalProps}
563
+ isDisabled={isDisabled || allKeysDisabled}
564
+ key={item.key}>
565
+ {item.props.children({density, isMenu: true})}
566
+ </PickerItem>
567
+ );
568
+ }}
569
+ </Picker>
570
+ </div>
571
+ );
572
+ };
573
+
574
+ let CollapsingTabs = ({collection, containerRef, ...props}: {collection: Collection<Node<unknown>>, containerRef: any} & TabsProps) => {
575
+ let {density = 'regular', orientation = 'horizontal', labelBehavior = 'show', onSelectionChange} = props;
576
+ let [showItems, _setShowItems] = useState(true);
577
+ showItems = orientation === 'vertical' ? true : showItems;
578
+ let setShowItems = useCallback((value: boolean) => {
579
+ if (orientation === 'vertical') {
580
+ // if orientation is vertical, we always show the items
581
+ _setShowItems(true);
582
+ } else {
583
+ _setShowItems(value);
584
+ }
585
+ }, [orientation]);
586
+
587
+ let {direction} = useLocale();
588
+
589
+ let children = useMemo(() => [...collection], [collection]);
590
+
591
+ let listRef = useRef<HTMLDivElement | null>(null);
592
+ let updateOverflow = useEffectEvent(() => {
593
+ if (orientation === 'vertical' || !listRef.current || !containerRef?.current) {
594
+ return;
595
+ }
596
+ let container = listRef.current;
597
+ let containerRect = container.getBoundingClientRect();
598
+ let tabs = container.querySelectorAll('[data-hidden-tab]');
599
+ let lastTab = tabs[tabs.length - 1];
600
+ let lastTabRect = lastTab.getBoundingClientRect();
601
+ if (direction === 'ltr') {
602
+ setShowItems?.(lastTabRect.right <= containerRect.right);
603
+ } else {
604
+ setShowItems?.(lastTabRect.left >= containerRect.left);
605
+ }
606
+ });
607
+
608
+ useResizeObserver({ref: containerRef, onResize: updateOverflow});
609
+
610
+ useLayoutEffect(() => {
611
+ if (collection.size > 0) {
612
+ queueMicrotask(updateOverflow);
613
+ }
614
+ }, [collection.size, updateOverflow]);
615
+
616
+ let prevOrientation = useRef(orientation);
617
+ useLayoutEffect(() => {
618
+ if (collection.size > 0 && prevOrientation.current !== orientation) {
619
+ updateOverflow();
620
+ }
621
+ prevOrientation.current = orientation;
622
+ }, [collection.size, updateOverflow, orientation]);
623
+
624
+ useEffect(() => {
625
+ // Recalculate visible tags when fonts are loaded.
626
+ document.fonts?.ready.then(() => updateOverflow());
627
+ // eslint-disable-next-line react-hooks/exhaustive-deps
628
+ }, []);
629
+
630
+ let menuId = useId();
631
+ let valueId = useId();
632
+
633
+ let contents: ReactNode;
634
+ if (showItems) {
635
+ contents = (
636
+ <RACTabs
637
+ {...props}
638
+ style={{display: 'contents'}}>
359
639
  {props.children}
360
- </Provider>
361
- </RACTabs>
640
+ </RACTabs>
641
+ );
642
+ } else {
643
+ contents = (
644
+ <>
645
+ <TabsMenu
646
+ id={menuId}
647
+ valueId={valueId}
648
+ items={children}
649
+ onSelectionChange={onSelectionChange}
650
+ aria-label={props['aria-label']}
651
+ aria-describedby={props['aria-labelledby']} />
652
+ <CollapseContext.Provider value={{showTabs: false, menuId, valueId}}>
653
+ {props.children}
654
+ </CollapseContext.Provider>
655
+ </>
656
+ );
657
+ }
658
+
659
+ return (
660
+ <div style={props.UNSAFE_style} className={(props.UNSAFE_className || '') + tabs({orientation}, props.styles)} ref={containerRef}>
661
+ <div className={tablist({orientation, labelBehavior, density})}>
662
+ <HiddenTabs items={children} density={density} listRef={listRef} />
663
+ </div>
664
+ <CollapseContext.Provider value={{showTabs: true, menuId, valueId}}>
665
+ {contents}
666
+ </CollapseContext.Provider>
667
+ </div>
362
668
  );
363
- });
669
+ };