@octanejs/aria 0.0.4 → 0.0.5
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/package.json +7 -5
- package/src/autocomplete/useAutocomplete.ts +656 -0
- package/src/collections/BaseCollection.ts +374 -0
- package/src/collections/CollectionBuilder.ts +343 -0
- package/src/collections/Document.ts +451 -0
- package/src/collections/Hidden.ts +84 -0
- package/src/collections/useCachedChildren.ts +103 -0
- package/src/components/Autocomplete.ts +124 -0
- package/src/components/Breadcrumbs.ts +170 -0
- package/src/components/Button.ts +209 -0
- package/src/components/Checkbox.ts +593 -0
- package/src/components/Collection.ts +301 -0
- package/src/components/ComboBox.ts +477 -0
- package/src/components/Dialog.ts +195 -0
- package/src/components/Disclosure.ts +322 -0
- package/src/components/DragAndDrop.ts +177 -0
- package/src/components/FieldError.ts +79 -0
- package/src/components/Form.ts +65 -0
- package/src/components/GridList.ts +1099 -0
- package/src/components/Group.ts +146 -0
- package/src/components/Header.ts +32 -0
- package/src/components/Heading.ts +45 -0
- package/src/components/Input.ts +139 -0
- package/src/components/Keyboard.ts +23 -0
- package/src/components/Label.ts +30 -0
- package/src/components/Link.ts +137 -0
- package/src/components/ListBox.ts +886 -0
- package/src/components/Menu.ts +685 -0
- package/src/components/Meter.ts +100 -0
- package/src/components/Modal.ts +352 -0
- package/src/components/NumberField.ts +221 -0
- package/src/components/OverlayArrow.ts +117 -0
- package/src/components/Popover.ts +385 -0
- package/src/components/ProgressBar.ts +112 -0
- package/src/components/RadioGroup.ts +542 -0
- package/src/components/SearchField.ts +208 -0
- package/src/components/Select.ts +505 -0
- package/src/components/SelectionIndicator.ts +52 -0
- package/src/components/Separator.ts +97 -0
- package/src/components/SharedElementTransition.ts +209 -0
- package/src/components/Slider.ts +472 -0
- package/src/components/Switch.ts +449 -0
- package/src/components/Tabs.ts +675 -0
- package/src/components/TagGroup.ts +443 -0
- package/src/components/Text.ts +27 -0
- package/src/components/TextArea.ts +92 -0
- package/src/components/TextField.ts +200 -0
- package/src/components/ToggleButton.ts +147 -0
- package/src/components/ToggleButtonGroup.ts +106 -0
- package/src/components/Toolbar.ts +74 -0
- package/src/components/Tooltip.ts +249 -0
- package/src/components/index.ts +302 -0
- package/src/components/useDragAndDrop.ts +164 -0
- package/src/components/utils.ts +521 -0
- package/src/intl/autocomplete/ar-AE.json +3 -0
- package/src/intl/autocomplete/bg-BG.json +3 -0
- package/src/intl/autocomplete/cs-CZ.json +3 -0
- package/src/intl/autocomplete/da-DK.json +3 -0
- package/src/intl/autocomplete/de-DE.json +3 -0
- package/src/intl/autocomplete/el-GR.json +3 -0
- package/src/intl/autocomplete/en-US.json +3 -0
- package/src/intl/autocomplete/es-ES.json +3 -0
- package/src/intl/autocomplete/et-EE.json +3 -0
- package/src/intl/autocomplete/fi-FI.json +3 -0
- package/src/intl/autocomplete/fr-FR.json +3 -0
- package/src/intl/autocomplete/he-IL.json +3 -0
- package/src/intl/autocomplete/hr-HR.json +3 -0
- package/src/intl/autocomplete/hu-HU.json +3 -0
- package/src/intl/autocomplete/index.ts +75 -0
- package/src/intl/autocomplete/it-IT.json +3 -0
- package/src/intl/autocomplete/ja-JP.json +3 -0
- package/src/intl/autocomplete/ko-KR.json +3 -0
- package/src/intl/autocomplete/lt-LT.json +3 -0
- package/src/intl/autocomplete/lv-LV.json +3 -0
- package/src/intl/autocomplete/nb-NO.json +3 -0
- package/src/intl/autocomplete/nl-NL.json +3 -0
- package/src/intl/autocomplete/pl-PL.json +3 -0
- package/src/intl/autocomplete/pt-BR.json +3 -0
- package/src/intl/autocomplete/pt-PT.json +3 -0
- package/src/intl/autocomplete/ro-RO.json +3 -0
- package/src/intl/autocomplete/ru-RU.json +3 -0
- package/src/intl/autocomplete/sk-SK.json +3 -0
- package/src/intl/autocomplete/sl-SI.json +3 -0
- package/src/intl/autocomplete/sr-SP.json +3 -0
- package/src/intl/autocomplete/sv-SE.json +3 -0
- package/src/intl/autocomplete/tr-TR.json +3 -0
- package/src/intl/autocomplete/uk-UA.json +3 -0
- package/src/intl/autocomplete/zh-CN.json +3 -0
- package/src/intl/autocomplete/zh-TW.json +3 -0
- package/src/stately/autocomplete/useAutocompleteState.ts +85 -0
- package/src/utils/animation.ts +149 -0
- package/src/utils/useLoadMoreSentinel.ts +78 -0
- package/src/utils/useViewportSize.ts +123 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
// Ported from react-aria-components (source: .react-spectrum/packages/react-aria-components/src/Tabs.tsx).
|
|
2
|
+
// octane adaptations: `.tsx` → `.ts`, JSX → `createElement`; NO forwardRef — the forwarded ref
|
|
3
|
+
// is `props.ref` (Tabs passes it into `useContextProps` explicitly; TabList/TabPanels/TabPanel
|
|
4
|
+
// adapt theirs with `useObjectRef` exactly like upstream's forwarded refs); the plain-`.ts`
|
|
5
|
+
// components use the S()/subSlot component-slot convention. The collection composes the
|
|
6
|
+
// Phase-4 engine: `CollectionBuilder`/`createLeafComponent` from `../collections/
|
|
7
|
+
// CollectionBuilder`, `createHideableComponent` from `../collections/Hidden`, and the
|
|
8
|
+
// renderer's `CollectionRoot` via `CollectionRendererContext`. Upstream's RAC-local
|
|
9
|
+
// `CollectionProps` import is our `ItemCollectionProps` (see ./Collection.ts). Upstream's
|
|
10
|
+
// `inertValue` (React <19 string compat) collapses to the plain boolean — octane follows
|
|
11
|
+
// React 19 `inert` semantics. Explicit dep arrays are preserved verbatim.
|
|
12
|
+
import type {
|
|
13
|
+
AriaLabelingProps,
|
|
14
|
+
FocusEvents,
|
|
15
|
+
HoverEvents,
|
|
16
|
+
Collection as ICollection,
|
|
17
|
+
Key,
|
|
18
|
+
LinkDOMProps,
|
|
19
|
+
Node,
|
|
20
|
+
Orientation,
|
|
21
|
+
PressEvents,
|
|
22
|
+
} from '@react-types/shared';
|
|
23
|
+
import { createContext, createElement, useContext, useMemo, useRef, useState } from 'octane';
|
|
24
|
+
|
|
25
|
+
import { CollectionNode } from '../collections/BaseCollection';
|
|
26
|
+
import { CollectionBuilder, createLeafComponent } from '../collections/CollectionBuilder';
|
|
27
|
+
import { createHideableComponent } from '../collections/Hidden';
|
|
28
|
+
import { useFocusRing } from '../focus/useFocusRing';
|
|
29
|
+
import { useHover } from '../interactions/useHover';
|
|
30
|
+
import { S, subSlot } from '../internal';
|
|
31
|
+
import { type TabListState, useTabListState } from '../stately/tabs/useTabListState';
|
|
32
|
+
import { useTab } from '../tabs/useTab';
|
|
33
|
+
import { type AriaTabListProps, useTabList } from '../tabs/useTabList';
|
|
34
|
+
import { type AriaTabPanelProps, useTabPanel } from '../tabs/useTabPanel';
|
|
35
|
+
import { useEnterAnimation, useExitAnimation } from '../utils/animation';
|
|
36
|
+
import { filterDOMProps } from '../utils/filterDOMProps';
|
|
37
|
+
import { mergeProps } from '../utils/mergeProps';
|
|
38
|
+
import { useLayoutEffect } from '../utils/useLayoutEffect';
|
|
39
|
+
import { useObjectRef } from '../utils/useObjectRef';
|
|
40
|
+
import {
|
|
41
|
+
Collection,
|
|
42
|
+
CollectionRendererContext,
|
|
43
|
+
DefaultCollectionRenderer,
|
|
44
|
+
type ItemCollectionProps,
|
|
45
|
+
usePersistedKeys,
|
|
46
|
+
} from './Collection';
|
|
47
|
+
import { SelectionIndicatorContext } from './SelectionIndicator';
|
|
48
|
+
import { SharedElementTransition } from './SharedElementTransition';
|
|
49
|
+
import {
|
|
50
|
+
type ClassNameOrFunction,
|
|
51
|
+
type ContextValue,
|
|
52
|
+
dom,
|
|
53
|
+
type DOMRenderProps,
|
|
54
|
+
type PossibleLinkDOMRenderProps,
|
|
55
|
+
Provider,
|
|
56
|
+
type RenderProps,
|
|
57
|
+
type SlotProps,
|
|
58
|
+
type StyleProps,
|
|
59
|
+
type StyleRenderProps,
|
|
60
|
+
useContextProps,
|
|
61
|
+
useRenderProps,
|
|
62
|
+
useSlottedContext,
|
|
63
|
+
} from './utils';
|
|
64
|
+
|
|
65
|
+
// octane adaptation: structural bag (upstream's `GlobalDOMAttributes` drags React handler types).
|
|
66
|
+
type GlobalDOMAttributes = Record<string, any>;
|
|
67
|
+
type RefObject<T> = { current: T };
|
|
68
|
+
|
|
69
|
+
export interface TabsProps
|
|
70
|
+
extends
|
|
71
|
+
Omit<AriaTabListProps<any>, 'items' | 'children'>,
|
|
72
|
+
RenderProps<TabsRenderProps>,
|
|
73
|
+
SlotProps,
|
|
74
|
+
GlobalDOMAttributes {
|
|
75
|
+
/**
|
|
76
|
+
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
|
|
77
|
+
* element. A function may be provided to compute the class based on component state.
|
|
78
|
+
*
|
|
79
|
+
* @default 'react-aria-Tabs'
|
|
80
|
+
*/
|
|
81
|
+
className?: ClassNameOrFunction<TabsRenderProps>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface TabsRenderProps {
|
|
85
|
+
/**
|
|
86
|
+
* The orientation of the tabs.
|
|
87
|
+
*
|
|
88
|
+
* @selector [data-orientation="horizontal | vertical"]
|
|
89
|
+
*/
|
|
90
|
+
orientation: Orientation;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface TabListProps<T>
|
|
94
|
+
extends
|
|
95
|
+
StyleRenderProps<TabListRenderProps>,
|
|
96
|
+
AriaLabelingProps,
|
|
97
|
+
Omit<ItemCollectionProps<T>, 'disabledKeys'>,
|
|
98
|
+
GlobalDOMAttributes {
|
|
99
|
+
/**
|
|
100
|
+
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
|
|
101
|
+
* element. A function may be provided to compute the class based on component state.
|
|
102
|
+
*
|
|
103
|
+
* @default 'react-aria-TabList'
|
|
104
|
+
*/
|
|
105
|
+
className?: ClassNameOrFunction<TabListRenderProps>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface TabListRenderProps {
|
|
109
|
+
/**
|
|
110
|
+
* The orientation of the tab list.
|
|
111
|
+
*
|
|
112
|
+
* @selector [data-orientation="horizontal | vertical"]
|
|
113
|
+
*/
|
|
114
|
+
orientation: Orientation;
|
|
115
|
+
/**
|
|
116
|
+
* State of the tab list.
|
|
117
|
+
*/
|
|
118
|
+
state: TabListState<unknown>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface TabProps
|
|
122
|
+
extends
|
|
123
|
+
Omit<RenderProps<TabRenderProps>, 'render'>,
|
|
124
|
+
PossibleLinkDOMRenderProps<'div', TabRenderProps>,
|
|
125
|
+
AriaLabelingProps,
|
|
126
|
+
LinkDOMProps,
|
|
127
|
+
HoverEvents,
|
|
128
|
+
FocusEvents,
|
|
129
|
+
PressEvents,
|
|
130
|
+
Omit<GlobalDOMAttributes, 'onClick'> {
|
|
131
|
+
/**
|
|
132
|
+
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
|
|
133
|
+
* element. A function may be provided to compute the class based on component state.
|
|
134
|
+
*
|
|
135
|
+
* @default 'react-aria-Tab'
|
|
136
|
+
*/
|
|
137
|
+
className?: ClassNameOrFunction<TabRenderProps>;
|
|
138
|
+
/** The unique id of the tab. */
|
|
139
|
+
id?: Key;
|
|
140
|
+
/** Whether the tab is disabled. */
|
|
141
|
+
isDisabled?: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface TabRenderProps {
|
|
145
|
+
/**
|
|
146
|
+
* Whether the tab is currently hovered with a mouse.
|
|
147
|
+
*
|
|
148
|
+
* @selector [data-hovered]
|
|
149
|
+
*/
|
|
150
|
+
isHovered: boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Whether the tab is currently in a pressed state.
|
|
153
|
+
*
|
|
154
|
+
* @selector [data-pressed]
|
|
155
|
+
*/
|
|
156
|
+
isPressed: boolean;
|
|
157
|
+
/**
|
|
158
|
+
* Whether the tab is currently selected.
|
|
159
|
+
*
|
|
160
|
+
* @selector [data-selected]
|
|
161
|
+
*/
|
|
162
|
+
isSelected: boolean;
|
|
163
|
+
/**
|
|
164
|
+
* Whether the tab is currently focused.
|
|
165
|
+
*
|
|
166
|
+
* @selector [data-focused]
|
|
167
|
+
*/
|
|
168
|
+
isFocused: boolean;
|
|
169
|
+
/**
|
|
170
|
+
* Whether the tab is currently keyboard focused.
|
|
171
|
+
*
|
|
172
|
+
* @selector [data-focus-visible]
|
|
173
|
+
*/
|
|
174
|
+
isFocusVisible: boolean;
|
|
175
|
+
/**
|
|
176
|
+
* Whether the tab is disabled.
|
|
177
|
+
*
|
|
178
|
+
* @selector [data-disabled]
|
|
179
|
+
*/
|
|
180
|
+
isDisabled: boolean;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface TabPanelProps
|
|
184
|
+
extends AriaTabPanelProps, RenderProps<TabPanelRenderProps>, GlobalDOMAttributes {
|
|
185
|
+
/**
|
|
186
|
+
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
|
|
187
|
+
* element. A function may be provided to compute the class based on component state.
|
|
188
|
+
*
|
|
189
|
+
* @default 'react-aria-TabPanel'
|
|
190
|
+
*/
|
|
191
|
+
className?: ClassNameOrFunction<TabPanelRenderProps>;
|
|
192
|
+
/**
|
|
193
|
+
* Whether to mount the tab panel in the DOM even when it is not currently selected. Inactive tab
|
|
194
|
+
* panels are inert and cannot be interacted with. They must be styled appropriately so this is
|
|
195
|
+
* clear to the user visually.
|
|
196
|
+
*
|
|
197
|
+
* @default false
|
|
198
|
+
*/
|
|
199
|
+
shouldForceMount?: boolean;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface TabPanelRenderProps {
|
|
203
|
+
/**
|
|
204
|
+
* Whether the tab panel is currently focused.
|
|
205
|
+
*
|
|
206
|
+
* @selector [data-focused]
|
|
207
|
+
*/
|
|
208
|
+
isFocused: boolean;
|
|
209
|
+
/**
|
|
210
|
+
* Whether the tab panel is currently keyboard focused.
|
|
211
|
+
*
|
|
212
|
+
* @selector [data-focus-visible]
|
|
213
|
+
*/
|
|
214
|
+
isFocusVisible: boolean;
|
|
215
|
+
/**
|
|
216
|
+
* Whether the tab panel is currently non-interactive. This occurs when the
|
|
217
|
+
* `shouldForceMount` prop is true, and the corresponding tab is not selected.
|
|
218
|
+
*
|
|
219
|
+
* @selector [data-inert]
|
|
220
|
+
*/
|
|
221
|
+
isInert: boolean;
|
|
222
|
+
/**
|
|
223
|
+
* Whether the tab panel is currently entering. Use this to apply animations.
|
|
224
|
+
*
|
|
225
|
+
* @selector [data-entering]
|
|
226
|
+
*/
|
|
227
|
+
isEntering: boolean;
|
|
228
|
+
/**
|
|
229
|
+
* Whether the tab panel is currently exiting. Use this to apply animations.
|
|
230
|
+
*
|
|
231
|
+
* @selector [data-exiting]
|
|
232
|
+
*/
|
|
233
|
+
isExiting: boolean;
|
|
234
|
+
/**
|
|
235
|
+
* State of the tab list.
|
|
236
|
+
*/
|
|
237
|
+
state: TabListState<unknown>;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export const TabsContext = createContext<ContextValue<TabsProps, HTMLDivElement>>(null);
|
|
241
|
+
export const TabListStateContext = createContext<TabListState<any> | null>(null);
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Tabs organize content into multiple sections and allow users to navigate between them.
|
|
245
|
+
*/
|
|
246
|
+
export function Tabs(props: TabsProps): any {
|
|
247
|
+
const slot = S('Tabs');
|
|
248
|
+
let ref: any;
|
|
249
|
+
[props, ref] = useContextProps(props, props.ref, TabsContext, subSlot(slot, 'ctx'));
|
|
250
|
+
let { children, orientation = 'horizontal' } = props;
|
|
251
|
+
children = useMemo(
|
|
252
|
+
() =>
|
|
253
|
+
typeof children === 'function' ? children({ orientation, defaultChildren: null }) : children,
|
|
254
|
+
[children, orientation],
|
|
255
|
+
subSlot(slot, 'children'),
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
return createElement(CollectionBuilder, {
|
|
259
|
+
content: children,
|
|
260
|
+
children: (collection: ICollection<Node<any>>) =>
|
|
261
|
+
createElement(TabsInner, { props, collection, tabsRef: ref }),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
interface TabsInnerProps {
|
|
266
|
+
props: TabsProps;
|
|
267
|
+
collection: ICollection<Node<any>>;
|
|
268
|
+
tabsRef: RefObject<HTMLDivElement | null>;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function TabsInner({ props, tabsRef: ref, collection }: TabsInnerProps): any {
|
|
272
|
+
const slot = S('TabsInner');
|
|
273
|
+
let { orientation = 'horizontal' } = props;
|
|
274
|
+
let state = useTabListState(
|
|
275
|
+
{
|
|
276
|
+
...props,
|
|
277
|
+
collection,
|
|
278
|
+
children: undefined,
|
|
279
|
+
} as any,
|
|
280
|
+
subSlot(slot, 'state'),
|
|
281
|
+
);
|
|
282
|
+
let { focusProps, isFocused, isFocusVisible } = useFocusRing(
|
|
283
|
+
{ within: true },
|
|
284
|
+
subSlot(slot, 'focusRing'),
|
|
285
|
+
);
|
|
286
|
+
let values = useMemo(
|
|
287
|
+
() => ({
|
|
288
|
+
orientation,
|
|
289
|
+
isFocusWithin: isFocused,
|
|
290
|
+
isFocusVisible,
|
|
291
|
+
}),
|
|
292
|
+
[orientation, isFocused, isFocusVisible],
|
|
293
|
+
subSlot(slot, 'values'),
|
|
294
|
+
);
|
|
295
|
+
let renderProps = useRenderProps(
|
|
296
|
+
{
|
|
297
|
+
...props,
|
|
298
|
+
defaultClassName: 'react-aria-Tabs',
|
|
299
|
+
values,
|
|
300
|
+
} as any,
|
|
301
|
+
subSlot(slot, 'render'),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
let DOMProps = filterDOMProps(props, { global: true });
|
|
305
|
+
|
|
306
|
+
return createElement(dom.div, {
|
|
307
|
+
...mergeProps(DOMProps, renderProps, focusProps),
|
|
308
|
+
ref,
|
|
309
|
+
slot: props.slot || undefined,
|
|
310
|
+
'data-focused': isFocused || undefined,
|
|
311
|
+
'data-orientation': orientation,
|
|
312
|
+
'data-focus-visible': isFocusVisible || undefined,
|
|
313
|
+
'data-disabled': state.isDisabled || undefined,
|
|
314
|
+
children: createElement(Provider, {
|
|
315
|
+
values: [
|
|
316
|
+
[TabsContext, props],
|
|
317
|
+
[TabListStateContext, state],
|
|
318
|
+
] as any,
|
|
319
|
+
children: renderProps.children,
|
|
320
|
+
}),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* A TabList is used within Tabs to group tabs that a user can switch between. The ids of the items
|
|
326
|
+
* within the <TabList> must match up with a corresponding item inside the <TabPanels>.
|
|
327
|
+
*/
|
|
328
|
+
export function TabList<T extends object>(props: TabListProps<T>): any {
|
|
329
|
+
let state = useContext(TabListStateContext);
|
|
330
|
+
return state
|
|
331
|
+
? createElement(TabListInner, { props, forwardedRef: (props as any).ref })
|
|
332
|
+
: createElement(Collection, props as any);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
interface TabListInnerProps<T> {
|
|
336
|
+
props: TabListProps<T>;
|
|
337
|
+
forwardedRef: any;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function TabListInner<T extends object>({ props, forwardedRef }: TabListInnerProps<T>): any {
|
|
341
|
+
const slot = S('TabListInner');
|
|
342
|
+
let state = useContext(TabListStateContext)!;
|
|
343
|
+
let { CollectionRoot } = useContext(CollectionRendererContext);
|
|
344
|
+
let { orientation = 'horizontal', keyboardActivation = 'automatic' } =
|
|
345
|
+
useSlottedContext(TabsContext)!;
|
|
346
|
+
let objectRef = useObjectRef<HTMLDivElement>(forwardedRef, subSlot(slot, 'objectRef'));
|
|
347
|
+
|
|
348
|
+
let { tabListProps } = useTabList(
|
|
349
|
+
{
|
|
350
|
+
...props,
|
|
351
|
+
orientation,
|
|
352
|
+
keyboardActivation,
|
|
353
|
+
} as any,
|
|
354
|
+
state,
|
|
355
|
+
objectRef,
|
|
356
|
+
subSlot(slot, 'tabList'),
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
let renderProps = useRenderProps(
|
|
360
|
+
{
|
|
361
|
+
...props,
|
|
362
|
+
children: null,
|
|
363
|
+
defaultClassName: 'react-aria-TabList',
|
|
364
|
+
values: {
|
|
365
|
+
orientation,
|
|
366
|
+
state,
|
|
367
|
+
},
|
|
368
|
+
} as any,
|
|
369
|
+
subSlot(slot, 'render'),
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
let DOMProps = filterDOMProps(props, { global: true });
|
|
373
|
+
delete DOMProps.id;
|
|
374
|
+
|
|
375
|
+
let persistedKeys = usePersistedKeys(
|
|
376
|
+
state.selectionManager.focusedKey,
|
|
377
|
+
subSlot(slot, 'persisted'),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
return createElement(dom.div, {
|
|
381
|
+
...mergeProps(DOMProps, renderProps, tabListProps),
|
|
382
|
+
ref: objectRef,
|
|
383
|
+
'data-orientation': orientation || undefined,
|
|
384
|
+
children: createElement(SharedElementTransition, {
|
|
385
|
+
children: createElement(CollectionRoot, {
|
|
386
|
+
collection: state.collection,
|
|
387
|
+
persistedKeys,
|
|
388
|
+
}),
|
|
389
|
+
}),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
class TabItemNode extends CollectionNode<unknown> {
|
|
394
|
+
static readonly type = 'item';
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* A Tab provides a title for an individual item within a TabList.
|
|
399
|
+
*/
|
|
400
|
+
export const Tab: (props: TabProps & { ref?: any }) => any = createLeafComponent(
|
|
401
|
+
TabItemNode,
|
|
402
|
+
// The third (item) parameter is declared so `render.length === 3` keeps the
|
|
403
|
+
// engine's "cannot be rendered outside a collection" guard; it is always
|
|
404
|
+
// provided when rendered from a collection node.
|
|
405
|
+
(props: TabProps, forwardedRef: any, item?: Node<unknown>): any => {
|
|
406
|
+
const slot = S('Tab');
|
|
407
|
+
let state = useContext(TabListStateContext)!;
|
|
408
|
+
let ref = useObjectRef<any>(forwardedRef, subSlot(slot, 'objectRef'));
|
|
409
|
+
let { tabProps, isSelected, isDisabled, isPressed } = useTab(
|
|
410
|
+
{ key: item!.key, ...props },
|
|
411
|
+
state,
|
|
412
|
+
ref,
|
|
413
|
+
subSlot(slot, 'tab'),
|
|
414
|
+
);
|
|
415
|
+
let { focusProps, isFocused, isFocusVisible } = useFocusRing(
|
|
416
|
+
undefined,
|
|
417
|
+
subSlot(slot, 'focusRing'),
|
|
418
|
+
);
|
|
419
|
+
let { hoverProps, isHovered } = useHover(
|
|
420
|
+
{
|
|
421
|
+
isDisabled,
|
|
422
|
+
onHoverStart: props.onHoverStart,
|
|
423
|
+
onHoverEnd: props.onHoverEnd,
|
|
424
|
+
onHoverChange: props.onHoverChange,
|
|
425
|
+
},
|
|
426
|
+
subSlot(slot, 'hover'),
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
let renderProps = useRenderProps<TabRenderProps, any>(
|
|
430
|
+
{
|
|
431
|
+
...props,
|
|
432
|
+
id: undefined,
|
|
433
|
+
children: item!.rendered,
|
|
434
|
+
defaultClassName: 'react-aria-Tab',
|
|
435
|
+
values: {
|
|
436
|
+
isSelected,
|
|
437
|
+
isDisabled,
|
|
438
|
+
isFocused,
|
|
439
|
+
isFocusVisible,
|
|
440
|
+
isPressed,
|
|
441
|
+
isHovered,
|
|
442
|
+
},
|
|
443
|
+
} as any,
|
|
444
|
+
subSlot(slot, 'render'),
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
let ElementType = item!.props.href ? dom.a : dom.div;
|
|
448
|
+
let DOMProps = filterDOMProps(props as any, { global: true });
|
|
449
|
+
delete DOMProps.id;
|
|
450
|
+
delete DOMProps.onClick;
|
|
451
|
+
|
|
452
|
+
return createElement(ElementType, {
|
|
453
|
+
...mergeProps(DOMProps, renderProps, tabProps, focusProps, hoverProps),
|
|
454
|
+
ref,
|
|
455
|
+
'data-selected': isSelected || undefined,
|
|
456
|
+
'data-disabled': isDisabled || undefined,
|
|
457
|
+
'data-focused': isFocused || undefined,
|
|
458
|
+
'data-focus-visible': isFocusVisible || undefined,
|
|
459
|
+
'data-pressed': isPressed || undefined,
|
|
460
|
+
'data-hovered': isHovered || undefined,
|
|
461
|
+
children: createElement(SelectionIndicatorContext.Provider, {
|
|
462
|
+
value: { isSelected },
|
|
463
|
+
children: renderProps.children,
|
|
464
|
+
}),
|
|
465
|
+
});
|
|
466
|
+
},
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
export interface TabPanelsProps<T>
|
|
470
|
+
extends
|
|
471
|
+
Omit<ItemCollectionProps<T>, 'disabledKeys'>,
|
|
472
|
+
StyleProps,
|
|
473
|
+
DOMRenderProps<'div', undefined>,
|
|
474
|
+
GlobalDOMAttributes {
|
|
475
|
+
/**
|
|
476
|
+
* The CSS [className](https://developer.mozilla.org/en-US/docs/Web/API/Element/className) for the
|
|
477
|
+
* element.
|
|
478
|
+
*
|
|
479
|
+
* @default 'react-aria-TabPanels'
|
|
480
|
+
*/
|
|
481
|
+
className?: string;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Groups multiple `<TabPanel>` elements, and provides CSS variables for animated transitions.
|
|
486
|
+
*/
|
|
487
|
+
export const TabPanels: <T extends object>(props: TabPanelsProps<T> & { ref?: any }) => any =
|
|
488
|
+
createHideableComponent(function TabPanels(props: TabPanelsProps<any>, forwardedRef: any): any {
|
|
489
|
+
const slot = S('TabPanels');
|
|
490
|
+
let state = useContext(TabListStateContext)!;
|
|
491
|
+
let ref = useObjectRef<HTMLDivElement>(forwardedRef, subSlot(slot, 'objectRef'));
|
|
492
|
+
|
|
493
|
+
let selectedKeyRef = useRef(state.selectedKey, subSlot(slot, 'selectedKey'));
|
|
494
|
+
let prevSize = useRef<DOMRect | null>(null, subSlot(slot, 'prevSize'));
|
|
495
|
+
let hasTransition = useRef<boolean | null>(null, subSlot(slot, 'hasTransition'));
|
|
496
|
+
useLayoutEffect(
|
|
497
|
+
() => {
|
|
498
|
+
let el = ref.current;
|
|
499
|
+
if (!el) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (hasTransition.current == null) {
|
|
504
|
+
hasTransition.current = /width|height|block-size|inline-size|all/.test(
|
|
505
|
+
window.getComputedStyle(el).transition,
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (
|
|
510
|
+
hasTransition.current &&
|
|
511
|
+
selectedKeyRef.current != null &&
|
|
512
|
+
selectedKeyRef.current !== state.selectedKey
|
|
513
|
+
) {
|
|
514
|
+
// Measure auto size.
|
|
515
|
+
el.style.setProperty('--tab-panel-width', 'auto');
|
|
516
|
+
el.style.setProperty('--tab-panel-height', 'auto');
|
|
517
|
+
let { width, height } = el.getBoundingClientRect();
|
|
518
|
+
|
|
519
|
+
if (
|
|
520
|
+
prevSize.current &&
|
|
521
|
+
(prevSize.current.width !== width || prevSize.current.height !== height)
|
|
522
|
+
) {
|
|
523
|
+
// Revert to previous size.
|
|
524
|
+
el.style.setProperty('--tab-panel-width', prevSize.current.width + 'px');
|
|
525
|
+
el.style.setProperty('--tab-panel-height', prevSize.current.height + 'px');
|
|
526
|
+
|
|
527
|
+
// Force style re-calculation to trigger animations.
|
|
528
|
+
window.getComputedStyle(el).height;
|
|
529
|
+
|
|
530
|
+
// Animate to current pixel size.
|
|
531
|
+
el.style.setProperty('--tab-panel-width', width + 'px');
|
|
532
|
+
el.style.setProperty('--tab-panel-height', height + 'px');
|
|
533
|
+
|
|
534
|
+
// When animations complete, revert back to auto size.
|
|
535
|
+
Promise.all(el.getAnimations().map((a) => a.finished))
|
|
536
|
+
.then(() => {
|
|
537
|
+
el.style.setProperty('--tab-panel-width', 'auto');
|
|
538
|
+
el.style.setProperty('--tab-panel-height', 'auto');
|
|
539
|
+
})
|
|
540
|
+
.catch(() => {});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
selectedKeyRef.current = state.selectedKey;
|
|
545
|
+
},
|
|
546
|
+
[ref, state.selectedKey],
|
|
547
|
+
subSlot(slot, 'measure'),
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
// Store previous size before DOM updates occur.
|
|
551
|
+
// This breaks the rules of hooks because there is no effect that runs _before_ DOM updates.
|
|
552
|
+
if (
|
|
553
|
+
state.selectedKey != null &&
|
|
554
|
+
state.selectedKey !== selectedKeyRef.current &&
|
|
555
|
+
ref.current &&
|
|
556
|
+
hasTransition.current
|
|
557
|
+
) {
|
|
558
|
+
prevSize.current = ref.current.getBoundingClientRect();
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
let DOMProps = filterDOMProps(props, { labelable: true, global: true });
|
|
562
|
+
delete DOMProps.id;
|
|
563
|
+
|
|
564
|
+
return createElement(dom.div, {
|
|
565
|
+
render: props.render,
|
|
566
|
+
...DOMProps,
|
|
567
|
+
ref,
|
|
568
|
+
style: props.style,
|
|
569
|
+
className: props.className || 'react-aria-TabPanels',
|
|
570
|
+
children: createElement(Collection, props as any),
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* A TabPanel provides the content for a tab.
|
|
576
|
+
*/
|
|
577
|
+
export const TabPanel: (props: TabPanelProps & { ref?: any }) => any = createHideableComponent(
|
|
578
|
+
function TabPanel(props: TabPanelProps, forwardedRef: any): any {
|
|
579
|
+
const slot = S('TabPanel');
|
|
580
|
+
const state = useContext(TabListStateContext)!;
|
|
581
|
+
let ref = useObjectRef<HTMLDivElement>(forwardedRef, subSlot(slot, 'objectRef'));
|
|
582
|
+
|
|
583
|
+
// Track if the tab panel was initially selected on mount (after extra render to populate the collection).
|
|
584
|
+
// In this case, we don't want to trigger animations.
|
|
585
|
+
let isSelected = state.selectedKey === props.id;
|
|
586
|
+
let [isInitiallySelected, setInitiallySelected] = useState<boolean | null>(
|
|
587
|
+
state.selectedKey != null ? isSelected : null,
|
|
588
|
+
subSlot(slot, 'initiallySelected'),
|
|
589
|
+
);
|
|
590
|
+
if (isInitiallySelected == null && state.selectedKey != null) {
|
|
591
|
+
setInitiallySelected(isSelected);
|
|
592
|
+
} else if (!isSelected && isInitiallySelected) {
|
|
593
|
+
setInitiallySelected(false);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
let isExiting = useExitAnimation(ref, isSelected, subSlot(slot, 'exit'));
|
|
597
|
+
if (!isSelected && !props.shouldForceMount && !isExiting) {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return createElement(TabPanelInner, {
|
|
602
|
+
...props,
|
|
603
|
+
tabPanelRef: ref,
|
|
604
|
+
isInitiallySelected: isInitiallySelected || false,
|
|
605
|
+
isExiting,
|
|
606
|
+
});
|
|
607
|
+
},
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
function TabPanelInner(
|
|
611
|
+
props: TabPanelProps & {
|
|
612
|
+
tabPanelRef: RefObject<HTMLDivElement | null>;
|
|
613
|
+
isInitiallySelected: boolean;
|
|
614
|
+
isExiting: boolean;
|
|
615
|
+
},
|
|
616
|
+
): any {
|
|
617
|
+
const slot = S('TabPanelInner');
|
|
618
|
+
let state = useContext(TabListStateContext)!;
|
|
619
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
620
|
+
let { id, tabPanelRef: ref, isInitiallySelected, isExiting, ...otherProps } = props;
|
|
621
|
+
let { tabPanelProps } = useTabPanel(props, state, ref, subSlot(slot, 'tabPanel'));
|
|
622
|
+
let { focusProps, isFocused, isFocusVisible } = useFocusRing(
|
|
623
|
+
undefined,
|
|
624
|
+
subSlot(slot, 'focusRing'),
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
let isSelected = state.selectedKey === props.id;
|
|
628
|
+
let isEntering =
|
|
629
|
+
useEnterAnimation(ref, undefined, subSlot(slot, 'enter')) && !isInitiallySelected;
|
|
630
|
+
let renderProps = useRenderProps(
|
|
631
|
+
{
|
|
632
|
+
...props,
|
|
633
|
+
defaultClassName: 'react-aria-TabPanel',
|
|
634
|
+
values: {
|
|
635
|
+
isFocused,
|
|
636
|
+
isFocusVisible,
|
|
637
|
+
// octane adaptation: upstream `inertValue` is React <19 string compat; octane
|
|
638
|
+
// follows React 19 boolean `inert` semantics.
|
|
639
|
+
isInert: !isSelected,
|
|
640
|
+
isEntering,
|
|
641
|
+
isExiting,
|
|
642
|
+
state,
|
|
643
|
+
},
|
|
644
|
+
} as any,
|
|
645
|
+
subSlot(slot, 'render'),
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
let DOMProps = filterDOMProps(otherProps, { global: true });
|
|
649
|
+
delete DOMProps.id;
|
|
650
|
+
|
|
651
|
+
let domProps = isSelected
|
|
652
|
+
? mergeProps(DOMProps, tabPanelProps, focusProps, renderProps)
|
|
653
|
+
: mergeProps(DOMProps, renderProps);
|
|
654
|
+
|
|
655
|
+
return createElement(dom.div, {
|
|
656
|
+
...domProps,
|
|
657
|
+
ref,
|
|
658
|
+
'data-focused': isFocused || undefined,
|
|
659
|
+
'data-focus-visible': isFocusVisible || undefined,
|
|
660
|
+
inert: !isSelected || (props as any).inert || undefined,
|
|
661
|
+
'data-inert': !isSelected ? 'true' : undefined,
|
|
662
|
+
'data-entering': isEntering || undefined,
|
|
663
|
+
'data-exiting': isExiting || undefined,
|
|
664
|
+
children: createElement(Provider, {
|
|
665
|
+
values: [
|
|
666
|
+
[TabsContext, null],
|
|
667
|
+
[TabListStateContext, null],
|
|
668
|
+
] as any,
|
|
669
|
+
children: createElement(CollectionRendererContext.Provider, {
|
|
670
|
+
value: DefaultCollectionRenderer,
|
|
671
|
+
children: renderProps.children,
|
|
672
|
+
}),
|
|
673
|
+
}),
|
|
674
|
+
});
|
|
675
|
+
}
|