[fix] Pressable cursor and touch-action styles

Also add unit tests for Pressable.

Fix #1764
This commit is contained in:
Nicolas Gallagher
2020-10-12 13:37:42 -07:00
parent 78174d7b48
commit c2019a9881
3 changed files with 383 additions and 3 deletions
@@ -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
View File
@@ -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';