Skip to content
Merged
35 changes: 24 additions & 11 deletions packages/@react-aria/interactions/src/usePress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ class PressEvent implements IPressEvent {
}

const LINK_CLICKED = Symbol('linkClicked');
const STYLE_ID = 'react-aria-pressable-style';
const PRESSABLE_ATTRIBUTE = 'data-react-aria-pressable';

/**
* Handles press interactions across mouse, touch, keyboard, and screen readers.
Expand Down Expand Up @@ -815,17 +817,28 @@ export function usePress(props: PressHookProps): PressResult {

// Avoid onClick delay for double tap to zoom by default.
useEffect(() => {
let element = domRef?.current;
if (element && (element instanceof getOwnerWindow(element).Element)) {
// Only apply touch-action if not already set by another CSS rule.
let style = getOwnerWindow(element).getComputedStyle(element);
if (style.touchAction === 'auto') {
// touchAction: 'manipulation' is supposed to be equivalent, but in
// Safari it causes onPointerCancel not to fire on scroll.
// https://bugs.webkit.org/show_bug.cgi?id=240917
(element as HTMLElement).style.touchAction = 'pan-x pan-y pinch-zoom';
}
if (!domRef || process.env.NODE_ENV === 'test') {
return;
}

const ownerDocument = getOwnerDocument(domRef.current);
if (!ownerDocument || !ownerDocument.head || ownerDocument.getElementById(STYLE_ID)) {
return;
}

const style = ownerDocument.createElement('style');
style.id = STYLE_ID;
// touchAction: 'manipulation' is supposed to be equivalent, but in
// Safari it causes onPointerCancel not to fire on scroll.
// https://bugs.webkit.org/show_bug.cgi?id=240917
style.textContent = `
@layer {
[${PRESSABLE_ATTRIBUTE}] {
touch-action: pan-x pan-y pinch-zoom;
}
}
`.trim();
ownerDocument.head.prepend(style);
}, [domRef]);

// Remove user-select: none in case component unmounts immediately after pressStart
Expand All @@ -844,7 +857,7 @@ export function usePress(props: PressHookProps): PressResult {

return {
isPressed: isPressedProp || isPressed,
pressProps: mergeProps(domProps, pressProps)
pressProps: mergeProps(domProps, pressProps, {[PRESSABLE_ATTRIBUTE]: true})
};
}

Expand Down
37 changes: 37 additions & 0 deletions packages/react-aria-components/stories/Button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,40 @@ function RippleButton(props) {
</Button>
);
}

function ButtonPerformanceExample() {
const [count, setCount] = useState(0);
const [showButtons, setShowButtons] = useState(false);

const handlePress = () => {
if (!showButtons) {
setShowButtons(true);
} else {
setCount(count + 1);
}
};

return (
<div>
<Button style={{marginTop: 24, marginBottom: 16}} onPress={handlePress}>
{showButtons ? 'Re-render' : 'Render'}
</Button>
{showButtons && (
<div style={{display: 'flex', gap: 2, flexWrap: 'wrap'}} key={count}>
{new Array(20000).fill(0).map((_, i) => (
<Button key={i}>Press me</Button>
))}
</div>
)}
</div>
);
}

export const ButtonPerformance = {
render: (args) => <ButtonPerformanceExample {...args} />,
parameters: {
description: {
data: 'When usePress is used on the page, there should be a <style> tag placed in the head of the document that applies touch-action: pan-x pan-y pinch-zoom to the [data-react-aria-pressable] elements.'
}
}
};