diff --git a/packages/@react-spectrum/s2/src/SegmentedControl.tsx b/packages/@react-spectrum/s2/src/SegmentedControl.tsx index de31da5bde6..0dafd0cc08d 100644 --- a/packages/@react-spectrum/s2/src/SegmentedControl.tsx +++ b/packages/@react-spectrum/s2/src/SegmentedControl.tsx @@ -13,13 +13,13 @@ import {AriaLabelingProps, DOMRef, DOMRefValue, FocusableRef, Key} from '@react-types/shared'; import {baseColor, focusRing, style} from '../style' with {type: 'macro'}; import {centerBaseline} from './CenterBaseline'; -import {ContextValue, DEFAULT_SLOT, Provider, TextContext as RACTextContext, SlotProps, ToggleButton, ToggleButtonGroup, ToggleButtonRenderProps, ToggleGroupStateContext} from 'react-aria-components'; +import {ContextValue, DEFAULT_SLOT, Provider, TextContext as RACTextContext, SelectionIndicator, SlotProps, ToggleButton, ToggleButtonGroup, ToggleButtonRenderProps, ToggleGroupStateContext} from 'react-aria-components'; import {control, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'}; -import {createContext, forwardRef, ReactNode, RefObject, useCallback, useContext, useRef} from 'react'; +import {createContext, forwardRef, ReactNode, useCallback, useContext, useRef} from 'react'; import {IconContext} from './Icon'; import {pressScale} from './pressScale'; import {Text, TextContext} from './Content'; -import {useDOMRef, useFocusableRef, useMediaQuery} from '@react-spectrum/utils'; +import {useDOMRef, useFocusableRef} from '@react-spectrum/utils'; import {useLayoutEffect} from '@react-aria/utils'; import {useSpectrumContextProps} from './useSpectrumContextProps'; @@ -113,6 +113,13 @@ const slider = style<{isDisabled: boolean}>({ left: 0, width: 'full', height: 'full', + contain: 'strict', + transition: { + default: '[translate,width]', + '@media (prefers-reduced-motion: reduce)': 'none' + }, + transitionDuration: 200, + transitionTimingFunction: 'out', position: 'absolute', boxSizing: 'border-box', borderStyle: 'solid', @@ -130,8 +137,6 @@ const slider = style<{isDisabled: boolean}>({ interface InternalSegmentedControlContextProps { register?: (value: Key, isDisabled?: boolean) => void, - prevRef?: RefObject, - currentSelectedRef?: RefObject, isJustified?: boolean } @@ -139,8 +144,6 @@ interface DefaultSelectionTrackProps { defaultValue?: Key | null, value?: Key | null, children: ReactNode, - prevRef: RefObject, - currentSelectedRef: RefObject, isJustified?: boolean } @@ -158,14 +161,7 @@ export const SegmentedControl = /*#__PURE__*/ forwardRef(function SegmentedContr } = props; let domRef = useDOMRef(ref); - let prevRef = useRef(null); - let currentSelectedRef = useRef(null); - let onChange = (values: Set) => { - if (currentSelectedRef.current) { - prevRef.current = currentSelectedRef?.current.getBoundingClientRect(); - } - if (onSelectionChange) { let firstKey = values.values().next().value; if (firstKey != null) { @@ -186,7 +182,7 @@ export const SegmentedControl = /*#__PURE__*/ forwardRef(function SegmentedContr onSelectionChange={onChange} className={(props.UNSAFE_className || '') + segmentedControl(null, props.styles)} aria-label={props['aria-label']}> - + {props.children} @@ -209,7 +205,7 @@ function DefaultSelectionTracker(props: DefaultSelectionTrackProps) { return ( {props.children} @@ -222,46 +218,21 @@ function DefaultSelectionTracker(props: DefaultSelectionTrackProps) { export const SegmentedControlItem = /*#__PURE__*/ forwardRef(function SegmentedControlItem(props: SegmentedControlItemProps, ref: FocusableRef) { let domRef = useFocusableRef(ref); let divRef = useRef(null); - let {register, prevRef, currentSelectedRef, isJustified} = useContext(InternalSegmentedControlContext); - let state = useContext(ToggleGroupStateContext); - let isSelected = state?.selectedKeys.has(props.id); - // do not apply animation if a user has the prefers-reduced-motion setting - let reduceMotion = useMediaQuery('(prefers-reduced-motion: reduce)'); + let {register, isJustified} = useContext(InternalSegmentedControlContext); useLayoutEffect(() => { register?.(props.id); }, [register, props.id]); - useLayoutEffect(() => { - if (isSelected && prevRef?.current && currentSelectedRef?.current && !reduceMotion) { - let currentItem = currentSelectedRef?.current.getBoundingClientRect(); - - let deltaX = prevRef?.current.left - currentItem?.left; - - currentSelectedRef.current.animate( - [ - {transform: `translateX(${deltaX}px)`, width: `${prevRef?.current.width}px`}, - {transform: 'translateX(0px)', width: `${currentItem.width}px`} - ], - { - duration: 200, - easing: 'ease-out' - } - ); - - prevRef.current = null; - } - }, [isSelected, reduceMotion, prevRef, currentSelectedRef]); - return ( (props.UNSAFE_className || '') + controlItem({...renderProps, isJustified}, props.styles)} > - {({isSelected, isPressed, isDisabled}) => ( + {({isPressed, isDisabled}) => ( <> - {isSelected &&
} + , DOMRefValue>>(null); const InternalTabsContext = createContext & { tablistRef?: RefObject, - prevRef?: RefObject, selectedKey?: Key | null }>({}); @@ -133,14 +133,6 @@ export const Tabs = forwardRef(function Tabs(props: TabsProps, ref: DOMRef(null); - let prevRef = useRef(null); - - let onChange = useEffectEvent((val: Key) => { - if (tablistRef.current) { - prevRef.current = tablistRef.current.querySelector('[role=tab][data-selected=true]')?.getBoundingClientRect() ?? null; - } - setValue(val); - }); return ( )} @@ -294,6 +285,13 @@ const selectedIndicator = style<{isDisabled: boolean, orientation?: Orientation} vertical: '[2px]' } }, + contain: 'strict', + transition: { + default: '[translate,width,height]', + '@media (prefers-reduced-motion: reduce)': 'none' + }, + transitionDuration: 200, + transitionTimingFunction: 'out', bottom: { default: 0 }, @@ -374,7 +372,7 @@ const icon = style({ }); export function Tab(props: TabProps): ReactNode { - let {density, orientation, labelBehavior, prevRef} = useContext(InternalTabsContext) ?? {}; + let {density, orientation, labelBehavior} = useContext(InternalTabsContext) ?? {}; let contentId = useId(); let ariaLabelledBy = props['aria-labelledby'] || ''; @@ -390,7 +388,6 @@ export function Tab(props: TabProps): ReactNode { {({ // @ts-ignore isMenu, - isSelected, isDisabled }) => { if (isMenu) { @@ -417,10 +414,8 @@ export function Tab(props: TabProps): ReactNode { }] ]}> + isDisabled={isDisabled}> {typeof props.children === 'string' ? {props.children} : props.children} @@ -431,53 +426,17 @@ export function Tab(props: TabProps): ReactNode { ); } -function TabInner({isSelected, isDisabled, orientation, children, prevRef}: { - isSelected: boolean, +function TabInner({isDisabled, orientation, children}: { isDisabled: boolean, orientation: Orientation, - children: ReactNode, - prevRef?: RefObject + children: ReactNode }) { - let reduceMotion = useMediaQuery('(prefers-reduced-motion: reduce)'); let ref = useRef(null); - - useLayoutEffect(() => { - if (isSelected && prevRef?.current && ref?.current && !reduceMotion) { - let currentItem = ref?.current.getBoundingClientRect(); - - if (orientation === 'horizontal') { - let deltaX = prevRef.current.left - currentItem.left; - ref.current.animate( - [ - {transform: `translateX(${deltaX}px)`, width: `${prevRef.current.width}px`}, - {transform: 'translateX(0px)', width: '100%'} - ], - { - duration: 200, - easing: 'ease-out' - } - ); - } else { - let deltaY = prevRef.current.top - currentItem.top; - ref.current.animate( - [ - {transform: `translateY(${deltaY}px)`, height: `${prevRef.current.height}px`}, - {transform: 'translateY(0px)', height: '100%'} - ], - { - duration: 200, - easing: 'ease-out' - } - ); - } - - prevRef.current = null; - } - }, [isSelected, reduceMotion, prevRef, orientation]); + let isHidden = useContext(HiddenTabsContext); return ( <> - {isSelected &&
} + {!isHidden && } {children} ); @@ -549,6 +508,8 @@ function isEveryTabDisabled(collection: Collection> | undefined, disa return false; } +const HiddenTabsContext = createContext(false); + let HiddenTabs = function (props: { listRef: RefObject, items: Array>, @@ -573,18 +534,20 @@ let HiddenTabs = function (props: { overflow: 'hidden', opacity: 0 })}> - {items.map((item) => { - // pull off individual props as an allow list, don't want refs or other props getting through - return ( -
- {item.props.children({size, density})} -
- ); - })} + + {items.map((item) => { + // pull off individual props as an allow list, don't want refs or other props getting through + return ( +
+ {item.props.children({size, density})} +
+ ); + })} +
); }; diff --git a/packages/dev/s2-docs/pages/react-aria/GridList.mdx b/packages/dev/s2-docs/pages/react-aria/GridList.mdx index 7fcb080411e..894d2be8efb 100644 --- a/packages/dev/s2-docs/pages/react-aria/GridList.mdx +++ b/packages/dev/s2-docs/pages/react-aria/GridList.mdx @@ -263,11 +263,11 @@ function Example() { -```tsx links={{GridList: '#gridlist', GridListItem: '#gridlistitem', GridListLoadMoreItem: '#gridlistloadmoreitem', Button: 'Button.html', Checkbox: 'Checkbox.html'}} +```tsx links={{GridList: '#gridlist', GridListItem: '#gridlistitem', GridListLoadMoreItem: '#gridlistloadmoreitem', Button: 'Button.html', Checkbox: 'Checkbox.html', SelectionIndicator: 'selection.html#animated-selectionindicator'}}