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
8 changes: 7 additions & 1 deletion packages/@react-aria/calendar/src/useCalendarCell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,13 @@ export function useCalendarCell(props: AriaCalendarCellProps, state: CalendarSta
// outside the original pressed element.
// (JSDOM does not support this)
if ('releasePointerCapture' in e.target) {
e.target.releasePointerCapture(e.pointerId);
if ('hasPointerCapture' in e.target) {
if (e.target.hasPointerCapture(e.pointerId)) {
e.target.releasePointerCapture(e.pointerId);
}
} else {
e.target.releasePointerCapture(e.pointerId);
}
}
},
onContextMenu(e) {
Expand Down
8 changes: 7 additions & 1 deletion packages/@react-aria/interactions/src/usePress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,13 @@ export function usePress(props: PressHookProps): PressResult {
// This enables onPointerLeave and onPointerEnter to fire.
let target = getEventTarget(e.nativeEvent);
if ('releasePointerCapture' in target) {
target.releasePointerCapture(e.pointerId);
if ('hasPointerCapture' in target) {
if (target.hasPointerCapture(e.pointerId)) {
target.releasePointerCapture(e.pointerId);
}
} else {
(target as Element).releasePointerCapture(e.pointerId);
}
}
}

Expand Down
18 changes: 16 additions & 2 deletions packages/@react-aria/interactions/test/usePress.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -420,8 +420,10 @@ describe('usePress', function () {

let el = res.getByText('test');
el.releasePointerCapture = jest.fn();
el.hasPointerCapture = jest.fn().mockReturnValue(true);
fireEvent(el, pointerEvent('pointerdown', {pointerId: 1, pointerType: 'mouse', clientX: 0, clientY: 0}));
expect(el.releasePointerCapture).toHaveBeenCalled();
expect(el.hasPointerCapture).toHaveBeenCalledWith(1);
expect(el.releasePointerCapture).toHaveBeenCalledWith(1);
// react listens for pointerout and pointerover instead of pointerleave and pointerenter...
fireEvent(el, pointerEvent('pointerout', {pointerId: 1, pointerType: 'mouse', clientX: 100, clientY: 100}));
fireEvent(document, pointerEvent('pointerup', {pointerId: 1, pointerType: 'mouse', clientX: 100, clientY: 100}));
Expand Down Expand Up @@ -560,6 +562,16 @@ describe('usePress', function () {
]);
});

it('should not call releasePointerCapture when hasPointerCapture returns false', function () {
let res = render(<Example />);
let el = res.getByText('test');
el.releasePointerCapture = jest.fn();
el.hasPointerCapture = jest.fn().mockReturnValue(false);
fireEvent(el, pointerEvent('pointerdown', {pointerId: 1, pointerType: 'mouse', clientX: 0, clientY: 0}));
expect(el.hasPointerCapture).toHaveBeenCalledWith(1);
expect(el.releasePointerCapture).not.toHaveBeenCalled();
});

it('should handle pointer cancel events', function () {
let events = [];
let addEvent = (e) => events.push(e);
Expand Down Expand Up @@ -4011,8 +4023,10 @@ describe('usePress', function () {

const el = shadowRoot.getElementById('testElement');
el.releasePointerCapture = jest.fn();
el.hasPointerCapture = jest.fn().mockReturnValue(true);
fireEvent(el, pointerEvent('pointerdown', {pointerId: 1, pointerType: 'mouse', clientX: 0, clientY: 0}));
expect(el.releasePointerCapture).toHaveBeenCalled();
expect(el.hasPointerCapture).toHaveBeenCalledWith(1);
expect(el.releasePointerCapture).toHaveBeenCalledWith(1);
// react listens for pointerout and pointerover instead of pointerleave and pointerenter...
fireEvent(el, pointerEvent('pointerout', {pointerId: 1, pointerType: 'mouse', clientX: 100, clientY: 100}));
fireEvent(document, pointerEvent('pointerup', {pointerId: 1, pointerType: 'mouse', clientX: 100, clientY: 100}));
Expand Down
7 changes: 6 additions & 1 deletion packages/@react-aria/overlays/src/ariaHideOutside.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,12 @@ export function ariaHideOutside(targets: Element[], options?: AriaHideOutsideOpt

// If the parent element of the added nodes is not within one of the targets,
// and not already inside a hidden node, hide all of the new children.
if (![...visibleNodes, ...hiddenNodes].some(node => node.contains(change.target))) {
if (
change.target.isConnected &&
![...visibleNodes, ...hiddenNodes].some((node) =>
node.contains(change.target)
)
) {
for (let node of change.addedNodes) {
if (
(node instanceof HTMLElement || node instanceof SVGElement) &&
Expand Down
72 changes: 71 additions & 1 deletion packages/@react-aria/overlays/test/ariaHideOutside.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import {act, render, waitFor} from '@react-spectrum/test-utils-internal';
import {ariaHideOutside} from '../src';
import React, {useState} from 'react';
import React, {useRef, useState} from 'react';

describe('ariaHideOutside', function () {
it('should hide everything except the provided element [button]', function () {
Expand Down Expand Up @@ -275,6 +275,76 @@ describe('ariaHideOutside', function () {
expect(() => getByTestId('test')).not.toThrow();
});

it('should handle when a new element is added and then reparented', async function () {

let Test = () => {
const ref = useRef(null);
const mutate = () => {
let parent = document.createElement('ul');
let child = document.createElement('li');
ref.current.append(parent);
parent.appendChild(child);
parent.remove(); // this results in a mutation record for a disconnected ul with a connected li (through the new ul parent) in `addedNodes`
let newParent = document.createElement('ul');
newParent.appendChild(child);
ref.current.append(newParent);
};

return (
<>
<div data-testid="test" ref={ref}>
<button onClick={mutate}>Mutate</button>
</div>
</>
);
};

let {queryByRole, getAllByRole, getByTestId} = render(<Test />);

ariaHideOutside([getByTestId('test')]);

queryByRole('button').click();
await Promise.resolve(); // Wait for mutation observer tick

expect(getAllByRole('listitem')).toHaveLength(1);
});

it('should handle when a new element is added and then reparented to a hidden container', async function () {

let Test = () => {
const ref = useRef(null);
const mutate = () => {
let parent = document.createElement('ul');
let child = document.createElement('li');
ref.current.append(parent);
parent.appendChild(child);
parent.remove(); // this results in a mutation record for a disconnected ul with a connected li (through the new ul parent) in `addedNodes`
let newParent = document.createElement('ul');
newParent.appendChild(child);
ref.current.append(newParent);
};

return (
<>
<div data-testid="test">
<button onClick={mutate}>Mutate</button>
</div>
<div data-testid="sibling" ref={ref} />
</>
);
};

let {queryByRole, queryAllByRole, getByTestId} = render(<Test />);

ariaHideOutside([getByTestId('test')]);

queryByRole('button').click();
await Promise.resolve(); // Wait for mutation observer tick

expect(queryAllByRole('listitem')).toHaveLength(0);
});


it('work when called multiple times', function () {
let {getByRole, getAllByRole} = render(
<>
Expand Down
7 changes: 3 additions & 4 deletions packages/@react-aria/selection/src/useSelectableCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
disallowTypeAhead = false,
shouldUseVirtualFocus,
allowsTabNavigation = false,
isVirtualized,
// If no scrollRef is provided, assume the collection ref is the scrollable region
scrollRef = ref,
linkBehavior = 'action'
Expand Down Expand Up @@ -328,7 +327,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
// Store the scroll position so we can restore it later.
/// TODO: should this happen all the time??
let scrollPos = useRef({top: 0, left: 0});
useEvent(scrollRef, 'scroll', isVirtualized ? undefined : () => {
useEvent(scrollRef, 'scroll', () => {
scrollPos.current = {
top: scrollRef.current?.scrollTop ?? 0,
left: scrollRef.current?.scrollLeft ?? 0
Expand Down Expand Up @@ -369,7 +368,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
} else {
navigateToKey(manager.firstSelectedKey ?? delegate.getFirstKey?.());
}
} else if (!isVirtualized && scrollRef.current) {
} else if (scrollRef.current) {
// Restore the scroll position to what it was before.
scrollRef.current.scrollTop = scrollPos.current.top;
scrollRef.current.scrollLeft = scrollPos.current.left;
Expand Down Expand Up @@ -581,7 +580,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
// This will be marshalled to either the first or last item depending on where focus came from.
let tabIndex: number | undefined = undefined;
if (!shouldUseVirtualFocus) {
tabIndex = manager.focusedKey == null ? 0 : -1;
tabIndex = manager.isFocused ? -1 : 0;
}

let collectionId = useCollectionId(manager.collection);
Expand Down
6 changes: 2 additions & 4 deletions packages/@react-spectrum/s2/src/ComboBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export interface ComboboxStyleProps {
size?: 'S' | 'M' | 'L' | 'XL'
}
export interface ComboBoxProps<T extends object> extends
Omit<AriaComboBoxProps<T>, 'children' | 'style' | 'className' | 'defaultFilter' | 'allowsEmptyCollection' | keyof GlobalDOMAttributes>,
Omit<AriaComboBoxProps<T>, 'children' | 'style' | 'className' | 'defaultFilter' | 'allowsEmptyCollection' | 'isTriggerUpWhenOpen' | keyof GlobalDOMAttributes>,
ComboboxStyleProps,
StyleProps,
SpectrumLabelableProps,
Expand Down Expand Up @@ -354,6 +354,7 @@ export const ComboBox = /*#__PURE__*/ (forwardRef as forwardRefType)(function Co
return (
<AriaComboBox
{...comboBoxProps}
isTriggerUpWhenOpen
allowsEmptyCollection
style={UNSAFE_style}
className={UNSAFE_className + style(field(), getAllowedOverrides())({
Expand Down Expand Up @@ -643,9 +644,6 @@ const ComboboxInner = forwardRef(function ComboboxInner(props: ComboBoxProps<any
)}
<Button
ref={buttonRef}
// Prevent press scale from sticking while ComboBox is open.
// @ts-ignore
isPressed={false}
style={renderProps => pressScale(buttonRef)(renderProps)}
className={renderProps => inputButton({
...renderProps,
Expand Down
6 changes: 2 additions & 4 deletions packages/@react-spectrum/s2/src/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import {useSpectrumContextProps} from './useSpectrumContextProps';


export interface DatePickerProps<T extends DateValue> extends
Omit<AriaDatePickerProps<T>, 'children' | 'className' | 'style' | keyof GlobalDOMAttributes>,
Omit<AriaDatePickerProps<T>, 'children' | 'className' | 'style' | 'isTriggerUpWhenOpen' | keyof GlobalDOMAttributes>,
Pick<CalendarProps<T>, 'createCalendar' | 'pageBehavior' | 'firstDayOfWeek' | 'isDateUnavailable'>,
Pick<PopoverProps, 'shouldFlip'>,
StyleProps,
Expand Down Expand Up @@ -155,6 +155,7 @@ export const DatePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(function
ref={ref}
isRequired={isRequired}
{...dateFieldProps}
isTriggerUpWhenOpen
style={UNSAFE_style}
className={(UNSAFE_className || '') + style(field(), getAllowedOverrides())({
isInForm: !!formContext,
Expand Down Expand Up @@ -277,9 +278,6 @@ export function CalendarButton(props: {isOpen: boolean, size: 'S' | 'M' | 'L' |
return (
<Button
ref={buttonRef}
// Prevent press scale from sticking while DatePicker is open.
// @ts-ignore
isPressed={false}
onFocusChange={setButtonHasFocus}
style={pressScale(buttonRef)}
className={renderProps => inputButton({
Expand Down
3 changes: 2 additions & 1 deletion packages/@react-spectrum/s2/src/DateRangePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {useSpectrumContextProps} from './useSpectrumContextProps';


export interface DateRangePickerProps<T extends DateValue> extends
Omit<AriaDateRangePickerProps<T>, 'children' | 'className' | 'style' | keyof GlobalDOMAttributes>,
Omit<AriaDateRangePickerProps<T>, 'children' | 'className' | 'style' | 'isTriggerUpWhenOpen' | keyof GlobalDOMAttributes>,
Pick<RangeCalendarProps<T>, 'createCalendar' | 'pageBehavior' | 'firstDayOfWeek' | 'isDateUnavailable'>,
Pick<PopoverProps, 'shouldFlip'>,
StyleProps,
Expand Down Expand Up @@ -89,6 +89,7 @@ export const DateRangePicker = /*#__PURE__*/ (forwardRef as forwardRefType)(func
ref={ref}
isRequired={isRequired}
{...dateFieldProps}
isTriggerUpWhenOpen
style={UNSAFE_style}
className={(UNSAFE_className || '') + style(field(), getAllowedOverrides())({
isInForm: !!formContext,
Expand Down
11 changes: 2 additions & 9 deletions packages/@react-spectrum/s2/src/DialogTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@
*/

import {DialogTrigger as AriaDialogTrigger, DialogTriggerProps as AriaDialogTriggerProps} from 'react-aria-components';
import {PressResponder} from '@react-aria/interactions';
import {ReactNode} from 'react';

export interface DialogTriggerProps extends AriaDialogTriggerProps {}
export type DialogTriggerProps = Omit<AriaDialogTriggerProps, 'isTriggerUpWhenOpen'>;

/**
* DialogTrigger serves as a wrapper around a Dialog and its associated trigger, linking the Dialog's
Expand All @@ -23,12 +22,6 @@ export interface DialogTriggerProps extends AriaDialogTriggerProps {}
*/
export function DialogTrigger(props: DialogTriggerProps): ReactNode {
return (
<AriaDialogTrigger {...props}>
{/* RAC sets isPressed via PressResponder when the dialog is open.
We don't want press scaling to appear to get "stuck", so override this. */}
<PressResponder isPressed={false}>
{props.children}
</PressResponder>
</AriaDialogTrigger>
<AriaDialogTrigger {...props} isTriggerUpWhenOpen />
);
}
4 changes: 1 addition & 3 deletions packages/@react-spectrum/s2/src/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import {useSpectrumContextProps} from './useSpectrumContextProps';
// viewbox on LinkOut is super weird just because i copied the icon from designs...
// need to strip id's from icons

export interface MenuTriggerProps extends AriaMenuTriggerProps {
export interface MenuTriggerProps extends Omit<AriaMenuTriggerProps, 'isTriggerUpWhenOpen'> {
/**
* Alignment of the menu relative to the trigger.
*
Expand Down Expand Up @@ -547,8 +547,6 @@ export function MenuItem(props: MenuItemProps): ReactNode {
* linking the Menu's open state with the trigger's press state.
*/
function MenuTrigger(props: MenuTriggerProps): ReactNode {
// RAC sets isPressed via PressResponder when the menu is open.
// We don't want press scaling to appear to get "stuck", so override this.
// For mouse interactions, menus open on press start. When the popover underlay appears
// it covers the trigger button, causing onPressEnd to fire immediately and no press scaling
// to occur. We override this by listening for pointerup on the document ourselves.
Expand Down
6 changes: 2 additions & 4 deletions packages/@react-spectrum/s2/src/Picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export interface PickerStyleProps {

type SelectionMode = 'single' | 'multiple';
export interface PickerProps<T extends object, M extends SelectionMode = 'single'> extends
Omit<AriaSelectProps<T, M>, 'children' | 'style' | 'className' | 'allowsEmptyCollection' | keyof GlobalDOMAttributes>,
Omit<AriaSelectProps<T, M>, 'children' | 'style' | 'className' | 'allowsEmptyCollection' | 'isTriggerUpWhenOpen' | keyof GlobalDOMAttributes>,
PickerStyleProps,
StyleProps,
SpectrumLabelableProps,
Expand Down Expand Up @@ -351,6 +351,7 @@ export const Picker = /*#__PURE__*/ (forwardRef as forwardRefType)(function Pick
return (
<AriaSelect
{...pickerProps}
isTriggerUpWhenOpen
aria-describedby={spinnerId}
placeholder={placeholder}
style={UNSAFE_style}
Expand Down Expand Up @@ -522,9 +523,6 @@ const PickerButton = createHideableComponent(function PickerButton<T extends obj
<Button
ref={buttonRef}
style={renderProps => pressScale(buttonRef)(renderProps)}
// Prevent press scale from sticking while Picker is open.
// @ts-ignore
isPressed={false}
className={renderProps => inputButton({
...renderProps,
size: size,
Expand Down
Loading
Loading