Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 15 additions & 44 deletions packages/@react-spectrum/s2/src/SegmentedControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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',
Expand All @@ -130,17 +137,13 @@ const slider = style<{isDisabled: boolean}>({

interface InternalSegmentedControlContextProps {
register?: (value: Key, isDisabled?: boolean) => void,
prevRef?: RefObject<DOMRect | null>,
currentSelectedRef?: RefObject<HTMLDivElement | null>,
isJustified?: boolean
}

interface DefaultSelectionTrackProps {
defaultValue?: Key | null,
value?: Key | null,
children: ReactNode,
prevRef: RefObject<DOMRect | null>,
currentSelectedRef: RefObject<HTMLDivElement | null>,
isJustified?: boolean
}

Expand All @@ -158,14 +161,7 @@ export const SegmentedControl = /*#__PURE__*/ forwardRef(function SegmentedContr
} = props;
let domRef = useDOMRef(ref);

let prevRef = useRef<DOMRect>(null);
let currentSelectedRef = useRef<HTMLDivElement>(null);

let onChange = (values: Set<Key>) => {
if (currentSelectedRef.current) {
prevRef.current = currentSelectedRef?.current.getBoundingClientRect();
}

if (onSelectionChange) {
let firstKey = values.values().next().value;
if (firstKey != null) {
Expand All @@ -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']}>
<DefaultSelectionTracker defaultValue={defaultSelectedKey} value={selectedKey} prevRef={prevRef} currentSelectedRef={currentSelectedRef} isJustified={props.isJustified}>
<DefaultSelectionTracker defaultValue={defaultSelectedKey} value={selectedKey} isJustified={props.isJustified}>
{props.children}
</DefaultSelectionTracker>
</ToggleButtonGroup>
Expand All @@ -209,7 +205,7 @@ function DefaultSelectionTracker(props: DefaultSelectionTrackProps) {
return (
<Provider
values={[
[InternalSegmentedControlContext, {register: register, prevRef: props.prevRef, currentSelectedRef: props.currentSelectedRef, isJustified: props.isJustified}]
[InternalSegmentedControlContext, {register: register, isJustified: props.isJustified}]
]}>
{props.children}
</Provider>
Expand All @@ -222,46 +218,21 @@ function DefaultSelectionTracker(props: DefaultSelectionTrackProps) {
export const SegmentedControlItem = /*#__PURE__*/ forwardRef(function SegmentedControlItem(props: SegmentedControlItemProps, ref: FocusableRef<HTMLButtonElement>) {
let domRef = useFocusableRef(ref);
let divRef = useRef<HTMLDivElement>(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 (
<ToggleButton
{...props}
ref={domRef}
style={props.UNSAFE_style}
className={renderProps => (props.UNSAFE_className || '') + controlItem({...renderProps, isJustified}, props.styles)} >
{({isSelected, isPressed, isDisabled}) => (
{({isPressed, isDisabled}) => (
<>
{isSelected && <div className={slider({isDisabled})} ref={currentSelectedRef} />}
<SelectionIndicator className={slider({isDisabled})} />
<Provider
values={[
[IconContext, {
Expand Down
103 changes: 33 additions & 70 deletions packages/@react-spectrum/s2/src/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
Tab as RACTab,
TabList as RACTabList,
Tabs as RACTabs,
SelectionIndicator,
TabListStateContext,
TabRenderProps
} from 'react-aria-components';
Expand All @@ -36,7 +37,7 @@ import {inertValue, useEffectEvent, useId, useLabels, useLayoutEffect, useResize
import {Picker, PickerItem} from './TabsPicker';
import {Text, TextContext} from './Content';
import {useControlledState} from '@react-stately/utils';
import {useDOMRef, useMediaQuery} from '@react-spectrum/utils';
import {useDOMRef} from '@react-spectrum/utils';
import {useHasTabbableChild} from '@react-aria/focus';
import {useLocale} from '@react-aria/i18n';
import {useSpectrumContextProps} from './useSpectrumContextProps';
Expand Down Expand Up @@ -79,7 +80,6 @@ export interface TabPanelProps extends Omit<AriaTabPanelProps, 'children' | 'sty
export const TabsContext = createContext<ContextValue<Partial<TabsProps>, DOMRefValue<HTMLDivElement>>>(null);
const InternalTabsContext = createContext<Partial<TabsProps> & {
tablistRef?: RefObject<HTMLDivElement | null>,
prevRef?: RefObject<DOMRect | null>,
selectedKey?: Key | null
}>({});

Expand Down Expand Up @@ -133,14 +133,6 @@ export const Tabs = forwardRef(function Tabs(props: TabsProps, ref: DOMRef<HTMLD
}

let tablistRef = useRef<HTMLDivElement | null>(null);
let prevRef = useRef<DOMRect | null>(null);

let onChange = useEffectEvent((val: Key) => {
if (tablistRef.current) {
prevRef.current = tablistRef.current.querySelector('[role=tab][data-selected=true]')?.getBoundingClientRect() ?? null;
}
setValue(val);
});

return (
<Provider
Expand All @@ -152,8 +144,7 @@ export const Tabs = forwardRef(function Tabs(props: TabsProps, ref: DOMRef<HTMLD
disabledKeys,
selectedKey: value,
tablistRef,
prevRef,
onSelectionChange: onChange,
onSelectionChange: setValue,
labelBehavior,
'aria-label': props['aria-label'],
'aria-labelledby': props['aria-labelledby']
Expand All @@ -164,7 +155,7 @@ export const Tabs = forwardRef(function Tabs(props: TabsProps, ref: DOMRef<HTMLD
<CollapsingTabs
{...props}
selectedKey={value}
onSelectionChange={onChange}
onSelectionChange={setValue}
collection={collection}
containerRef={domRef} />
)}
Expand Down Expand Up @@ -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
},
Expand Down Expand Up @@ -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'] || '';
Expand All @@ -390,7 +388,6 @@ export function Tab(props: TabProps): ReactNode {
{({
// @ts-ignore
isMenu,
isSelected,
isDisabled
}) => {
if (isMenu) {
Expand All @@ -417,10 +414,8 @@ export function Tab(props: TabProps): ReactNode {
}]
]}>
<TabInner
isSelected={isSelected}
orientation={orientation!}
isDisabled={isDisabled}
prevRef={prevRef}>
isDisabled={isDisabled}>
{typeof props.children === 'string' ? <Text>{props.children}</Text> : props.children}
</TabInner>
</Provider>
Expand All @@ -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<DOMRect | null>
children: ReactNode
}) {
let reduceMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
let ref = useRef<HTMLDivElement | null>(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 && <div ref={ref} className={selectedIndicator({isDisabled, orientation})} />}
{!isHidden && <SelectionIndicator ref={ref} className={selectedIndicator({isDisabled, orientation})} />}
{children}
</>
);
Expand Down Expand Up @@ -549,6 +508,8 @@ function isEveryTabDisabled<T>(collection: Collection<Node<T>> | undefined, disa
return false;
}

const HiddenTabsContext = createContext(false);

let HiddenTabs = function (props: {
listRef: RefObject<HTMLDivElement | null>,
items: Array<Node<any>>,
Expand All @@ -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 (
<div
data-hidden-tab
style={item.props.UNSAFE_style}
key={item.key}
className={item.props.className({size, density})}>
{item.props.children({size, density})}
</div>
);
})}
<HiddenTabsContext.Provider value>
{items.map((item) => {
// pull off individual props as an allow list, don't want refs or other props getting through
return (
<div
data-hidden-tab
style={item.props.UNSAFE_style}
key={item.key}
className={item.props.className({size, density})}>
{item.props.children({size, density})}
</div>
);
})}
</HiddenTabsContext.Provider>
</div>
);
};
Expand Down
4 changes: 2 additions & 2 deletions packages/dev/s2-docs/pages/react-aria/GridList.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -263,11 +263,11 @@ function Example() {

<Anatomy role="img" aria-label="Anatomy diagram of a list container, consisting of multiple list items. Each list item contains a drag button, a selection checkbox, an icon, a title, and a description." />

```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'}}
<GridList>
<GridListItem>
<Button slot="drag" />
<Checkbox slot="selection" />
<Checkbox slot="selection" /> or <SelectionIndicator />
</GridListItem>
<GridListLoadMoreItem />
</GridList>
Expand Down
3 changes: 2 additions & 1 deletion packages/dev/s2-docs/pages/react-aria/ListBox.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -372,11 +372,12 @@ function Example() {

<Anatomy role="img" aria-label="Anatomy diagram of a list container, consisting of multiple list items. Each list item contains a label and description. The items are grouped into a section with a header." />

```tsx links={{ListBox: '#listbox', ListBoxItem: '#listboxitem', ListBoxSection: '#listboxsection', ListBoxLoadMoreItem: '#listboxloadmoreitem'}}
```tsx links={{ListBox: '#listbox', ListBoxItem: '#listboxitem', ListBoxSection: '#listboxsection', ListBoxLoadMoreItem: '#listboxloadmoreitem', SelectionIndicator: 'selection.html#animated-selectionindicator'}}
<ListBox>
<ListBoxItem>
<Text slot="label" />
<Text slot="description" />
<SelectionIndicator />
</ListBoxItem>
<ListBoxSection>
<Header />
Expand Down
3 changes: 2 additions & 1 deletion packages/dev/s2-docs/pages/react-aria/Menu.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ import {MenuTrigger, Menu, MenuItem, Button, Popover} from 'react-aria-component

<Anatomy />

```tsx links={{MenuTrigger: '#menutrigger', Button: 'Button.html', Popover: 'Popover.html', Menu: '#menu', MenuItem: '#menuitem', Separator: 'Separator.html', MenuSection: '#menusection', SubmenuTrigger: '#submenutrigger'}}
```tsx links={{MenuTrigger: '#menutrigger', Button: 'Button.html', Popover: 'Popover.html', Menu: '#menu', MenuItem: '#menuitem', Separator: 'Separator.html', MenuSection: '#menusection', SubmenuTrigger: '#submenutrigger', SelectionIndicator: 'selection.html#animated-selectionindicator'}}
<MenuTrigger>
<Button />
<Popover>
Expand All @@ -478,6 +478,7 @@ import {MenuTrigger, Menu, MenuItem, Button, Popover} from 'react-aria-component
<Text slot="label" />
<Text slot="description" />
<Keyboard />
<SelectionIndicator />
</MenuItem>
<Separator />
<MenuSection>
Expand Down
6 changes: 4 additions & 2 deletions packages/dev/s2-docs/pages/react-aria/RadioGroup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,12 @@ import {Form} from 'react-aria-components';

<Anatomy />

```tsx links={{RadioGroup: '#radiogroup', Radio: '#radio'}}
```tsx links={{RadioGroup: '#radiogroup', Radio: '#radio', SelectionIndicator: 'selection.html#animated-selectionindicator'}}
<RadioGroup>
<Label />
<Radio />
<Radio>
<SelectionIndicator />
</Radio>
<Text slot="description" />
<FieldError />
</RadioGroup>
Expand Down
Loading