e render props for TrapFocus (#1755)

This commit is contained in:
Kiwi
2018-07-20 19:51:13 -03:00
committed by Wyatt Johnson
parent 6bbb645552
commit afe816ba41
4 changed files with 55 additions and 49 deletions
@@ -2,7 +2,7 @@
border-radius: var(--round-corners);
background-color: transparent;
position: relative;
display: flex;
display: inline-flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
@@ -5,6 +5,7 @@ menu: UI Kit
import { Playground } from 'docz'
import TrapFocus from './TrapFocus'
import Button from '../Button'
# TrapFocus
@@ -12,28 +13,15 @@ Traps focus inside component when using keyboard navigation for accessibility pu
## Basic usage
```ts
import React from "react";
class Dialog extends React.Component {
private firstFocusable: HTMLElement;
private lastFocusable: HTMLElement;
private setFirstFocusable = (ref: HTMLElement) => (this.firstFocusable = ref);
private setLastFocusable = (ref: HTMLElement) => (this.lastFocusable = ref);
public render() {
return (
<Playground>
<div>
<TrapFocus>
{({firstFocusableRef, lastFocusableRef}) =>
<div>
<TrapFocus
firstFocusable={this.firstFocusable}
lastFocusable={this.lastFocusable}
>
<TextField ref={this.setFirstFocusable} />
<Button ref={this.setLastFocusable}>Send</Button>
</TrapFocus>
<Button forwardRef={firstFocusableRef} autoFocus>Cancel</Button>
<Button forwardRef={lastFocusableRef}>Send</Button>
</div>
);
}
}
```
}
</TrapFocus>
</div>
</Playground>
@@ -5,11 +5,16 @@ import Sinon from "sinon";
import { PropTypesOf } from "talk-ui/types";
import TrapFocus from "./TrapFocus";
const FakeFocusable: any = class extends React.Component {
public focus = Sinon.spy();
public render() {
return null;
}
};
it("renders correctly", () => {
const props: PropTypesOf<TrapFocus> = {
firstFocusable: null,
lastFocusable: null,
children: (
children: () => (
<>
<span>child1</span>
<span>child2</span>
@@ -25,15 +30,13 @@ it("renders correctly", () => {
});
it("Change focus to `lastFocusable` when focus reaches beginning", () => {
const fakeHTMLElementBegin = { focus: Sinon.spy() };
const fakeHTMLElementEnd = { focus: Sinon.spy() };
const props: PropTypesOf<TrapFocus> = {
firstFocusable: fakeHTMLElementBegin as any,
lastFocusable: fakeHTMLElementEnd as any,
children: (
children: ({ firstFocusableRef, lastFocusableRef }) => (
<>
<FakeFocusable ref={firstFocusableRef} />
<span>child1</span>
<span>child2</span>
<FakeFocusable ref={lastFocusableRef} />
</>
),
};
@@ -43,20 +46,22 @@ it("Change focus to `lastFocusable` when focus reaches beginning", () => {
</div>
);
renderer.root.findAllByProps({ tabIndex: 0 })[0].props.onFocus();
expect(fakeHTMLElementBegin.focus.called).toBe(false);
expect(fakeHTMLElementEnd.focus.called).toBe(true);
expect(
renderer.root.findAllByType(FakeFocusable)[0].instance.focus.called
).toBe(false);
expect(
renderer.root.findAllByType(FakeFocusable)[1].instance.focus.called
).toBe(true);
});
it("Change focus to `firstFocusable` when focus reaches the end", () => {
const fakeHTMLElementBegin = { focus: Sinon.spy() };
const fakeHTMLElementEnd = { focus: Sinon.spy() };
it("Change focus to `firstFocusable` when focus reaches end", () => {
const props: PropTypesOf<TrapFocus> = {
firstFocusable: fakeHTMLElementBegin as any,
lastFocusable: fakeHTMLElementEnd as any,
children: (
children: ({ firstFocusableRef, lastFocusableRef }) => (
<>
<FakeFocusable ref={firstFocusableRef} />
<span>child1</span>
<span>child2</span>
<FakeFocusable ref={lastFocusableRef} />
</>
),
};
@@ -66,6 +71,10 @@ it("Change focus to `firstFocusable` when focus reaches the end", () => {
</div>
);
renderer.root.findAllByProps({ tabIndex: 0 })[1].props.onFocus();
expect(fakeHTMLElementBegin.focus.called).toBe(true);
expect(fakeHTMLElementEnd.focus.called).toBe(false);
expect(
renderer.root.findAllByType(FakeFocusable)[0].instance.focus.called
).toBe(true);
expect(
renderer.root.findAllByType(FakeFocusable)[1].instance.focus.called
).toBe(false);
});
@@ -1,25 +1,34 @@
import React from "react";
import React, { RefObject } from "react";
export interface Focusable {
focus: () => void;
}
interface RenderProps {
firstFocusableRef: RefObject<Focusable>;
lastFocusableRef: RefObject<Focusable>;
}
export interface TrapFocusProps {
firstFocusable: Focusable | null;
lastFocusable: Focusable | null;
children: React.ReactNode;
children: (props: RenderProps) => React.ReactNode;
}
export default class TrapFocus extends React.Component<TrapFocusProps> {
private firstFocusableRef = React.createRef<Focusable>();
private lastFocusableRef = React.createRef<Focusable>();
// Trap keyboard focus inside the dropdown until a value has been chosen.
public focusBegin = () => this.props.firstFocusable!.focus();
public focusEnd = () => this.props.lastFocusable!.focus();
private focusBegin = () => this.firstFocusableRef.current!.focus();
private focusEnd = () => this.lastFocusableRef.current!.focus();
public render() {
return (
<>
<div tabIndex={0} onFocus={this.focusEnd} />
{this.props.children}
{this.props.children({
firstFocusableRef: this.firstFocusableRef,
lastFocusableRef: this.lastFocusableRef,
})}
<div tabIndex={0} onFocus={this.focusBegin} />
</>
);