react-aria-components 3.0.0-nightly-412a51816-250219 → 3.0.0-nightly-a792c1ad5-250222

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 (80) hide show
  1. package/dist/Autocomplete.main.js +20 -10
  2. package/dist/Autocomplete.main.js.map +1 -1
  3. package/dist/Autocomplete.mjs +20 -10
  4. package/dist/Autocomplete.module.js +20 -10
  5. package/dist/Autocomplete.module.js.map +1 -1
  6. package/dist/GridList.main.js.map +1 -1
  7. package/dist/GridList.module.js.map +1 -1
  8. package/dist/Group.main.js.map +1 -1
  9. package/dist/Group.module.js.map +1 -1
  10. package/dist/ListBox.main.js +16 -16
  11. package/dist/ListBox.main.js.map +1 -1
  12. package/dist/ListBox.mjs +17 -17
  13. package/dist/ListBox.module.js +17 -17
  14. package/dist/ListBox.module.js.map +1 -1
  15. package/dist/Menu.main.js +72 -7
  16. package/dist/Menu.main.js.map +1 -1
  17. package/dist/Menu.mjs +72 -8
  18. package/dist/Menu.module.js +72 -8
  19. package/dist/Menu.module.js.map +1 -1
  20. package/dist/Popover.main.js +38 -14
  21. package/dist/Popover.main.js.map +1 -1
  22. package/dist/Popover.mjs +39 -15
  23. package/dist/Popover.module.js +39 -15
  24. package/dist/Popover.module.js.map +1 -1
  25. package/dist/SearchField.main.js +3 -1
  26. package/dist/SearchField.main.js.map +1 -1
  27. package/dist/SearchField.mjs +4 -2
  28. package/dist/SearchField.module.js +4 -2
  29. package/dist/SearchField.module.js.map +1 -1
  30. package/dist/Table.main.js +10 -38
  31. package/dist/Table.main.js.map +1 -1
  32. package/dist/Table.mjs +10 -38
  33. package/dist/Table.module.js +10 -38
  34. package/dist/Table.module.js.map +1 -1
  35. package/dist/TableLayout.main.js.map +1 -1
  36. package/dist/TableLayout.module.js.map +1 -1
  37. package/dist/TextField.main.js +3 -1
  38. package/dist/TextField.main.js.map +1 -1
  39. package/dist/TextField.mjs +4 -2
  40. package/dist/TextField.module.js +4 -2
  41. package/dist/TextField.module.js.map +1 -1
  42. package/dist/Toast.main.js +150 -0
  43. package/dist/Toast.main.js.map +1 -0
  44. package/dist/Toast.mjs +139 -0
  45. package/dist/Toast.module.js +139 -0
  46. package/dist/Toast.module.js.map +1 -0
  47. package/dist/Tree.main.js +26 -25
  48. package/dist/Tree.main.js.map +1 -1
  49. package/dist/Tree.mjs +21 -20
  50. package/dist/Tree.module.js +21 -20
  51. package/dist/Tree.module.js.map +1 -1
  52. package/dist/Virtualizer.main.js +4 -1
  53. package/dist/Virtualizer.main.js.map +1 -1
  54. package/dist/Virtualizer.mjs +4 -1
  55. package/dist/Virtualizer.module.js +4 -1
  56. package/dist/Virtualizer.module.js.map +1 -1
  57. package/dist/import.mjs +11 -5
  58. package/dist/main.js +32 -13
  59. package/dist/main.js.map +1 -1
  60. package/dist/module.js +11 -5
  61. package/dist/module.js.map +1 -1
  62. package/dist/types.d.ts +136 -28
  63. package/dist/types.d.ts.map +1 -1
  64. package/i18n/index.js +1 -1
  65. package/i18n/index.mjs +1 -1
  66. package/package.json +34 -32
  67. package/src/Autocomplete.tsx +14 -11
  68. package/src/GridList.tsx +5 -0
  69. package/src/Group.tsx +1 -0
  70. package/src/ListBox.tsx +9 -8
  71. package/src/Menu.tsx +68 -10
  72. package/src/Popover.tsx +47 -21
  73. package/src/SearchField.tsx +3 -3
  74. package/src/Table.tsx +10 -40
  75. package/src/TableLayout.ts +1 -1
  76. package/src/TextField.tsx +3 -3
  77. package/src/Toast.tsx +185 -0
  78. package/src/Tree.tsx +50 -37
  79. package/src/Virtualizer.tsx +18 -3
  80. package/src/index.ts +13 -7
