mirror of
https://github.com/zoriya/react-native-web.git
synced 2026-06-04 11:04:58 +00:00
[fix] Pressable cursor and touch-action styles
Also add unit tests for Pressable. Fix #1764
This commit is contained in:
+187
@@ -0,0 +1,187 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`components/Pressable default 1`] = `
|
||||||
|
<div
|
||||||
|
class="css-view-1dbjc4n r-cursor-1loqt21 r-touchAction-1otgn73"
|
||||||
|
data-focusable="true"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/Pressable focus interaction 1`] = `
|
||||||
|
<div
|
||||||
|
class="css-view-1dbjc4n r-cursor-1loqt21 r-touchAction-1otgn73"
|
||||||
|
data-focusable="true"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/Pressable focus interaction 2`] = `
|
||||||
|
<div
|
||||||
|
class="css-view-1dbjc4n r-cursor-1loqt21 r-touchAction-1otgn73"
|
||||||
|
data-focusable="true"
|
||||||
|
data-focusvisible-polyfill="true"
|
||||||
|
style="outline: focus-ring;"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-testid="focus-content"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/Pressable focus interaction 3`] = `
|
||||||
|
<div
|
||||||
|
class="css-view-1dbjc4n r-cursor-1loqt21 r-touchAction-1otgn73"
|
||||||
|
data-focusable="true"
|
||||||
|
style=""
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/Pressable hover interaction 1`] = `
|
||||||
|
<div
|
||||||
|
class="css-view-1dbjc4n r-cursor-1loqt21 r-touchAction-1otgn73"
|
||||||
|
data-focusable="true"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/Pressable hover interaction 2`] = `
|
||||||
|
<div
|
||||||
|
class="css-view-1dbjc4n r-cursor-1loqt21 r-touchAction-1otgn73"
|
||||||
|
data-focusable="true"
|
||||||
|
style="outline: hover-ring;"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-testid="hover-content"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/Pressable hover interaction 3`] = `
|
||||||
|
<div
|
||||||
|
class="css-view-1dbjc4n r-cursor-1loqt21 r-touchAction-1otgn73"
|
||||||
|
data-focusable="true"
|
||||||
|
style=""
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/Pressable press interaction 1`] = `
|
||||||
|
<div
|
||||||
|
class="css-view-1dbjc4n r-cursor-1loqt21 r-touchAction-1otgn73"
|
||||||
|
data-focusable="true"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/Pressable press interaction 2`] = `
|
||||||
|
<div
|
||||||
|
class="css-view-1dbjc4n r-cursor-1loqt21 r-touchAction-1otgn73"
|
||||||
|
data-focusable="true"
|
||||||
|
style="outline: press-ring;"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-testid="press-content"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/Pressable press interaction 3`] = `
|
||||||
|
<div
|
||||||
|
class="css-view-1dbjc4n r-cursor-1loqt21 r-touchAction-1otgn73"
|
||||||
|
data-focusable="true"
|
||||||
|
style=""
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/Pressable prop "accessibilityLabel" value is set 1`] = `
|
||||||
|
<div
|
||||||
|
aria-label="label"
|
||||||
|
class="css-view-1dbjc4n r-cursor-1loqt21 r-touchAction-1otgn73"
|
||||||
|
data-focusable="true"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/Pressable prop "accessibilityLiveRegion" value is set 1`] = `
|
||||||
|
<div
|
||||||
|
aria-live="polite"
|
||||||
|
class="css-view-1dbjc4n r-cursor-1loqt21 r-touchAction-1otgn73"
|
||||||
|
data-focusable="true"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/Pressable prop "accessibilityRole" value alters HTML element 1`] = `
|
||||||
|
<a
|
||||||
|
class="css-reset-4rbku5 css-cursor-18t94o4 css-view-1dbjc4n r-cursor-1loqt21 r-touchAction-1otgn73"
|
||||||
|
data-focusable="true"
|
||||||
|
role="link"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/Pressable prop "accessibilityRole" value is "button" 1`] = `
|
||||||
|
<div
|
||||||
|
class="css-cursor-18t94o4 css-view-1dbjc4n r-cursor-1loqt21 r-touchAction-1otgn73"
|
||||||
|
data-focusable="true"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/Pressable prop "accessibilityRole" value is set 1`] = `
|
||||||
|
<div
|
||||||
|
class="css-view-1dbjc4n r-cursor-1loqt21 r-touchAction-1otgn73"
|
||||||
|
data-focusable="true"
|
||||||
|
role="presentation"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/Pressable prop "disabled" 1`] = `
|
||||||
|
<div
|
||||||
|
aria-disabled="true"
|
||||||
|
class="css-view-1dbjc4n r-cursor-1loqt21 r-touchAction-1otgn73"
|
||||||
|
disabled=""
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/Pressable prop "nativeID" value is set 1`] = `
|
||||||
|
<div
|
||||||
|
class="css-view-1dbjc4n r-cursor-1loqt21 r-touchAction-1otgn73"
|
||||||
|
data-focusable="true"
|
||||||
|
id="nativeID"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/Pressable prop "pointerEvents" 1`] = `
|
||||||
|
<div
|
||||||
|
class="css-view-1dbjc4n r-cursor-1loqt21 r-pointerEvents-ah5dr5 r-touchAction-1otgn73"
|
||||||
|
data-focusable="true"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/Pressable prop "style" value is set 1`] = `
|
||||||
|
<div
|
||||||
|
class="css-view-1dbjc4n r-cursor-1loqt21 r-touchAction-1otgn73"
|
||||||
|
data-focusable="true"
|
||||||
|
style="border-top-width: 5px; border-right-width: 5px; border-bottom-width: 5px; border-left-width: 5px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`components/Pressable prop "testID" value is set 1`] = `
|
||||||
|
<div
|
||||||
|
class="css-view-1dbjc4n r-cursor-1loqt21 r-touchAction-1otgn73"
|
||||||
|
data-focusable="true"
|
||||||
|
data-testid="123"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
/* eslint-env jasmine, jest */
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import Pressable from '../';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createEventTarget } from 'dom-event-testing-library';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
|
||||||
|
describe('components/Pressable', () => {
|
||||||
|
test('default', () => {
|
||||||
|
const { container } = render(<Pressable />);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('prop "accessibilityLabel"', () => {
|
||||||
|
test('value is set', () => {
|
||||||
|
const { container } = render(<Pressable accessibilityLabel="label" />);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('prop "accessibilityLiveRegion"', () => {
|
||||||
|
test('value is set', () => {
|
||||||
|
const { container } = render(<Pressable accessibilityLiveRegion="polite" />);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('prop "accessibilityRole"', () => {
|
||||||
|
test('value is set', () => {
|
||||||
|
const { container } = render(<Pressable accessibilityRole="none" />);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('value is "button"', () => {
|
||||||
|
const { container } = render(<Pressable accessibilityRole="button" />);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('value alters HTML element', () => {
|
||||||
|
const { container } = render(<Pressable accessibilityRole="link" />);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('prop "disabled"', () => {
|
||||||
|
const { container } = render(<Pressable disabled={true} />);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('prop "nativeID"', () => {
|
||||||
|
test('value is set', () => {
|
||||||
|
const { container } = render(<Pressable nativeID="nativeID" />);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('focus interaction', () => {
|
||||||
|
let container;
|
||||||
|
const onBlur = jest.fn();
|
||||||
|
const onFocus = jest.fn();
|
||||||
|
const ref = React.createRef();
|
||||||
|
act(() => {
|
||||||
|
({ container } = render(
|
||||||
|
<Pressable
|
||||||
|
children={({ focused }) => (focused ? <div data-testid="focus-content" /> : null)}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onFocus={onFocus}
|
||||||
|
ref={ref}
|
||||||
|
style={({ focused }) => [focused && { outline: 'focus-ring' }]}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
});
|
||||||
|
const target = createEventTarget(ref.current);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
act(() => {
|
||||||
|
target.focus();
|
||||||
|
});
|
||||||
|
expect(onFocus).toBeCalled();
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
act(() => {
|
||||||
|
target.blur();
|
||||||
|
});
|
||||||
|
expect(onBlur).toBeCalled();
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hover interaction', () => {
|
||||||
|
let container;
|
||||||
|
const ref = React.createRef();
|
||||||
|
act(() => {
|
||||||
|
({ container } = render(
|
||||||
|
<Pressable
|
||||||
|
children={({ hovered }) => (hovered ? <div data-testid="hover-content" /> : null)}
|
||||||
|
ref={ref}
|
||||||
|
style={({ hovered }) => [hovered && { outline: 'hover-ring' }]}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
});
|
||||||
|
const target = createEventTarget(ref.current);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
act(() => {
|
||||||
|
target.pointerover();
|
||||||
|
});
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
act(() => {
|
||||||
|
target.pointerout();
|
||||||
|
});
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('press interaction', () => {
|
||||||
|
let container;
|
||||||
|
const onPress = jest.fn();
|
||||||
|
const onPressIn = jest.fn();
|
||||||
|
const onPressOut = jest.fn();
|
||||||
|
const ref = React.createRef();
|
||||||
|
act(() => {
|
||||||
|
({ container } = render(
|
||||||
|
<Pressable
|
||||||
|
children={({ pressed }) => (pressed ? <div data-testid="press-content" /> : null)}
|
||||||
|
onPress={onPress}
|
||||||
|
onPressIn={onPressIn}
|
||||||
|
onPressOut={onPressOut}
|
||||||
|
ref={ref}
|
||||||
|
style={({ pressed }) => [pressed && { outline: 'press-ring' }]}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
});
|
||||||
|
const target = createEventTarget(ref.current);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
act(() => {
|
||||||
|
target.pointerdown({ button: 0 });
|
||||||
|
jest.runAllTimers();
|
||||||
|
});
|
||||||
|
expect(onPressIn).toBeCalled();
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
act(() => {
|
||||||
|
target.pointerup({ button: 0 });
|
||||||
|
jest.runAllTimers();
|
||||||
|
});
|
||||||
|
expect(onPressOut).toBeCalled();
|
||||||
|
expect(onPress).toBeCalled();
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('prop "ref"', () => {
|
||||||
|
test('value is set', () => {
|
||||||
|
const ref = jest.fn();
|
||||||
|
render(<Pressable ref={ref} />);
|
||||||
|
expect(ref).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('node has imperative methods', () => {
|
||||||
|
const ref = React.createRef();
|
||||||
|
act(() => {
|
||||||
|
render(<Pressable ref={ref} />);
|
||||||
|
});
|
||||||
|
const node = ref.current;
|
||||||
|
expect(typeof node.measure === 'function');
|
||||||
|
expect(typeof node.measureLayout === 'function');
|
||||||
|
expect(typeof node.measureInWindow === 'function');
|
||||||
|
expect(typeof node.setNativeProps === 'function');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('prop "pointerEvents"', () => {
|
||||||
|
const { container } = render(<Pressable pointerEvents="box-only" />);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('prop "style"', () => {
|
||||||
|
test('value is set', () => {
|
||||||
|
const { container } = render(<Pressable style={{ borderWidth: 5 }} />);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('prop "testID"', () => {
|
||||||
|
test('value is set', () => {
|
||||||
|
const { container } = render(<Pressable testID="123" />);
|
||||||
|
expect(container.firstChild).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+11
-3
@@ -18,6 +18,7 @@ import { forwardRef, memo, useMemo, useState, useRef } from 'react';
|
|||||||
import useMergeRefs from '../../modules/useMergeRefs';
|
import useMergeRefs from '../../modules/useMergeRefs';
|
||||||
import useHover from '../../modules/useHover';
|
import useHover from '../../modules/useHover';
|
||||||
import usePressEvents from '../../modules/usePressEvents';
|
import usePressEvents from '../../modules/usePressEvents';
|
||||||
|
import StyleSheet from '../StyleSheet';
|
||||||
import View from '../View';
|
import View from '../View';
|
||||||
|
|
||||||
export type StateCallbackType = $ReadOnly<{|
|
export type StateCallbackType = $ReadOnly<{|
|
||||||
@@ -157,7 +158,7 @@ function Pressable(props: Props, forwardedRef): React.Node {
|
|||||||
onBlur={createFocusHandler(onBlur, false)}
|
onBlur={createFocusHandler(onBlur, false)}
|
||||||
onFocus={createFocusHandler(onFocus, true)}
|
onFocus={createFocusHandler(onFocus, true)}
|
||||||
ref={setRef}
|
ref={setRef}
|
||||||
style={typeof style === 'function' ? style(interactionState) : style}
|
style={[styles.root, typeof style === 'function' ? style(interactionState) : style]}
|
||||||
>
|
>
|
||||||
{typeof children === 'function' ? children(interactionState) : children}
|
{typeof children === 'function' ? children(interactionState) : children}
|
||||||
</View>
|
</View>
|
||||||
@@ -165,10 +166,17 @@ function Pressable(props: Props, forwardedRef): React.Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function useForceableState(forced: boolean): [boolean, (boolean) => void] {
|
function useForceableState(forced: boolean): [boolean, (boolean) => void] {
|
||||||
const [pressed, setPressed] = useState(false);
|
const [bool, setBool] = useState(false);
|
||||||
return [pressed || forced, setPressed];
|
return [bool || forced, setBool];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
root: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
touchAction: 'manipulation'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const MemoedPressable = memo(forwardRef(Pressable));
|
const MemoedPressable = memo(forwardRef(Pressable));
|
||||||
MemoedPressable.displayName = 'Pressable';
|
MemoedPressable.displayName = 'Pressable';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user