From 8f94fdd7422a241a5a06332c46fa626a6dce98d9 Mon Sep 17 00:00:00 2001 From: Mohamed Akram Date: Fri, 18 Jul 2025 22:49:01 +0400 Subject: [PATCH] fix: context provider values not memoized --- eslint.config.mjs | 3 +- .../dnd/stories/VirtualizedListBox.tsx | 4 +- .../interactions/src/PressResponder.tsx | 4 +- .../interactions/src/useFocusable.tsx | 16 +++-- .../overlays/src/PortalProvider.tsx | 9 ++- .../accordion/src/Accordion.tsx | 11 +++- .../@react-spectrum/card/src/CardView.tsx | 7 +- .../card/stories/Card.stories.tsx | 5 +- .../color/src/ColorSwatchPicker.tsx | 6 +- .../dialog/src/DialogContainer.tsx | 6 +- .../dialog/src/DialogTrigger.tsx | 8 +-- packages/@react-spectrum/form/src/Form.tsx | 10 +-- .../@react-spectrum/list/src/ListView.tsx | 26 +++++++- .../listbox/src/ListBoxBase.tsx | 9 ++- .../menu/src/ContextualHelpTrigger.tsx | 8 ++- packages/@react-spectrum/menu/src/Menu.tsx | 13 +++- .../@react-spectrum/menu/src/MenuTrigger.tsx | 6 +- .../menu/src/SubmenuTrigger.tsx | 54 ++++++++-------- .../@react-spectrum/provider/src/Provider.tsx | 47 +++++++++----- .../@react-spectrum/radio/src/RadioGroup.tsx | 9 ++- packages/@react-spectrum/s2/src/Accordion.tsx | 9 ++- .../@react-spectrum/s2/src/AvatarGroup.tsx | 8 ++- .../@react-spectrum/s2/src/Breadcrumbs.tsx | 7 +- packages/@react-spectrum/s2/src/Card.tsx | 18 +++++- .../@react-spectrum/s2/src/CheckboxGroup.tsx | 12 +++- .../s2/src/ColorSwatchPicker.tsx | 6 +- packages/@react-spectrum/s2/src/ComboBox.tsx | 5 +- .../s2/src/DialogContainer.tsx | 15 +++-- packages/@react-spectrum/s2/src/DropZone.tsx | 1 + packages/@react-spectrum/s2/src/Field.tsx | 14 ++-- packages/@react-spectrum/s2/src/Form.tsx | 28 +++++--- .../s2/src/ImageCoordinator.tsx | 9 ++- packages/@react-spectrum/s2/src/Menu.tsx | 18 ++++-- .../@react-spectrum/s2/src/NumberField.tsx | 1 + packages/@react-spectrum/s2/src/Picker.tsx | 13 ++-- .../@react-spectrum/s2/src/RadioGroup.tsx | 8 ++- packages/@react-spectrum/s2/src/Tabs.tsx | 8 ++- .../@react-spectrum/s2/src/TabsPicker.tsx | 22 ++++--- packages/@react-spectrum/s2/src/TagGroup.tsx | 9 ++- packages/@react-spectrum/s2/src/TextField.tsx | 1 + packages/@react-spectrum/s2/src/Tooltip.tsx | 18 +++--- packages/@react-spectrum/s2/src/TreeView.tsx | 9 ++- .../table/src/TableViewBase.tsx | 64 +++++++++++++------ packages/@react-spectrum/tabs/src/Tabs.tsx | 47 +++++++++----- .../tooltip/src/TooltipTrigger.tsx | 22 ++++--- .../@react-spectrum/tree/src/TreeView.tsx | 6 +- .../utils/src/BreakpointProvider.tsx | 5 +- .../pages/react-aria/home/MouseAnimation.tsx | 6 +- packages/dev/docs/src/Layout.js | 8 ++- packages/dev/docs/src/types.js | 12 ++-- packages/dev/s2-docs/src/TypePopover.tsx | 15 +++-- .../dev/s2-docs/src/VisualExampleClient.tsx | 28 ++++---- packages/react-aria-components/src/Button.tsx | 6 +- .../react-aria-components/src/Calendar.tsx | 8 ++- .../src/ColorSwatchPicker.tsx | 4 +- .../react-aria-components/src/ComboBox.tsx | 5 +- packages/react-aria-components/src/Form.tsx | 8 ++- .../react-aria-components/src/ListBox.tsx | 4 +- packages/react-aria-components/src/Meter.tsx | 6 +- .../react-aria-components/src/Popover.tsx | 3 +- .../react-aria-components/src/ProgressBar.tsx | 6 +- .../react-aria-components/src/Tooltip.tsx | 6 +- .../react-aria-components/src/Virtualizer.tsx | 4 +- 63 files changed, 514 insertions(+), 259 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 8caaa872e68..d1e85930754 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -193,6 +193,7 @@ export default [{ "react/display-name": OFF, "react/jsx-curly-spacing": [ERROR, "never"], "react/jsx-indent-props": [ERROR, ERROR], + "react/jsx-no-constructed-context-values": ERROR, "react/jsx-no-duplicate-props": ERROR, "react/jsx-no-literals": OFF, "react/jsx-no-undef": ERROR, @@ -504,4 +505,4 @@ export default [{ rules: { "react/react-in-jsx-scope": OFF, }, -}]; \ No newline at end of file +}]; diff --git a/packages/@react-aria/dnd/stories/VirtualizedListBox.tsx b/packages/@react-aria/dnd/stories/VirtualizedListBox.tsx index 4e13a2c01ad..0776a486de6 100644 --- a/packages/@react-aria/dnd/stories/VirtualizedListBox.tsx +++ b/packages/@react-aria/dnd/stories/VirtualizedListBox.tsx @@ -170,8 +170,10 @@ React.forwardRef(function (props: any, ref) { let focusedKey = dropState.target?.type === 'item' ? dropState.target.key : state.selectionManager.focusedKey; let persistedKeys = useMemo(() => focusedKey != null ? new Set([focusedKey]) : null, [focusedKey]); + const context = useMemo(() => ({state, dropState}), [dropState, state]); + return ( - + mergeProps(prevContext || {}, { ...props, ref, register() { @@ -35,7 +35,7 @@ React.forwardRef(({children, ...props}: PressResponderProps, ref: ForwardedRef): Focusable export const FocusableProvider: React.ForwardRefExoticComponent> = React.forwardRef(function FocusableProvider(props: FocusableProviderProps, ref: ForwardedRef) { - let {children, ...otherProps} = props; + let {children} = props; let objRef = useObjectRef(ref); - let context = { - ...otherProps, - ref: objRef - }; + let context = useMemo(() => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const {children: _, ...otherProps} = props; + return { + ...otherProps, + ref: objRef + }; + }, [objRef, props]); return ( diff --git a/packages/@react-aria/overlays/src/PortalProvider.tsx b/packages/@react-aria/overlays/src/PortalProvider.tsx index 1105191af35..7451ebb104b 100644 --- a/packages/@react-aria/overlays/src/PortalProvider.tsx +++ b/packages/@react-aria/overlays/src/PortalProvider.tsx @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import React, {createContext, JSX, ReactNode, useContext} from 'react'; +import React, {createContext, JSX, ReactNode, useContext, useMemo} from 'react'; export interface PortalProviderProps { /** Should return the element where we should portal to. Can clear the context by passing null. */ @@ -29,8 +29,13 @@ export const PortalContext: React.Context = createCo export function UNSAFE_PortalProvider(props: PortalProviderProps): JSX.Element { let {getContainer} = props; let {getContainer: ctxGetContainer} = useUNSAFE_PortalContext(); + + const context = useMemo(() => ({ + getContainer: getContainer === null ? undefined : getContainer ?? ctxGetContainer + }), [ctxGetContainer, getContainer]); + return ( - + {props.children} ); diff --git a/packages/@react-spectrum/accordion/src/Accordion.tsx b/packages/@react-spectrum/accordion/src/Accordion.tsx index 1344ca0cff1..98a1b6fffc6 100644 --- a/packages/@react-spectrum/accordion/src/Accordion.tsx +++ b/packages/@react-spectrum/accordion/src/Accordion.tsx @@ -15,7 +15,7 @@ import {Button, DisclosureGroup, DisclosureGroupProps, DisclosurePanelProps, Dis import ChevronLeftMedium from '@spectrum-icons/ui/ChevronLeftMedium'; import ChevronRightMedium from '@spectrum-icons/ui/ChevronRightMedium'; import {classNames, useDOMRef, useStyleProps} from '@react-spectrum/utils'; -import React, {createContext, forwardRef} from 'react'; +import React, {createContext, forwardRef, useMemo} from 'react'; import styles from '@adobe/spectrum-css-temp/components/accordion/vars.css'; import {useLocale} from '@react-aria/i18n'; import {useProviderProps} from '@react-spectrum/provider'; @@ -34,8 +34,13 @@ export const Accordion = /*#__PURE__*/(forwardRef as forwardRefType)(function Ac props = useProviderProps(props); let {styleProps} = useStyleProps(props); let domRef = useDOMRef(ref); + + const context = useMemo(() => ({ + isQuiet: props.isQuiet || false} + ), [props.isQuiet]); + return ( - + , 'role'>} - className={classNames(styles, 'spectrum-Accordion-itemContent', styleProps.className)} + className={classNames(styles, 'spectrum-Accordion-itemContent', styleProps.className)} {...props}> {props.children} diff --git a/packages/@react-spectrum/card/src/CardView.tsx b/packages/@react-spectrum/card/src/CardView.tsx index e060a2e84a4..10458800cf7 100644 --- a/packages/@react-spectrum/card/src/CardView.tsx +++ b/packages/@react-spectrum/card/src/CardView.tsx @@ -109,8 +109,13 @@ export const CardView = React.forwardRef(function CardView(pro let persistedKeys = useMemo(() => focusedKey != null ? new Set([focusedKey]) : null, [focusedKey]); // TODO: does aria-row count and aria-col count need to be modified? Perhaps aria-col count needs to be omitted + + const context = useMemo(() => ({ + state, isQuiet, layout: cardViewLayout, cardOrientation, renderEmptyState + }), [cardOrientation, cardViewLayout, isQuiet, renderEmptyState, state]); + return ( - + { isSelected: () => !prev.selectionManager.isSelected() } }))}); + const context = useMemo(() => ({state}), [state]); return (
- +
diff --git a/packages/@react-spectrum/color/src/ColorSwatchPicker.tsx b/packages/@react-spectrum/color/src/ColorSwatchPicker.tsx index 320e8fb0fdc..8e5abd92930 100644 --- a/packages/@react-spectrum/color/src/ColorSwatchPicker.tsx +++ b/packages/@react-spectrum/color/src/ColorSwatchPicker.tsx @@ -13,7 +13,7 @@ import {ColorSwatchPicker as AriaColorSwatchPicker, ColorSwatchPickerItem as AriaColorSwatchPickerItem} from 'react-aria-components'; import {Color} from '@react-types/color'; import {DOMRef, StyleProps, ValueBase} from '@react-types/shared'; -import React, {forwardRef, ReactElement, ReactNode} from 'react'; +import React, {forwardRef, ReactElement, ReactNode, useMemo} from 'react'; import {SpectrumColorSwatchContext, SpectrumColorSwatchProps} from './ColorSwatch'; import {style} from '@react-spectrum/style-macro-s1' with {type: 'macro'}; import {useDOMRef, useStyleProps} from '@react-spectrum/utils'; @@ -51,6 +51,8 @@ export const ColorSwatchPicker = forwardRef(function ColorSwatchPicker(props: Sp let {styleProps} = useStyleProps(props); let domRef = useDOMRef(ref); + const context = useMemo(() => ({useWrapper, size, rounding}), [rounding, size]); + return ( - + {props.children} diff --git a/packages/@react-spectrum/dialog/src/DialogContainer.tsx b/packages/@react-spectrum/dialog/src/DialogContainer.tsx index e8baa85c408..c0b9cb59967 100644 --- a/packages/@react-spectrum/dialog/src/DialogContainer.tsx +++ b/packages/@react-spectrum/dialog/src/DialogContainer.tsx @@ -12,7 +12,7 @@ import {DialogContext} from './context'; import {Modal} from '@react-spectrum/overlays'; -import React, {JSX, ReactElement, useState} from 'react'; +import React, {JSX, ReactElement, useMemo, useState} from 'react'; import {SpectrumDialogContainerProps} from '@react-types/dialog'; import {useOverlayTriggerState} from '@react-stately/overlays'; @@ -50,11 +50,11 @@ export function DialogContainer(props: SpectrumDialogContainerProps): JSX.Elemen setLastChild(child); } - let context = { + let context = useMemo(() => ({ type, onClose: onDismiss, isDismissable - }; + }), [isDismissable, onDismiss, type]); let state = useOverlayTriggerState({ isOpen: !!child, diff --git a/packages/@react-spectrum/dialog/src/DialogTrigger.tsx b/packages/@react-spectrum/dialog/src/DialogTrigger.tsx index 4639858b389..cf391ba34f8 100644 --- a/packages/@react-spectrum/dialog/src/DialogTrigger.tsx +++ b/packages/@react-spectrum/dialog/src/DialogTrigger.tsx @@ -14,7 +14,7 @@ import {DialogContext} from './context'; import {Modal, Popover, Tray} from '@react-spectrum/overlays'; import {OverlayTriggerState, useOverlayTriggerState} from '@react-stately/overlays'; import {PressResponder} from '@react-aria/interactions'; -import React, {Fragment, JSX, ReactElement, useEffect, useRef} from 'react'; +import React, {Fragment, JSX, ReactElement, useEffect, useMemo, useRef} from 'react'; import {SpectrumDialogClose, SpectrumDialogProps, SpectrumDialogTriggerProps} from '@react-types/dialog'; import {useIsMobileDevice} from '@react-spectrum/utils'; import {useOverlayTrigger} from '@react-aria/overlays'; @@ -57,7 +57,7 @@ function DialogTrigger(props: SpectrumDialogTriggerProps) { let onExiting = () => isExiting.current = true; let onExited = () => isExiting.current = false; - + useEffect(() => { return () => { if ((wasOpen.current || isExiting.current) && type !== 'popover' && type !== 'tray' && process.env.NODE_ENV !== 'production') { @@ -184,12 +184,12 @@ interface SpectrumDialogTriggerBase { } function DialogTriggerBase({type, state, isDismissable, dialogProps = {}, triggerProps = {}, overlay, trigger}: SpectrumDialogTriggerBase) { - let context = { + let context = useMemo(() => ({ type, onClose: state.close, isDismissable, ...dialogProps - }; + }), [dialogProps, isDismissable, state.close, type]); return ( diff --git a/packages/@react-spectrum/form/src/Form.tsx b/packages/@react-spectrum/form/src/Form.tsx index b15d9012d5f..9e65c0c7800 100644 --- a/packages/@react-spectrum/form/src/Form.tsx +++ b/packages/@react-spectrum/form/src/Form.tsx @@ -15,7 +15,7 @@ import {classNames, useDOMRef, useStyleProps} from '@react-spectrum/utils'; import {filterDOMProps} from '@react-aria/utils'; import {FormValidationContext} from '@react-stately/form'; import {Provider, useProviderProps} from '@react-spectrum/provider'; -import React, {useContext} from 'react'; +import React, {useContext, useMemo} from 'react'; import {SpectrumFormProps} from '@react-types/form'; import styles from '@adobe/spectrum-css-temp/components/fieldlabel/vars.css'; @@ -68,12 +68,14 @@ export const Form = React.forwardRef(function Form(props: SpectrumFormProps, ref let {styleProps} = useStyleProps(otherProps); let domRef = useDOMRef(ref); - let ctx = { + let ctx = useMemo(() => ({ labelPosition, labelAlign, necessityIndicator, validationBehavior - }; + }), [labelAlign, labelPosition, necessityIndicator, validationBehavior]); + + const validationContext = useMemo(() => validationErrors || {}, [validationErrors]); return (
- + {children} diff --git a/packages/@react-spectrum/list/src/ListView.tsx b/packages/@react-spectrum/list/src/ListView.tsx index 9481f98a410..9c798080d24 100644 --- a/packages/@react-spectrum/list/src/ListView.tsx +++ b/packages/@react-spectrum/list/src/ListView.tsx @@ -214,8 +214,32 @@ export const ListView = React.forwardRef(function ListView(pro let hasAnyChildren = useMemo(() => [...collection].some(item => item.hasChildNodes), [collection]); + const context = useMemo(() => ({ + state, + dragState, + dropState, + dragAndDropHooks, + onAction, + isListDraggable, + isListDroppable, + layout, + loadingState, + renderEmptyState + }), [ + dragAndDropHooks, + dragState, + dropState, + isListDraggable, + isListDroppable, + layout, + loadingState, + onAction, + renderEmptyState, + state + ]); + return ( - + focusedKey != null ? new Set([focusedKey]) : null, [focusedKey]); + const context = useMemo(() => ({ + state, + renderEmptyState, + shouldFocusOnHover, + shouldUseVirtualFocus + }), [renderEmptyState, shouldFocusOnHover, shouldUseVirtualFocus, state]); + return ( - + ({ + isUnavailable, triggerRef, ...submenuTriggerProps + }), [isUnavailable, submenuTriggerProps]); + return ( <> - {trigger} + {trigger} {submenuTriggerState.isOpen && overlay} diff --git a/packages/@react-spectrum/menu/src/Menu.tsx b/packages/@react-spectrum/menu/src/Menu.tsx index b14b256a51d..025156d0a36 100644 --- a/packages/@react-spectrum/menu/src/Menu.tsx +++ b/packages/@react-spectrum/menu/src/Menu.tsx @@ -21,7 +21,7 @@ import {MenuContext, MenuStateContext, useMenuStateContext} from './context'; import {MenuItem} from './MenuItem'; import {MenuSection} from './MenuSection'; import {mergeProps, useLayoutEffect, useSlotId, useSyncRef} from '@react-aria/utils'; -import React, {KeyboardEventHandler, ReactElement, ReactNode, RefObject, useContext, useEffect, useRef, useState} from 'react'; +import React, {KeyboardEventHandler, ReactElement, ReactNode, RefObject, useContext, useEffect, useMemo, useRef, useState} from 'react'; import {RootMenuTriggerState} from '@react-stately/menu'; import {SpectrumMenuProps} from '@react-types/menu'; import styles from '@adobe/spectrum-css-temp/components/menu/vars.css'; @@ -71,8 +71,17 @@ export const Menu = React.forwardRef(function Menu(props: Spec hasOpenSubmenu = nextMenuLevel != null; } + const context = useMemo(() => ({ + popoverContainer, + trayContainerRef, + menu: domRef, + submenu: submenuRef, + rootMenuTriggerState, + state + }), [domRef, popoverContainer, rootMenuTriggerState, state]); + return ( - +
({ ...menuProps, ref: menuRef, onClose: state.close, @@ -72,7 +72,7 @@ export const MenuTrigger = forwardRef(function MenuTrigger(props: SpectrumMenuTr } : undefined, UNSAFE_className: classNames(styles, {'spectrum-Menu-popover': !isMobile}), state - }; + }), [closeOnSelect, isMobile, menuProps, state]); // Close when clicking outside the root menu when a submenu is open. let rootOverlayRef = useRef(null); diff --git a/packages/@react-spectrum/menu/src/SubmenuTrigger.tsx b/packages/@react-spectrum/menu/src/SubmenuTrigger.tsx index 902870bc922..e07a9ce494b 100644 --- a/packages/@react-spectrum/menu/src/SubmenuTrigger.tsx +++ b/packages/@react-spectrum/menu/src/SubmenuTrigger.tsx @@ -15,7 +15,7 @@ import {Key} from '@react-types/shared'; import {MenuContext, SubmenuTriggerContext, useMenuStateContext} from './context'; import {mergeProps} from '@react-aria/utils'; import {Popover} from '@react-spectrum/overlays'; -import React, {type JSX, ReactElement, useRef} from 'react'; +import React, {type JSX, ReactElement, useMemo, useRef} from 'react'; import ReactDOM from 'react-dom'; import styles from '@adobe/spectrum-css-temp/components/menu/vars.css'; import {useLocale} from '@react-aria/i18n'; @@ -47,28 +47,8 @@ function SubmenuTrigger(props: SubmenuTriggerProps) { submenuRef: menuRef }, submenuTriggerState, triggerRef); let isMobile = useIsMobileDevice(); - let onBackButtonPress = () => { - submenuTriggerState.close(); - if (parentMenuRef.current && !parentMenuRef.current.contains(document.activeElement)) { - parentMenuRef.current.focus(); - } - }; let {direction} = useLocale(); - let mobileSubmenuKeyDown = (e: KeyboardEvent) => { - switch (e.key) { - case 'ArrowLeft': - if (direction === 'ltr') { - triggerRef.current?.focus(); - } - break; - case 'ArrowRight': - if (direction === 'rtl') { - triggerRef.current?.focus(); - } - break; - } - }; let overlay; @@ -104,7 +84,11 @@ function SubmenuTrigger(props: SubmenuTriggerProps) { ); } - let menuContext = { + const submenuContext = useMemo(() => ({ + triggerRef, ...submenuTriggerProps + }), [submenuTriggerProps]); + + let menuContext = useMemo(() => ({ ...mergeProps(submenuProps, { ref: menuRef, UNSAFE_style: isMobile ? { @@ -113,15 +97,33 @@ function SubmenuTrigger(props: SubmenuTriggerProps) { } : undefined, UNSAFE_className: classNames(styles, {'spectrum-Menu-popover': !isMobile}), ...(isMobile && { - onBackButtonPress, - onKeyDown: mobileSubmenuKeyDown + onBackButtonPress() { + submenuTriggerState.close(); + if (parentMenuRef.current && !parentMenuRef.current.contains(document.activeElement)) { + parentMenuRef.current.focus(); + } + }, + onKeyDown(e: KeyboardEvent) { + switch (e.key) { + case 'ArrowLeft': + if (direction === 'ltr') { + triggerRef.current?.focus(); + } + break; + case 'ArrowRight': + if (direction === 'rtl') { + triggerRef.current?.focus(); + } + break; + } + } }) }) - }; + }), [direction, isMobile, menuRef, parentMenuRef, submenuProps, submenuTriggerState]); return ( <> - {menuTrigger} + {menuTrigger} {overlay} diff --git a/packages/@react-spectrum/provider/src/Provider.tsx b/packages/@react-spectrum/provider/src/Provider.tsx index 53fde2faf3c..15576a7f573 100644 --- a/packages/@react-spectrum/provider/src/Provider.tsx +++ b/packages/@react-spectrum/provider/src/Provider.tsx @@ -24,7 +24,7 @@ import {filterDOMProps, RouterProvider} from '@react-aria/utils'; import {I18nProvider, useLocale} from '@react-aria/i18n'; import {ModalProvider, useModalProvider} from '@react-aria/overlays'; import {ProviderContext, ProviderProps} from '@react-types/provider'; -import React, {useContext, useEffect, useRef} from 'react'; +import React, {useContext, useEffect, useMemo, useRef} from 'react'; import styles from '@adobe/spectrum-css-temp/components/page/vars.css'; import typographyStyles from '@adobe/spectrum-css-temp/components/typography/index.css'; import {useColorScheme, useScale} from './mediaQueries'; @@ -73,27 +73,40 @@ export const Provider = React.forwardRef(function Provider(props: ProviderProps, ...otherProps } = props; - // select only the props with values so undefined props don't overwrite prevContext values - let currentProps = { - version, - theme, + let matchedBreakpoints = useMatchedBreakpoints(breakpoints!); + + // Merge options with parent provider + let context = useMemo(() => { + // select only the props with values so undefined props don't overwrite prevContext values + let currentProps = { + version, + theme, + breakpoints, + colorScheme, + scale, + isQuiet, + isEmphasized, + isDisabled, + isRequired, + isReadOnly, + validationState + }; + let filteredProps = {}; + Object.entries(currentProps).forEach(([key, value]) => value !== undefined && (filteredProps[key] = value)); + return Object.assign({}, prevContext, filteredProps); + }, [ breakpoints, colorScheme, - scale, - isQuiet, - isEmphasized, isDisabled, - isRequired, + isEmphasized, + isQuiet, isReadOnly, + isRequired, + prevContext, + scale, + theme, validationState - }; - - let matchedBreakpoints = useMatchedBreakpoints(breakpoints!); - let filteredProps = {}; - Object.entries(currentProps).forEach(([key, value]) => value !== undefined && (filteredProps[key] = value)); - - // Merge options with parent provider - let context = Object.assign({}, prevContext, filteredProps); + ]); // Only wrap in a DOM node if the theme, colorScheme, or scale changed let contents = children; diff --git a/packages/@react-spectrum/radio/src/RadioGroup.tsx b/packages/@react-spectrum/radio/src/RadioGroup.tsx index 33c0fcfda37..5f3356ad225 100644 --- a/packages/@react-spectrum/radio/src/RadioGroup.tsx +++ b/packages/@react-spectrum/radio/src/RadioGroup.tsx @@ -14,7 +14,7 @@ import {classNames, useDOMRef} from '@react-spectrum/utils'; import {DOMRef} from '@react-types/shared'; import {Field} from '@react-spectrum/label'; import {RadioContext} from './context'; -import React from 'react'; +import React, {useMemo} from 'react'; import {SpectrumRadioGroupProps} from '@react-types/radio'; import styles from '@adobe/spectrum-css-temp/components/fieldgroup/vars.css'; import {useFormProps} from '@react-spectrum/form'; @@ -39,6 +39,8 @@ export const RadioGroup = React.forwardRef(function RadioGroup(props: SpectrumRa let state = useRadioGroupState(props); let {radioGroupProps, ...otherProps} = useRadioGroup(props, state); + const context = useMemo(() => ({isEmphasized, state}), [isEmphasized, state]); + return ( + value={context}> {children}
diff --git a/packages/@react-spectrum/s2/src/Accordion.tsx b/packages/@react-spectrum/s2/src/Accordion.tsx index 07f2763055b..dba38de2fef 100644 --- a/packages/@react-spectrum/s2/src/Accordion.tsx +++ b/packages/@react-spectrum/s2/src/Accordion.tsx @@ -14,7 +14,7 @@ import {ContextValue, DisclosureGroup, DisclosureGroupProps, SlotProps} from 're import {DisclosureContext} from './Disclosure'; import {DOMProps, DOMRef, DOMRefValue, GlobalDOMAttributes} from '@react-types/shared'; import {getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with { type: 'macro' }; -import React, {createContext, forwardRef} from 'react'; +import React, {createContext, forwardRef, useMemo} from 'react'; import {style} from '../style' with { type: 'macro' }; import {useDOMRef} from '@react-spectrum/utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -58,8 +58,13 @@ export const Accordion = forwardRef(function Accordion(props: AccordionProps, re density = 'regular', isQuiet } = props; + + const context = useMemo(() => ({ + size, isQuiet, density + }), [density, isQuiet, size]); + return ( - + ({ + styles: avatar, size, isOverBackground: true + }), [size]); + return ( - +
(null); function CollapsingCollection({children, containerRef, onAction}) { + + const context = useMemo(() => ({ + containerRef, onAction + }), [containerRef, onAction]); + return ( - + {children} diff --git a/packages/@react-spectrum/s2/src/Card.tsx b/packages/@react-spectrum/s2/src/Card.tsx index bc7271451e6..f40a1c304db 100644 --- a/packages/@react-spectrum/s2/src/Card.tsx +++ b/packages/@react-spectrum/s2/src/Card.tsx @@ -17,7 +17,7 @@ import {Checkbox} from './Checkbox'; import {color, focusRing, lightDark, space, style} from '../style' with {type: 'macro'}; import {composeRenderProps, ContextValue, DEFAULT_SLOT, type GridListItem, GridListItemProps, Provider} from 'react-aria-components'; import {ContentContext, FooterContext, TextContext} from './Content'; -import {createContext, CSSProperties, forwardRef, ReactNode, useContext} from 'react'; +import {createContext, CSSProperties, forwardRef, ReactNode, useContext, useMemo} from 'react'; import {DividerContext} from './Divider'; import {DOMProps, DOMRef, DOMRefValue, GlobalDOMAttributes} from '@react-types/shared'; import {filterDOMProps, inertValue} from '@react-aria/utils'; @@ -413,6 +413,16 @@ export const Card = forwardRef(function Card(props: CardProps, ref: DOMRef ); + const context = useMemo(() => ({ + size, + isQuiet, + isCheckboxSelection: false, + isHovered: false, + isFocusVisible: false, + isSelected: false, + isPressed: false + }), [isQuiet, size]); + let ElementType = useContext(InternalCardViewContext); if (ElementType === 'div' || isSkeleton) { return ( @@ -424,7 +434,7 @@ export const Card = forwardRef(function Card(props: CardProps, ref: DOMRef - + {children}
@@ -442,6 +452,7 @@ export const Card = forwardRef(function Card(props: CardProps, ref: DOMRef {({selectionMode, selectionBehavior, isHovered, isFocusVisible, isSelected, isPressed}) => ( + // eslint-disable-next-line react/jsx-no-constructed-context-values {/* Selection indicator and checkbox move inside the preview for quiet cards */} {!isQuiet && } @@ -546,10 +557,11 @@ const collectionImage = style({ export const CollectionCardPreview = forwardRef(function CollectionCardPreview(props: CardPreviewProps, ref: DOMRef) { let {size} = useContext(InternalCardContext)!; + const context = useMemo(() => ({styles: collectionImage}), []); return (
- + {props.children}
diff --git a/packages/@react-spectrum/s2/src/CheckboxGroup.tsx b/packages/@react-spectrum/s2/src/CheckboxGroup.tsx index 470db185e2b..897c73acdca 100644 --- a/packages/@react-spectrum/s2/src/CheckboxGroup.tsx +++ b/packages/@react-spectrum/s2/src/CheckboxGroup.tsx @@ -16,7 +16,7 @@ import { ContextValue } from 'react-aria-components'; import {CheckboxContext} from './Checkbox'; -import {createContext, forwardRef, ReactNode, useContext} from 'react'; +import {createContext, forwardRef, ReactNode, useContext, useMemo} from 'react'; import {DOMRef, DOMRefValue, GlobalDOMAttributes, HelpTextProps, Orientation, SpectrumLabelableProps} from '@react-types/shared'; import {field, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import {FieldLabel, HelpText} from './Field'; @@ -75,6 +75,12 @@ export const CheckboxGroup = forwardRef(function CheckboxGroup(props: CheckboxGr } = props; let domRef = useDOMRef(ref); + const context = useMemo(() => ({ + ...formContext, size, isRequired: undefined + }), [formContext, size]); + + const checkboxContext = useMemo(() => ({isEmphasized}), [isEmphasized]); + return ( - - + + {children} diff --git a/packages/@react-spectrum/s2/src/ColorSwatchPicker.tsx b/packages/@react-spectrum/s2/src/ColorSwatchPicker.tsx index 9028a4f65e4..de3030213c0 100644 --- a/packages/@react-spectrum/s2/src/ColorSwatchPicker.tsx +++ b/packages/@react-spectrum/s2/src/ColorSwatchPicker.tsx @@ -12,7 +12,7 @@ import {ColorSwatchPicker as AriaColorSwatchPicker, ColorSwatchPickerItem as AriaColorSwatchPickerItem, Color, ContextValue, SlotProps} from 'react-aria-components'; import {ColorSwatchProps, InternalColorSwatchContext} from './ColorSwatch'; -import {createContext, forwardRef, ReactElement, ReactNode} from 'react'; +import {createContext, forwardRef, ReactElement, ReactNode, useMemo} from 'react'; import {DOMRef, DOMRefValue, ValueBase} from '@react-types/shared'; import {focusRing, space, style} from '../style' with {type: 'macro'}; import {getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; @@ -55,6 +55,8 @@ export const ColorSwatchPicker = forwardRef(function ColorSwatchPicker(props: Co } = props; let domRef = useDOMRef(ref); + const context = useMemo(() => ({useWrapper, size, rounding}), [rounding, size]); + return ( - + {props.children} diff --git a/packages/@react-spectrum/s2/src/ComboBox.tsx b/packages/@react-spectrum/s2/src/ComboBox.tsx index 58c7ca7ee66..3e8f41f00c9 100644 --- a/packages/@react-spectrum/s2/src/ComboBox.tsx +++ b/packages/@react-spectrum/s2/src/ComboBox.tsx @@ -577,9 +577,11 @@ const ComboboxInner = forwardRef(function ComboboxInner(props: ComboBoxProps ({size}), [size]); + return ( <> - + {ctx => ( + // eslint-disable-next-line react/jsx-no-constructed-context-values diff --git a/packages/@react-spectrum/s2/src/DialogContainer.tsx b/packages/@react-spectrum/s2/src/DialogContainer.tsx index ac26bcdc84c..6cc2ae995ab 100644 --- a/packages/@react-spectrum/s2/src/DialogContainer.tsx +++ b/packages/@react-spectrum/s2/src/DialogContainer.tsx @@ -11,7 +11,7 @@ */ import {ModalContext, useSlottedContext} from 'react-aria-components'; -import React, {ReactElement, ReactNode, useState} from 'react'; +import React, {ReactElement, ReactNode, useMemo, useState} from 'react'; import {SpectrumDialogContainerProps} from '@react-types/dialog'; export interface DialogContainerProps extends Omit {} @@ -47,14 +47,17 @@ export function DialogContainer(props: DialogContainerProps): ReactNode { setLastChild(child); } - let onOpenChange = (isOpen: boolean) => { - if (!isOpen) { - onDismiss(); + const context = useMemo(() => ({ + isOpen: !!child, + onOpenChange(isOpen: boolean) { + if (!isOpen) { + onDismiss(); + } } - }; + }), [child, onDismiss]); return ( - + {lastChild} ); diff --git a/packages/@react-spectrum/s2/src/DropZone.tsx b/packages/@react-spectrum/s2/src/DropZone.tsx index 3e755938ede..af25516ecd9 100644 --- a/packages/@react-spectrum/s2/src/DropZone.tsx +++ b/packages/@react-spectrum/s2/src/DropZone.tsx @@ -108,6 +108,7 @@ export const DropZone = /*#__PURE__*/ forwardRef(function DropZone(props: DropZo className={renderProps => (props.UNSAFE_className || '') + dropzone(renderProps, props.styles)}> {renderProps => ( <> + {/* eslint-disable-next-line react/jsx-no-constructed-context-values */} {props.children} diff --git a/packages/@react-spectrum/s2/src/Field.tsx b/packages/@react-spectrum/s2/src/Field.tsx index 3c915ad0b3f..05ed2ad50a6 100644 --- a/packages/@react-spectrum/s2/src/Field.tsx +++ b/packages/@react-spectrum/s2/src/Field.tsx @@ -18,7 +18,7 @@ import {CenterBaseline, centerBaseline, centerBaselineBefore} from './CenterBase import {composeRenderProps, FieldError, FieldErrorProps, Group, GroupProps, Label, LabelProps, Provider, Input as RACInput, InputProps as RACInputProps, Text} from 'react-aria-components'; import {ContextualHelpContext} from './ContextualHelp'; import {control, controlFont, fieldInput, fieldLabel, StyleProps, UnsafeStyles} from './style-utils' with {type: 'macro'}; -import {ForwardedRef, forwardRef, ReactNode} from 'react'; +import {ForwardedRef, forwardRef, ReactNode, useMemo} from 'react'; import {IconContext} from './Icon'; // @ts-ignore import intlMessages from '../intl/*.json'; @@ -67,6 +67,12 @@ export const FieldLabel = forwardRef(function FieldLabel(props: FieldLabelProps, labelProps.id = fallbackLabelPropsId; } + const context = useMemo(() => ({ + id: contextualHelpId, + 'aria-labelledby': labelProps?.id ? `${labelProps.id} ${contextualHelpId}` : undefined, + size: (size === 'L' || size === 'XL') ? 'S' : 'XS' + } as const), [contextualHelpId, labelProps.id, size]); + if (!props.children) { return null; } @@ -135,11 +141,7 @@ export const FieldLabel = forwardRef(function FieldLabel(props: FieldLabelProps, height: 0 })}> + value={context}> {contextualHelp} diff --git a/packages/@react-spectrum/s2/src/Form.tsx b/packages/@react-spectrum/s2/src/Form.tsx index e426f3f59ab..c1244001c10 100644 --- a/packages/@react-spectrum/s2/src/Form.tsx +++ b/packages/@react-spectrum/s2/src/Form.tsx @@ -78,6 +78,24 @@ export const Form = /*#__PURE__*/ forwardRef(function Form(props: FormProps, ref } = props; let domRef = useDOMRef(ref); + const context = useMemo(() => ({ + labelPosition, + labelAlign, + necessityIndicator, + isRequired, + isDisabled, + isEmphasized, + size + }), [ + isDisabled, + isEmphasized, + isRequired, + labelAlign, + labelPosition, + necessityIndicator, + size + ]); + return ( + value={context}> {props.children} diff --git a/packages/@react-spectrum/s2/src/ImageCoordinator.tsx b/packages/@react-spectrum/s2/src/ImageCoordinator.tsx index 9d2c6d8f978..d549f605b98 100644 --- a/packages/@react-spectrum/s2/src/ImageCoordinator.tsx +++ b/packages/@react-spectrum/s2/src/ImageCoordinator.tsx @@ -155,9 +155,14 @@ function ImageCoordinatorRoot(props: ImageCoordinatorProps) { }, [loadStartTime, loadedAll, timeout]); let revealAll = loadedAll || timedOut; + + const context = useMemo(() => ({ + revealAll, register, unregister, load + }), [load, register, revealAll, unregister]); + return useMemo(() => ( - + {children} - ), [group, children, revealAll, register, unregister, load]); + ), [children, context, group]); } diff --git a/packages/@react-spectrum/s2/src/Menu.tsx b/packages/@react-spectrum/s2/src/Menu.tsx index 44c66cd8b0c..19778d8e885 100644 --- a/packages/@react-spectrum/s2/src/Menu.tsx +++ b/packages/@react-spectrum/s2/src/Menu.tsx @@ -34,7 +34,7 @@ import {centerBaseline} from './CenterBaseline'; import {centerPadding, control, controlFont, controlSize, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; import CheckmarkIcon from '../ui-icons/Checkmark'; import ChevronRightIcon from '../ui-icons/Chevron'; -import {createContext, forwardRef, JSX, ReactNode, useContext, useRef, useState} from 'react'; +import {createContext, forwardRef, JSX, ReactNode, useContext, useMemo, useRef, useState} from 'react'; import {divider} from './Divider'; import {DOMRef, DOMRefValue, GlobalDOMAttributes, PressEvent} from '@react-types/shared'; import {forwardRefType} from './types'; @@ -356,8 +356,10 @@ export const Menu = /*#__PURE__*/ (forwardRef as forwardRefType)(function Menu ({size, isSubmenu: true, hideLinkOutIcon}), [hideLinkOutIcon, size]); + let content = ( - + ({ + align: props.align, + direction: props.direction, + shouldFlip: props.shouldFlip + }), [props.align, props.direction, props.shouldFlip]); + return ( + value={context}> {props.children} diff --git a/packages/@react-spectrum/s2/src/NumberField.tsx b/packages/@react-spectrum/s2/src/NumberField.tsx index c0c3cff832e..4f7e7776e98 100644 --- a/packages/@react-spectrum/s2/src/NumberField.tsx +++ b/packages/@react-spectrum/s2/src/NumberField.tsx @@ -211,6 +211,7 @@ export const NumberField = forwardRef(function NumberField(props: NumberFieldPro })({size, isStepperHidden: hideStepper})}> {ctx => ( + // eslint-disable-next-line react/jsx-no-constructed-context-values diff --git a/packages/@react-spectrum/s2/src/Picker.tsx b/packages/@react-spectrum/s2/src/Picker.tsx index 14e9c679d7c..62b0b6e6daa 100644 --- a/packages/@react-spectrum/s2/src/Picker.tsx +++ b/packages/@react-spectrum/s2/src/Picker.tsx @@ -334,6 +334,8 @@ export const Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(function Pick } let scale = useScale(); + const context = useMemo(() => ({size}), [size]); + return ( {({isDisabled, isOpen, isFocusVisible, isInvalid, isRequired}) => ( <> - + ({ + slots: { + icon: {render: centerBaseline({slot: 'icon', styles: iconCenterWrapper}), styles: icon} + } + }), []); return ( + value={context}> ({ + ...formContext, size, isEmphasized + }), [formContext, isEmphasized, size]); + return ( - + {children} diff --git a/packages/@react-spectrum/s2/src/Tabs.tsx b/packages/@react-spectrum/s2/src/Tabs.tsx index 7965b8f8db6..a5e339392be 100644 --- a/packages/@react-spectrum/s2/src/Tabs.tsx +++ b/packages/@react-spectrum/s2/src/Tabs.tsx @@ -667,6 +667,8 @@ let CollapsingTabs = ({collection, containerRef, ...props}: {collection: Collect let menuId = useId(); let valueId = useId(); + const hideContext = useMemo(() => ({showTabs: false, menuId, valueId}), [menuId, valueId]); + let contents: ReactNode; if (showItems) { contents = ( @@ -686,19 +688,21 @@ let CollapsingTabs = ({collection, containerRef, ...props}: {collection: Collect onSelectionChange={onSelectionChange} aria-label={props['aria-label']} aria-describedby={props['aria-labelledby']} /> - + {props.children} ); } + const showContext = useMemo(() => ({showTabs: true, menuId, valueId}), [menuId, valueId]); + return (
- + {contents}
diff --git a/packages/@react-spectrum/s2/src/TabsPicker.tsx b/packages/@react-spectrum/s2/src/TabsPicker.tsx index e986191cff2..6259ea672b8 100644 --- a/packages/@react-spectrum/s2/src/TabsPicker.tsx +++ b/packages/@react-spectrum/s2/src/TabsPicker.tsx @@ -48,7 +48,7 @@ import {Placement} from 'react-aria'; import {PopoverBase} from './Popover'; import {pressScale} from './pressScale'; import {raw} from '../style/style-macro' with {type: 'macro'}; -import React, {createContext, forwardRef, ReactNode, useContext, useRef} from 'react'; +import React, {createContext, forwardRef, ReactNode, useContext, useMemo, useRef} from 'react'; import {useFocusableRef} from '@react-spectrum/utils'; import {useFormProps} from './Form'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -314,6 +314,16 @@ export function PickerItem(props: PickerItemProps): ReactNode { let ref = useRef(null); let isLink = props.href != null; const size = 'M'; + const iconContext = useMemo(() => ({ + slots: { + icon: {render: centerBaseline({slot: 'icon', styles: iconCenterWrapper({})}), styles: icon} + } + }), []); + const textContext = useMemo(() => ({ + slots: { + [DEFAULT_SLOT]: {styles: label({size})} + } + }), []); return ( + value={iconContext}> + value={textContext}> {!isLink && } {typeof children === 'string' ? {children} : children} diff --git a/packages/@react-spectrum/s2/src/TagGroup.tsx b/packages/@react-spectrum/s2/src/TagGroup.tsx index f5827931fb3..3604c00fd65 100644 --- a/packages/@react-spectrum/s2/src/TagGroup.tsx +++ b/packages/@react-spectrum/s2/src/TagGroup.tsx @@ -90,8 +90,11 @@ export const TagGroup = /*#__PURE__*/ (forwardRef as forwardRefType)(function Ta [props, ref] = useSpectrumContextProps(props, ref, TagGroupContext); props = useFormProps(props); let {onRemove} = props; + + const context = useMemo(() => ({onRemove}), [onRemove]); + return ( - + }> {collection => } @@ -259,6 +262,8 @@ function TagGroupInner({ ); } + const context = useMemo(() => ({...formContext, size}), [formContext, size]); + return ( ({ }, position: 'relative' })({isEmpty})}> - + {ctx => ( + // eslint-disable-next-line react/jsx-no-constructed-context-values {children} diff --git a/packages/@react-spectrum/s2/src/Tooltip.tsx b/packages/@react-spectrum/s2/src/Tooltip.tsx index bc27615475c..66f0278867e 100644 --- a/packages/@react-spectrum/s2/src/Tooltip.tsx +++ b/packages/@react-spectrum/s2/src/Tooltip.tsx @@ -22,7 +22,7 @@ import { import {centerPadding, colorScheme, UnsafeStyles} from './style-utils' with {type: 'macro'}; import {ColorScheme} from '@react-types/provider'; import {ColorSchemeContext} from './Provider'; -import {createContext, forwardRef, MutableRefObject, ReactNode, useCallback, useContext, useState} from 'react'; +import {createContext, forwardRef, MutableRefObject, ReactNode, useCallback, useContext, useMemo, useState} from 'react'; import {DOMRef, GlobalDOMAttributes} from '@react-types/shared'; import {style} from '../style' with {type: 'macro'}; import {useDOMRef} from '@react-spectrum/utils'; @@ -198,16 +198,18 @@ export function TooltipTrigger(props: TooltipTriggerProps): ReactNode { ...triggerProps } = props; + const context = useMemo(() => ({ + containerPadding: containerPadding, + crossOffset: crossOffset, + offset: offset, + placement: placement, + shouldFlip: shouldFlip + }), [containerPadding, crossOffset, offset, placement, shouldFlip]); + return ( + value={context}> {props.children} diff --git a/packages/@react-spectrum/s2/src/TreeView.tsx b/packages/@react-spectrum/s2/src/TreeView.tsx index 7b479d46cfc..7734b9d4531 100644 --- a/packages/@react-spectrum/s2/src/TreeView.tsx +++ b/packages/@react-spectrum/s2/src/TreeView.tsx @@ -39,7 +39,7 @@ import {IconContext} from './Icon'; import intlMessages from '../intl/*.json'; import {ProgressCircle} from './ProgressCircle'; import {raw} from '../style/style-macro' with {type: 'macro'}; -import React, {createContext, forwardRef, JSXElementConstructor, ReactElement, ReactNode, useContext, useRef} from 'react'; +import React, {createContext, forwardRef, JSXElementConstructor, ReactElement, ReactNode, useContext, useMemo, useRef} from 'react'; import {TextContext} from './Content'; import {useDOMRef} from '@react-spectrum/utils'; import {useLocale, useLocalizedStringFormatter} from 'react-aria'; @@ -118,6 +118,9 @@ export const TreeView = /*#__PURE__*/ (forwardRef as forwardRefType)(function Tr let domRef = useDOMRef(ref); + const rendererContext = useMemo(() => ({renderer}), [renderer]); + const internalContext = useMemo(() => ({isDetached, isEmphasized}), [isDetached, isEmphasized]); + return ( - - + + (props: TableBaseProps, ref: DOMRef ({ + state, + dragState, + dropState, + dragAndDropHooks, + isTableDraggable, + isTableDroppable, + layout, + onResizeStart, + onResize: props.onResize, + onResizeEnd, + headerRowHovered, + isInResizeMode, + setIsInResizeMode, + isEmpty, + onFocusedResizer, + headerMenuOpen, + setHeaderMenuOpen, + renderEmptyState: props.renderEmptyState + }), [ + dragAndDropHooks, + dragState, + dropState, + headerMenuOpen, + headerRowHovered, + isEmpty, + isInResizeMode, + isTableDraggable, + isTableDroppable, + layout, + onResizeEnd, + onResizeStart, + props.onResize, + props.renderEmptyState, + state + ]); + return ( + value={context}> ({ + dragButtonProps, dragButtonRef, isFocusVisibleWithin + }), [dragButtonProps, isFocusVisibleWithin]); + return ( - + {isTableDroppable && isFirstRow && (props: Spec useResizeObserver({ref: wrapperRef, onResize: checkShouldCollapse}); - let tabPanelProps: HTMLAttributes = { - 'aria-labelledby': undefined - }; - - // When the tabs are collapsed, the tabPanel should be labelled by the Picker button element. let collapsibleTabListId = useId(); - if (collapsed && orientation !== 'vertical') { - tabPanelProps['aria-labelledby'] = collapsibleTabListId; - } + + const context = useMemo(() => { + let tabPanelProps: HTMLAttributes = { + 'aria-labelledby': undefined + }; + + // When the tabs are collapsed, the tabPanel should be labelled by the Picker button element. + if (collapsed && orientation !== 'vertical') { + tabPanelProps['aria-labelledby'] = collapsibleTabListId; + } + + return { + tabProps: {...props, orientation, density}, + tabState: {tabListState, setTabListState, selectedTab, collapsed}, + refs: {tablistRef, wrapperRef}, + tabPanelProps, + tabLineState: tabPositions + }; + }, [ + collapsed, + collapsibleTabListId, + density, + orientation, + props, + selectedTab, + tabListState, + tabPositions + ]); + return ( - +
state.close(true) }); + const context = useMemo(() => ({ + state, + placement, + ref: overlayRef, + UNSAFE_style: overlayProps.style, + arrowProps, + arrowRef: arrowRef, + ...tooltipProps + }), [arrowProps, overlayProps.style, placement, state, tooltipProps]); + return ( {trigger} + value={context}> {tooltip} diff --git a/packages/@react-spectrum/tree/src/TreeView.tsx b/packages/@react-spectrum/tree/src/TreeView.tsx index 4f70908457f..878132b018f 100644 --- a/packages/@react-spectrum/tree/src/TreeView.tsx +++ b/packages/@react-spectrum/tree/src/TreeView.tsx @@ -29,7 +29,7 @@ import ChevronRightMedium from '@spectrum-icons/ui/ChevronRightMedium'; import {DOMRef, Expandable, Key, SelectionBehavior, SpectrumSelectionProps, StyleProps} from '@react-types/shared'; import {focusRing, style} from '@react-spectrum/style-macro-s1' with {type: 'macro'}; import {isAndroid} from '@react-aria/utils'; -import React, {createContext, JSX, JSXElementConstructor, ReactElement, ReactNode, useRef} from 'react'; +import React, {createContext, JSX, JSXElementConstructor, ReactElement, ReactNode, useMemo, useRef} from 'react'; import {SlotProvider, useDOMRef, useStyleProps} from '@react-spectrum/utils'; import {useButton} from '@react-aria/button'; import {useLocale} from '@react-aria/i18n'; @@ -103,8 +103,10 @@ export const TreeView = React.forwardRef(function TreeView(pro let domRef = useDOMRef(ref); let selectionBehavior = selectionStyle === 'highlight' ? 'replace' : 'toggle'; + const context = useMemo(() => ({renderer}), [renderer]); + return ( - + (UNSAFE_className ?? '') + tree(renderProps)} selectionBehavior={selectionBehavior as SelectionBehavior} ref={domRef}> {props.children} diff --git a/packages/@react-spectrum/utils/src/BreakpointProvider.tsx b/packages/@react-spectrum/utils/src/BreakpointProvider.tsx index e84deda0bbb..31617e21560 100644 --- a/packages/@react-spectrum/utils/src/BreakpointProvider.tsx +++ b/packages/@react-spectrum/utils/src/BreakpointProvider.tsx @@ -1,4 +1,4 @@ -import React, {ReactNode, useContext, useEffect, useState} from 'react'; +import React, {ReactNode, useContext, useEffect, useMemo, useState} from 'react'; import {useIsSSR} from '@react-aria/ssr'; interface Breakpoints { @@ -25,9 +25,10 @@ export function BreakpointProvider(props: BreakpointProviderProps): ReactNode { children, matchedBreakpoints } = props; + const context = useMemo(() => ({matchedBreakpoints}), [matchedBreakpoints]); return ( + value={context} > {children} ); diff --git a/packages/dev/docs/pages/react-aria/home/MouseAnimation.tsx b/packages/dev/docs/pages/react-aria/home/MouseAnimation.tsx index 0c6873a3719..58d55fd804a 100644 --- a/packages/dev/docs/pages/react-aria/home/MouseAnimation.tsx +++ b/packages/dev/docs/pages/react-aria/home/MouseAnimation.tsx @@ -13,7 +13,7 @@ import {animate, useIntersectionObserver} from './utils'; import {Button} from 'tailwind-starter/Button'; import {ButtonContext, Key, TooltipTrigger} from 'react-aria-components'; import {CogIcon, PencilIcon, ShareIcon} from 'lucide-react'; -import React, {ReactNode, useCallback, useRef, useState} from 'react'; +import React, {ReactNode, useCallback, useMemo, useRef, useState} from 'react'; import {Tooltip} from 'tailwind-starter/Tooltip'; export function MouseAnimation(): ReactNode { @@ -132,6 +132,8 @@ export function MouseAnimation(): ReactNode { } }; + const context = useMemo(() => ({isPressed}), [isPressed]); + return (
Share onOpenChange('settings', o)}> - + diff --git a/packages/dev/docs/src/Layout.js b/packages/dev/docs/src/Layout.js index 60f8592e172..0771fa8b342 100644 --- a/packages/dev/docs/src/Layout.js +++ b/packages/dev/docs/src/Layout.js @@ -28,7 +28,7 @@ import linkStyle from '@adobe/spectrum-css-temp/components/link/vars.css'; import {MDXProvider} from '@mdx-js/react'; import pageStyles from '@adobe/spectrum-css-temp/components/page/vars.css'; import path from 'path'; -import React from 'react'; +import React, {useMemo} from 'react'; import ruleStyles from '@adobe/spectrum-css-temp/components/rule/vars.css'; import sideNavStyles from '@adobe/spectrum-css-temp/components/sidenav/vars.css'; import {SlotProvider} from '@react-spectrum/utils'; @@ -515,6 +515,7 @@ function Footer() { export const PageContext = React.createContext(); export function BaseLayout({scripts, styles, pages, currentPage, publicUrl, children, toc}) { let pathToPage = currentPage.filePath.substring(currentPage.filePath.indexOf('packages/'), currentPage.filePath.length); + const context = useMemo(() => ({pages, currentPage}), [currentPage, pages]); return (
@@ -524,7 +525,7 @@ export function BaseLayout({scripts, styles, pages, currentPage, publicUrl, chil - + {children} @@ -609,6 +610,7 @@ export function Time({date}) { export function ExampleLayout({scripts, styles, pages, currentPage, publicUrl, children, toc}) { let pathToPage = currentPage.filePath.substring(currentPage.filePath.indexOf('packages/'), currentPage.filePath.length); + const context = useMemo(() => ({pages, currentPage}), [currentPage, pages]); return (
@@ -617,7 +619,7 @@ export function ExampleLayout({scripts, styles, pages, currentPage, publicUrl, c - +
diff --git a/packages/dev/docs/src/types.js b/packages/dev/docs/src/types.js index b8bdfce27f0..4e3658fe931 100644 --- a/packages/dev/docs/src/types.js +++ b/packages/dev/docs/src/types.js @@ -17,7 +17,7 @@ import {getDoc} from 'globals-docs'; import linkStyle from '@adobe/spectrum-css-temp/components/link/vars.css'; import Lowlight from 'react-lowlight'; import Markdown from 'markdown-to-jsx'; -import React, {useContext} from 'react'; +import React, {useContext, useMemo} from 'react'; import styles from './docs.css'; import tableStyles from '@adobe/spectrum-css-temp/components/table/vars.css'; import typographyStyles from '@adobe/spectrum-css-temp/components/typography/vars.css'; @@ -224,8 +224,10 @@ export function Indent({params, open, close, children, alwaysIndent}) { small += ' '; } + const context = useMemo(() => ({small, large, alwaysIndent}), [alwaysIndent, large, small]); + return ( - + {open} {children} {close} @@ -267,6 +269,8 @@ export function JoinList({elements, joiner, minIndent = 2, newlineBefore, neverI ); } + const context = useMemo(() => ({small, large, alwaysIndent}), [alwaysIndent, large, small]); + return elements .filter(Boolean) .reduce((acc, v, i) => [ @@ -277,7 +281,7 @@ export function JoinList({elements, joiner, minIndent = 2, newlineBefore, neverI {contents} , @@ -374,7 +378,7 @@ function Parameter({name, value, default: defaultValue, optional, rest}) { } export function LinkProvider({children}) { - let links = new Map(); + let links = useMemo(() => new Map(), []); return ( {children} diff --git a/packages/dev/s2-docs/src/TypePopover.tsx b/packages/dev/s2-docs/src/TypePopover.tsx index 642fea5f864..9773b7c6fa3 100644 --- a/packages/dev/s2-docs/src/TypePopover.tsx +++ b/packages/dev/s2-docs/src/TypePopover.tsx @@ -2,7 +2,7 @@ import {Breadcrumb, Breadcrumbs, DialogTrigger, Popover} from '@react-spectrum/s2'; import {ColorLink} from './Link'; -import React, {createContext, ReactNode, useContext, useState} from 'react'; +import React, {createContext, ReactNode, useContext, useMemo, useState} from 'react'; import {style} from '@react-spectrum/s2/style' with {type: 'macro'}; interface TypeLinkContextValue { @@ -18,18 +18,21 @@ export function TypePopover({name, children}: {name: string, children: ReactNode let [breadcrumbs, setBreadcrumbs] = useState([{id: 0, name, children}]); let ctx = useContext(TypeLinkContext); + const context = useMemo(() => ({ + push(name: string, children: ReactNode) { + setBreadcrumbs(breadcrumbs => [...breadcrumbs, {id: breadcrumbs.length, name, children}]); + } + }), []); + // If already inside a parent TypeLink, push onto the stack within the same popover. if (ctx) { return ( ctx.push(name, children)}>{name} ); } - + // Otherwise render the root popover with breadcrumbs for the stack of visited links. let cur = breadcrumbs.at(-1)!; - let push = (name: string, children: ReactNode) => { - setBreadcrumbs(breadcrumbs => [...breadcrumbs, {id: breadcrumbs.length, name, children}]); - }; return ( @@ -42,7 +45,7 @@ export function TypePopover({name, children}: {name: string, children: ReactNode styles={style({marginBottom: 16})}> {breadcrumbs.map(item => {item.name})} - + {cur.children}
diff --git a/packages/dev/s2-docs/src/VisualExampleClient.tsx b/packages/dev/s2-docs/src/VisualExampleClient.tsx index b287d85dbe6..d19146fd455 100644 --- a/packages/dev/s2-docs/src/VisualExampleClient.tsx +++ b/packages/dev/s2-docs/src/VisualExampleClient.tsx @@ -81,8 +81,12 @@ export function VisualExampleClient({component, name, importSource, controls, ch } }, []); + const context = useMemo(() => ({ + component, name, importSource, controls, props, setProps + }), [component, controls, importSource, name, props]); + return ( - +