Implement TrapFocus Component

This commit is contained in:
Chi Vinh Le
2018-07-18 16:01:42 -03:00
parent a4104f8bd8
commit 90cb56d92f
7 changed files with 175 additions and 17 deletions
@@ -25,23 +25,21 @@ interface InnerProps {
const defaultFormatter: Formatter = (value, unit, suffix, timestamp: string) =>
new Date(timestamp).toISOString();
class RelativeTime extends React.Component<InnerProps> {
public render() {
const { date, classes, live, className, formatter } = this.props;
return (
<UIContext.Consumer>
{({ timeagoFormatter }) => (
<TimeAgo
date={date}
className={cn(className, classes.root)}
live={live}
formatter={timeagoFormatter || formatter || defaultFormatter}
/>
)}
</UIContext.Consumer>
);
}
}
const RelativeTime: React.StatelessComponent<InnerProps> = props => {
const { date, classes, live, className, formatter } = props;
return (
<UIContext.Consumer>
{({ timeagoFormatter }) => (
<TimeAgo
date={date}
className={cn(className, classes.root)}
live={live}
formatter={timeagoFormatter || formatter || defaultFormatter}
/>
)}
</UIContext.Consumer>
);
};
const enhanced = withForwardRef(withStyles(styles)(RelativeTime));
export type RelativeTimeProps = PropTypesOf<typeof enhanced>;
@@ -0,0 +1,39 @@
---
name: TrapFocus
menu: UI Kit
---
import { Playground } from 'docz'
import TrapFocus from './TrapFocus'
# TrapFocus
Traps focus inside component when using keyboard navigation for accessibility purposes.
## 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 (
<div>
<TrapFocus
firstFocusable={this.firstFocusable}
lastFocusable={this.lastFocusable}
>
<TextField ref={this.setFirstFocusable} />
<Button ref={this.setLastFocusable}>Send</Button>
</TrapFocus>
</div>
);
}
}
```
@@ -0,0 +1,71 @@
import React from "react";
import { create } from "react-test-renderer";
import Sinon from "sinon";
import { PropTypesOf } from "talk-ui/types";
import TrapFocus from "./TrapFocus";
it("renders correctly", () => {
const props: PropTypesOf<TrapFocus> = {
firstFocusable: null,
lastFocusable: null,
children: (
<>
<span>child1</span>
<span>child2</span>
</>
),
};
const renderer = create(
<div>
<TrapFocus {...props} />
</div>
);
expect(renderer.toJSON()).toMatchSnapshot();
});
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: (
<>
<span>child1</span>
<span>child2</span>
</>
),
};
const renderer = create(
<div>
<TrapFocus {...props} />
</div>
);
renderer.root.findAllByProps({ tabIndex: 0 })[0].props.onFocus();
expect(fakeHTMLElementBegin.focus.called).toBe(false);
expect(fakeHTMLElementEnd.focus.called).toBe(true);
});
it("Change focus to `firstFocusable` when focus reaches the end", () => {
const fakeHTMLElementBegin = { focus: Sinon.spy() };
const fakeHTMLElementEnd = { focus: Sinon.spy() };
const props: PropTypesOf<TrapFocus> = {
firstFocusable: fakeHTMLElementBegin as any,
lastFocusable: fakeHTMLElementEnd as any,
children: (
<>
<span>child1</span>
<span>child2</span>
</>
),
};
const renderer = create(
<div>
<TrapFocus {...props} />
</div>
);
renderer.root.findAllByProps({ tabIndex: 0 })[1].props.onFocus();
expect(fakeHTMLElementBegin.focus.called).toBe(true);
expect(fakeHTMLElementEnd.focus.called).toBe(false);
});
@@ -0,0 +1,27 @@
import React from "react";
export interface Focusable {
focus: () => void;
}
interface TrapFocusProps {
firstFocusable: Focusable | null;
lastFocusable: Focusable | null;
children: React.ReactNode;
}
export default class TrapFocus extends React.Component<TrapFocusProps> {
// Trap keyboard focus inside the dropdown until a value has been chosen.
public focusBegin = () => this.props.firstFocusable!.focus();
public focusEnd = () => this.props.lastFocusable!.focus();
public render() {
return (
<>
<div tabIndex={0} onFocus={this.focusEnd} />
{this.props.children}
<div tabIndex={0} onFocus={this.focusBegin} />
</>
);
}
}
@@ -0,0 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<div>
<div
onFocus={[Function]}
tabIndex={0}
/>
<span>
child1
</span>
<span>
child2
</span>
<div
onFocus={[Function]}
tabIndex={0}
/>
</div>
`;
@@ -0,0 +1,2 @@
export * from "./TrapFocus";
export { default } from "./TrapFocus";
+1
View File
@@ -5,3 +5,4 @@ export { default as RelativeTime } from "./RelativeTime";
export { default as UIContext, UIContextProps } from "./UIContext";
export { default as Flex } from "./Flex";
export { default as MatchMedia } from "./MatchMedia";
export { default as TrapFocus } from "./TrapFocus";