package/src/Menu.tsx CHANGED
@@ -15,12 +15,12 @@ import {BaseCollection, Collection, CollectionBuilder, createBranchComponent, cr
15
15
  import {MenuTriggerProps as BaseMenuTriggerProps, Collection as ICollection, Node, TreeState, useMenuTriggerState, useTreeState} from 'react-stately';
16
16
  import {CollectionProps, CollectionRendererContext, ItemRenderProps, SectionContext, SectionProps, usePersistedKeys} from './Collection';
17
17
  import {ContextValue, DEFAULT_SLOT, Provider, RenderProps, ScrollableProps, SlotProps, StyleProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils';
18
+ import {DialogContext, OverlayTriggerStateContext} from './Dialog';
18
19
  import {filterDOMProps, mergeRefs, useObjectRef, useResizeObserver} from '@react-aria/utils';
19
20
  import {FocusStrategy, forwardRefType, HoverEvents, Key, LinkDOMProps, MultipleSelection} from '@react-types/shared';
20
21
  import {HeaderContext} from './Header';
21
22
  import {KeyboardContext} from './Keyboard';
22
23
  import {MultipleSelectionState, SelectionManager, useMultipleSelectionState} from '@react-stately/selection';
23
- import {OverlayTriggerStateContext} from './Dialog';
24
24
  import {PopoverContext} from './Popover';
25
25
  import {PressResponder, useHover} from '@react-aria/interactions';
26
26
  import React, {
@@ -70,7 +70,6 @@ export function MenuTrigger(props: MenuTriggerProps) {
70
70
  ref: ref,
71
71
  onResize: onResize
72
72
  });
73
-
74
73
  let scrollRef = useRef(null);
75
74
 
76
75
  return (
@@ -106,7 +105,7 @@ export interface SubmenuTriggerProps {
106
105
  delay?: number
107
106
  }
108
107
 
109
- const SubmenuTriggerContext = createContext<{parentMenuRef: RefObject<HTMLElement | null>} | null>(null);
108
+ const SubmenuTriggerContext = createContext<{parentMenuRef: RefObject<HTMLElement | null>, shouldUseVirtualFocus?: boolean} | null>(null);
110
109
 
111
110
  /**
112
111
  * A submenu trigger is used to wrap a submenu's trigger item and the submenu itself.
@@ -120,11 +119,12 @@ export const SubmenuTrigger = /*#__PURE__*/ createBranchComponent('submenutrigg
120
119
  let submenuTriggerState = useSubmenuTriggerState({triggerKey: item.key}, rootMenuTriggerState);
121
120
  let submenuRef = useRef<HTMLDivElement>(null);
122
121
  let itemRef = useObjectRef(ref);
123
- let {parentMenuRef} = useContext(SubmenuTriggerContext)!;
122
+ let {parentMenuRef, shouldUseVirtualFocus} = useContext(SubmenuTriggerContext)!;
124
123
  let {submenuTriggerProps, submenuProps, popoverProps} = useSubmenuTrigger({
125
124
  parentMenuRef,
126
125
  submenuRef,
127
- delay: props.delay
126
+ delay: props.delay,
127
+ shouldUseVirtualFocus
128
128
  }, submenuTriggerState, itemRef);
129
129
 
130
130
  return (
@@ -138,9 +138,62 @@ export const SubmenuTrigger = /*#__PURE__*/ createBranchComponent('submenutrigg
138
138
  trigger: 'SubmenuTrigger',
139
139
  triggerRef: itemRef,
140
140
  placement: 'end top',
141
- // Prevent parent popover from hiding submenu.
142
- // @ts-ignore
143
- 'data-react-aria-top-layer': true,
141
+ ...popoverProps
142
+ }]
143
+ ]}>
144
+ <CollectionBranch collection={state.collection} parent={item} />
145
+ {props.children[1]}
146
+ </Provider>
147
+ );
148
+ }, props => props.children[0]);
149
+
150
+ // TODO: make SubdialogTrigger unstable
151
+ export interface SubDialogTriggerProps {
152
+ /**
153
+ * The contents of the SubDialogTrigger. The first child should be an Item (the trigger) and the second child should be the Popover (for the subdialog).
154
+ */
155
+ children: ReactElement[],
156
+ /**
157
+ * The delay time in milliseconds for the subdialog to appear after hovering over the trigger.
158
+ * @default 200
159
+ */
160
+ delay?: number
161
+ }
162
+
163
+ /**
164
+ * A subdialog trigger is used to wrap a subdialog's trigger item and the subdialog itself.
165
+ *
166
+ * @version alpha
167
+ */
168
+ export const SubDialogTrigger = /*#__PURE__*/ createBranchComponent('subdialogtrigger', (props: SubDialogTriggerProps, ref: ForwardedRef<HTMLDivElement>, item) => {
169
+ let {CollectionBranch} = useContext(CollectionRendererContext);
170
+ let state = useContext(MenuStateContext)!;
171
+ let rootMenuTriggerState = useContext(RootMenuTriggerStateContext)!;
172
+ let submenuTriggerState = useSubmenuTriggerState({triggerKey: item.key}, rootMenuTriggerState);
173
+ let subdialogRef = useRef<HTMLDivElement>(null);
174
+ let itemRef = useObjectRef(ref);
175
+ let {parentMenuRef, shouldUseVirtualFocus} = useContext(SubmenuTriggerContext)!;
176
+ let {submenuTriggerProps, submenuProps, popoverProps} = useSubmenuTrigger({
177
+ parentMenuRef,
178
+ submenuRef: subdialogRef,
179
+ type: 'dialog',
180
+ delay: props.delay,
181
+ shouldUseVirtualFocus
182
+ // TODO: might need to have something like isUnavailable like we do for ContextualHelpTrigger
183
+ }, submenuTriggerState, itemRef);
184
+
185
+ return (
186
+ <Provider
187
+ values={[
188
+ [MenuItemContext, {...submenuTriggerProps, onAction: undefined, ref: itemRef}],
189
+ [DialogContext, {'aria-labelledby': submenuProps['aria-labelledby']}],
190
+ [MenuContext, submenuProps],
191
+ [OverlayTriggerStateContext, submenuTriggerState],
192
+ [PopoverContext, {
193
+ ref: subdialogRef,
194
+ trigger: 'SubDialogTrigger',
195
+ triggerRef: itemRef,
196
+ placement: 'end top',
144
197
  ...popoverProps
145
198
  }]
146
199
  ]}>
@@ -206,9 +259,14 @@ function MenuInner<T extends object>({props, collection, menuRef: ref}: MenuInne
206
259
  [MenuStateContext, state],
207
260
  [SeparatorContext, {elementType: 'div'}],
208
261
  [SectionContext, {name: 'MenuSection', render: MenuSectionInner}],
209
- [SubmenuTriggerContext, {parentMenuRef: ref}],
262
+ [SubmenuTriggerContext, {parentMenuRef: ref, shouldUseVirtualFocus: autocompleteMenuProps?.shouldUseVirtualFocus}],
210
263
  [MenuItemContext, null],
211
- [SelectionManagerContext, state.selectionManager]
264
+ [UNSTABLE_InternalAutocompleteContext, null],
265
+ [SelectionManagerContext, state.selectionManager],
266
+ /* Ensure root MenuTriggerState is defined, in case Menu is rendered outside a MenuTrigger. */
267
+ /* We assume the context can never change between defined and undefined. */
268
+ /* eslint-disable-next-line react-hooks/rules-of-hooks */
269
+ [RootMenuTriggerStateContext, triggerState ?? useMenuTriggerState({})]
212
270
  ]}>
213
271
  <CollectionRoot
214
272
  collection={state.collection}
package/src/Popover.tsx CHANGED
@@ -20,7 +20,7 @@ import {OverlayTriggerStateContext} from './Dialog';
20
20
  import React, {createContext, ForwardedRef, forwardRef, useContext, useRef, useState} from 'react';
21
21
  import {useIsHidden} from '@react-aria/collections';
22
22
 
23
- export interface PopoverProps extends Omit<PositionProps, 'isOpen'>, Omit<AriaPopoverProps, 'popoverRef' | 'triggerRef' | 'offset' | 'arrowSize'>, OverlayTriggerProps, RenderProps<PopoverRenderProps>, SlotProps {
23
+ export interface PopoverProps extends Omit<PositionProps, 'isOpen'>, Omit<AriaPopoverProps, 'popoverRef' | 'triggerRef' | 'groupRef' | 'offset' | 'arrowSize'>, OverlayTriggerProps, RenderProps<PopoverRenderProps>, SlotProps {
24
24
  /**
25
25
  * The name of the component that triggered the popover. This is reflected on the element
26
26
  * as the `data-trigger` attribute, and can be used to provide specific
@@ -80,6 +80,9 @@ export interface PopoverRenderProps {
80
80
 
81
81
  export const PopoverContext = createContext<ContextValue<PopoverProps, HTMLElement>>(null);
82
82
 
83
+ // Stores a ref for the portal container for a group of popovers (e.g. submenus).
84
+ const PopoverGroupContext = createContext<RefObject<Element | null> | null>(null);
85
+
83
86
  /**
84
87
  * A popover is an overlay element positioned relative to a trigger.
85
88
  */
@@ -137,6 +140,9 @@ function PopoverInner({state, isExiting, UNSTABLE_portalContainer, ...props}: Po
137
140
  // Referenced from: packages/@react-spectrum/tooltip/src/TooltipTrigger.tsx
138
141
  let arrowRef = useRef<HTMLDivElement>(null);
139
142
  let [arrowWidth, setArrowWidth] = useState(0);
143
+ let containerRef = useRef<HTMLDivElement | null>(null);
144
+ let groupCtx = useContext(PopoverGroupContext);
145
+ let isSubPopover = groupCtx && (props.trigger === 'SubmenuTrigger' || props.trigger === 'SubDialogTrigger');
140
146
  useLayoutEffect(() => {
141
147
  if (arrowRef.current && state.isOpen) {
142
148
  setArrowWidth(arrowRef.current.getBoundingClientRect().width);
@@ -146,7 +152,10 @@ function PopoverInner({state, isExiting, UNSTABLE_portalContainer, ...props}: Po
146
152
  let {popoverProps, underlayProps, arrowProps, placement} = usePopover({
147
153
  ...props,
148
154
  offset: props.offset ?? 8,
149
- arrowSize: arrowWidth
155
+ arrowSize: arrowWidth,
156
+ // If this is a submenu/subdialog, use the root popover's container
157
+ // to detect outside interaction and add aria-hidden.
158
+ groupRef: isSubPopover ? groupCtx! : containerRef
150
159
  }, state);
151
160
 
152
161
  let ref = props.popoverRef as RefObject<HTMLDivElement | null>;
@@ -163,27 +172,44 @@ function PopoverInner({state, isExiting, UNSTABLE_portalContainer, ...props}: Po
163
172
  });
164
173
 
165
174
  let style = {...popoverProps.style, ...renderProps.style};
175
+ let overlay = (
176
+ <div
177
+ {...mergeProps(filterDOMProps(props as any), popoverProps)}
178
+ {...renderProps}
179
+ ref={ref}
180
+ slot={props.slot || undefined}
181
+ style={style}
182
+ dir={props.dir}
183
+ data-trigger={props.trigger}
184
+ data-placement={placement}
185
+ data-entering={isEntering || undefined}
186
+ data-exiting={isExiting || undefined}>
187
+ {!props.isNonModal && <DismissButton onDismiss={state.close} />}
188
+ <OverlayArrowContext.Provider value={{...arrowProps, placement, ref: arrowRef}}>
189
+ {renderProps.children}
190
+ </OverlayArrowContext.Provider>
191
+ <DismissButton onDismiss={state.close} />
192
+ </div>
193
+ );
194
+
195
+ // If this is a root popover, render an extra div to act as the portal container for submenus/subdialogs.
196
+ if (!isSubPopover) {
197
+ return (
198
+ <Overlay {...props} isExiting={isExiting} portalContainer={UNSTABLE_portalContainer}>
199
+ {!props.isNonModal && state.isOpen && <div data-testid="underlay" {...underlayProps} style={{position: 'fixed', inset: 0}} />}
200
+ <div ref={containerRef} style={{display: 'contents'}}>
201
+ <PopoverGroupContext.Provider value={containerRef}>
202
+ {overlay}
203
+ </PopoverGroupContext.Provider>
204
+ </div>
205
+ </Overlay>
206
+ );
207
+ }
166
208
 
209
+ // Submenus/subdialogs are mounted into the root popover's container.
167
210
  return (
168
- <Overlay {...props} isExiting={isExiting} portalContainer={UNSTABLE_portalContainer}>
169
- {!props.isNonModal && state.isOpen && <div data-testid="underlay" {...underlayProps} style={{position: 'fixed', inset: 0}} />}
170
- <div
171
- {...mergeProps(filterDOMProps(props as any), popoverProps)}
172
- {...renderProps}
173
- ref={ref}
174
- slot={props.slot || undefined}
175
- style={style}
176
- dir={props.dir}
177
- data-trigger={props.trigger}
178
- data-placement={placement}
179
- data-entering={isEntering || undefined}
180
- data-exiting={isExiting || undefined}>
181
- {!props.isNonModal && <DismissButton onDismiss={state.close} />}
182
- <OverlayArrowContext.Provider value={{...arrowProps, placement, ref: arrowRef}}>
183
- {renderProps.children}
184
- </OverlayArrowContext.Provider>
185
- <DismissButton onDismiss={state.close} />
186
- </div>
211
+ <Overlay {...props} isExiting={isExiting} portalContainer={UNSTABLE_portalContainer ?? groupCtx?.current ?? undefined}>
212
+ {overlay}
187
213
  </Overlay>
188
214
  );
189
215
  }
@@ -13,14 +13,14 @@
13
13
  import {AriaSearchFieldProps, useSearchField} from 'react-aria';
14
14
  import {ButtonContext} from './Button';
15
15
  import {ContextValue, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils';
16
+ import {createHideableComponent} from '@react-aria/collections';
16
17
  import {FieldErrorContext} from './FieldError';
17
18
  import {filterDOMProps, mergeProps} from '@react-aria/utils';
18
19
  import {FormContext} from './Form';
19
- import {forwardRefType} from '@react-types/shared';
20
20
  import {GroupContext} from './Group';
21
21
  import {InputContext} from './Input';
22
22
  import {LabelContext} from './Label';
23
- import React, {createContext, ForwardedRef, forwardRef, useRef} from 'react';
23
+ import React, {createContext, ForwardedRef, useRef} from 'react';
24
24
  import {SearchFieldState, useSearchFieldState} from 'react-stately';
25
25
  import {TextContext} from './Text';
26
26
 
@@ -53,7 +53,7 @@ export const SearchFieldContext = createContext<ContextValue<SearchFieldProps, H
53
53
  /**
54
54
  * A search field allows a user to enter and clear a search query.
55
55
  */
56
- export const SearchField = /*#__PURE__*/ (forwardRef as forwardRefType)(function SearchField(props: SearchFieldProps, ref: ForwardedRef<HTMLDivElement>) {
56
+ export const SearchField = /*#__PURE__*/ createHideableComponent(function SearchField(props: SearchFieldProps, ref: ForwardedRef<HTMLDivElement>) {
57
57
  [props, ref] = useContextProps(props, ref, SearchFieldContext);
58
58
  let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
59
59
  let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
package/src/Table.tsx CHANGED
@@ -41,51 +41,21 @@ class TableCollection<T> extends BaseCollection<T> implements ITableCollection<T
41
41
 
42
42
  commit(firstKey: Key, lastKey: Key, isSSR = false) {
43
43
  this.updateColumns(isSSR);
44
- this.updateRows(isSSR);
45
- super.commit(firstKey, lastKey, isSSR);
46
- }
47
44
 
48
- private updateRows(isSSR: boolean) {
49
45
  this.rows = [];
50
- let visit = (node: Node<T>) => {
51
- if (node.hasChildNodes) {
52
- let rowHasCellWithColSpan = false;
53
- let childNodes: Iterable<GridNode<T>> = this.getChildren(node.key);
54
- for (let child of childNodes) {
55
- if (child.type === 'cell' && child.props?.colSpan !== undefined) {
56
- rowHasCellWithColSpan = true;
57
- break;
58
- }
59
- }
60
-
61
- if (rowHasCellWithColSpan) {
62
- let last: GridNode<T> | null = null;
63
- for (let child of childNodes) {
64
- child.colSpan = child.props?.colSpan;
65
- child.colspan = child.props?.colSpan;
66
- child.colIndex = !last ? child.index : (last.colIndex ?? last.index) + (last.colSpan ?? 1);
67
- last = child;
68
- }
69
-
70
- let lastColIndex = last?.colIndex ?? 0 + 1; // internally colIndex is 0 based
71
- let lastColSpan = last?.colSpan ?? 1;
72
- let numberOfCellsInRow = lastColIndex + lastColSpan;
73
-
74
- if (numberOfCellsInRow !== this.columns.length && !isSSR) {
75
- throw new Error(`Cell count must match column count. Found ${numberOfCellsInRow} cells and ${this.columns.length} columns.`);
76
- }
77
- } else {
78
- let numberOfCellsInRow = [...childNodes].length;
79
- if (numberOfCellsInRow !== this.columns.length && !isSSR) {
80
- throw new Error(`Cell count must match column count. Found ${numberOfCellsInRow} cells and ${this.columns.length} columns.`);
81
- }
46
+ for (let row of this.getChildren(this.body.key)) {
47
+ let lastChildKey = (row as CollectionNode<T>).lastChildKey;
48
+ if (lastChildKey != null) {
49
+ let lastCell = this.getItem(lastChildKey) as GridNode<T>;
50
+ let numberOfCellsInRow = (lastCell.colIndex ?? lastCell.index) + (lastCell.colSpan ?? 1);
51
+ if (numberOfCellsInRow !== this.columns.length && !isSSR) {
52
+ throw new Error(`Cell count must match column count. Found ${numberOfCellsInRow} cells and ${this.columns.length} columns.`);
82
53
  }
83
54
  }
84
- this.rows.push(node);
85
- };
86
- for (let child of this.getChildren(this.body.key)) {
87
- visit(child);
55
+ this.rows.push(row);
88
56
  }
57
+
58
+ super.commit(firstKey, lastKey, isSSR);
89
59
  }
90
60
 
91
61
  private updateColumns(isSSR: boolean) {
@@ -15,7 +15,7 @@ import {LayoutOptionsDelegate} from './Virtualizer';
15
15
  import {TableColumnResizeStateContext} from './Table';
16
16
  import {useContext, useMemo} from 'react';
17
17
 
18
- export class TableLayout<T> extends BaseTableLayout<T> implements LayoutOptionsDelegate<TableLayoutProps> {
18
+ export class TableLayout<T, O extends TableLayoutProps = TableLayoutProps> extends BaseTableLayout<T, O> implements LayoutOptionsDelegate<TableLayoutProps> {
19
19
  // Invalidate the layout whenever the column widths change.
20
20
  useLayoutOptions() {
21
21
  // This is not a React class component, just a regular class.
package/src/TextField.tsx CHANGED
@@ -12,13 +12,13 @@
12
12
 
13
13
  import {AriaTextFieldProps, useTextField} from 'react-aria';
14
14
  import {ContextValue, DOMProps, Provider, RACValidation, removeDataAttributes, RenderProps, SlotProps, useContextProps, useRenderProps, useSlot, useSlottedContext} from './utils';
15
+ import {createHideableComponent} from '@react-aria/collections';
15
16
  import {FieldErrorContext} from './FieldError';
16
17
  import {filterDOMProps, mergeProps} from '@react-aria/utils';
17
18
  import {FormContext} from './Form';
18
- import {forwardRefType} from '@react-types/shared';
19
19
  import {InputContext} from './Input';
20
20
  import {LabelContext} from './Label';
21
- import React, {createContext, ForwardedRef, forwardRef, useCallback, useRef, useState} from 'react';
21
+ import React, {createContext, ForwardedRef, useCallback, useRef, useState} from 'react';
22
22
  import {TextAreaContext} from './TextArea';
23
23
  import {TextContext} from './Text';
24
24
 
@@ -55,7 +55,7 @@ export const TextFieldContext = createContext<ContextValue<TextFieldProps, HTMLD
55
55
  /**
56
56
  * A text field allows a user to enter a plain text value with a keyboard.
57
57
  */
58
- export const TextField = /*#__PURE__*/ (forwardRef as forwardRefType)(function TextField(props: TextFieldProps, ref: ForwardedRef<HTMLDivElement>) {
58
+ export const TextField = /*#__PURE__*/ createHideableComponent(function TextField(props: TextFieldProps, ref: ForwardedRef<HTMLDivElement>) {
59
59
  [props, ref] = useContextProps(props, ref, TextFieldContext);
60
60
  let {validationBehavior: formValidationBehavior} = useSlottedContext(FormContext) || {};
61
61
  let validationBehavior = props.validationBehavior ?? formValidationBehavior ?? 'native';
package/src/Toast.tsx ADDED
@@ -0,0 +1,185 @@
1
+ /*
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import {AriaToastProps, AriaToastRegionProps, useToast, useToastRegion} from '@react-aria/toast';
14
+ import {ButtonContext} from './Button';
15
+ import {ContextValue, DEFAULT_SLOT, Provider, RenderProps, StyleRenderProps, useContextProps, useRenderProps} from './utils';
16
+ import {createPortal} from 'react-dom';
17
+ import {forwardRefType} from '@react-types/shared';
18
+ import {mergeProps, useFocusRing} from 'react-aria';
19
+ import {QueuedToast, ToastQueue, ToastState, useToastQueue} from '@react-stately/toast';
20
+ import React, {createContext, ForwardedRef, forwardRef, HTMLAttributes, JSX, ReactElement, useContext} from 'react';
21
+ import {TextContext} from './Text';
22
+ import {useObjectRef} from '@react-aria/utils';
23
+
24
+ const ToastStateContext = createContext<ToastState<any> | null>(null);
25
+
26
+ export interface ToastRegionRenderProps<T> {
27
+ /** A list of all currently visible toasts. */
28
+ visibleToasts: QueuedToast<T>[],
29
+ /**
30
+ * Whether the toast region is currently focused.
31
+ * @selector [data-focused]
32
+ */
33
+ isFocused: boolean,
34
+ /**
35
+ * Whether the toast region is keyboard focused.
36
+ * @selector [data-focus-visible]
37
+ */
38
+ isFocusVisible: boolean
39
+ }
40
+
41
+ export interface ToastRegionProps<T> extends AriaToastRegionProps, StyleRenderProps<ToastRegionRenderProps<T>> {
42
+ /** The queue of toasts to display. */
43
+ queue: ToastQueue<T>,
44
+ /** A function to render each toast. */
45
+ children: (renderProps: {toast: QueuedToast<T>}) => ReactElement
46
+ }
47
+
48
+ /**
49
+ * A ToastRegion displays one or more toast notifications.
50
+ */
51
+ export const ToastRegion = /*#__PURE__*/ (forwardRef as forwardRefType)(function ToastRegion<T>(props: ToastRegionProps<T>, ref: ForwardedRef<HTMLDivElement>): JSX.Element | null {
52
+ let state = useToastQueue(props.queue);
53
+ let objectRef = useObjectRef(ref);
54
+ let {regionProps} = useToastRegion(props, state, objectRef);
55
+
56
+ let {focusProps, isFocused, isFocusVisible} = useFocusRing();
57
+ let renderProps = useRenderProps({
58
+ ...props,
59
+ children: undefined,
60
+ defaultClassName: 'react-aria-ToastRegion',
61
+ values: {
62
+ visibleToasts: state.visibleToasts,
63
+ isFocused,
64
+ isFocusVisible
65
+ }
66
+ });
67
+
68
+ let region = (
69
+ <ToastStateContext.Provider value={state}>
70
+ <div
71
+ {...renderProps}
72
+ {...mergeProps(regionProps, focusProps)}
73
+ ref={objectRef}
74
+ data-focused={isFocused || undefined}
75
+ data-focus-visible={isFocusVisible || undefined}>
76
+ {typeof props.children === 'function' ? <ToastList {...props} style={{display: 'contents'}} /> : props.children}
77
+ </div>
78
+ </ToastStateContext.Provider>
79
+ );
80
+
81
+ return state.visibleToasts.length > 0 && typeof document !== 'undefined'
82
+ ? createPortal(region, document.body)
83
+ : null;
84
+ });
85
+
86
+ // TODO: possibly export this so additional children can be added to the region, outside the list.
87
+ const ToastList = /*#__PURE__*/ (forwardRef as forwardRefType)(function ToastList<T>(props: ToastRegionProps<T>, ref: ForwardedRef<HTMLOListElement>) {
88
+ let state = useContext(ToastStateContext)!;
89
+ return (
90
+ // @ts-ignore
91
+ <ol ref={ref} style={props.style} className={props.className}>
92
+ {state.visibleToasts.map((toast) => (
93
+ <li key={toast.key} style={{display: 'contents'}}>
94
+ {props.children({toast})}
95
+ </li>
96
+ ))}
97
+ </ol>
98
+ );
99
+ });
100
+
101
+ export interface ToastRenderProps<T> {
102
+ /**
103
+ * The toast object to display.
104
+ */
105
+ toast: QueuedToast<T>,
106
+ /**
107
+ * Whether the toast is currently focused.
108
+ * @selector [data-focused]
109
+ */
110
+ isFocused: boolean,
111
+ /**
112
+ * Whether the toast is keyboard focused.
113
+ * @selector [data-focus-visible]
114
+ */
115
+ isFocusVisible: boolean
116
+ }
117
+
118
+ export interface ToastProps<T> extends AriaToastProps<T>, RenderProps<ToastRenderProps<T>> {}
119
+
120
+ /**
121
+ * A Toast displays a brief, temporary notification of actions, errors, or other events in an application.
122
+ */
123
+ export const Toast = /*#__PURE__*/ (forwardRef as forwardRefType)(function Toast<T>(props: ToastProps<T>, ref: ForwardedRef<HTMLDivElement>) {
124
+ let state = useContext(ToastStateContext)!;
125
+ let objectRef = useObjectRef(ref);
126
+ let {toastProps, contentProps, titleProps, descriptionProps, closeButtonProps} = useToast(
127
+ props,
128
+ state,
129
+ objectRef
130
+ );
131
+
132
+ let {focusProps, isFocused, isFocusVisible} = useFocusRing();
133
+ let renderProps = useRenderProps({
134
+ ...props,
135
+ defaultClassName: 'react-aria-Toast',
136
+ values: {
137
+ toast: props.toast,
138
+ isFocused,
139
+ isFocusVisible
140
+ }
141
+ });
142
+
143
+ return (
144
+ <div
145
+ {...renderProps}
146
+ {...mergeProps(toastProps, focusProps)}
147
+ ref={objectRef}
148
+ data-focused={isFocused || undefined}
149
+ data-focus-visible={isFocusVisible || undefined}>
150
+ <Provider
151
+ values={[
152
+ [ToastContentContext, contentProps],
153
+ [TextContext, {
154
+ slots: {
155
+ [DEFAULT_SLOT]: {},
156
+ title: titleProps,
157
+ description: descriptionProps
158
+ }
159
+ }],
160
+ [ButtonContext, {
161
+ slots: {
162
+ [DEFAULT_SLOT]: {},
163
+ close: closeButtonProps
164
+ }
165
+ }]
166
+ ]}>
167
+ {renderProps.children}
168
+ </Provider>
169
+ </div>
170
+ );
171
+ });
172
+
173
+ export const ToastContentContext = createContext<ContextValue<HTMLAttributes<HTMLElement>, HTMLDivElement>>({});
174
+
175
+ /**
176
+ * ToastContent wraps the main content of a toast notification.
177
+ */
178
+ export const ToastContent = /*#__PURE__*/ forwardRef(function ToastContent(props: HTMLAttributes<HTMLElement>, ref: ForwardedRef<HTMLDivElement>) {
179
+ [props, ref] = useContextProps(props, ref, ToastContentContext);
180
+ return (
181
+ <div className="react-aria-ToastContent" {...props} ref={ref}>
182
+ {props.children}
183
+ </div>
184
+ );
185
+ });