mirror of
https://github.com/wassname/talk.git
synced 2026-07-02 22:48:09 +08:00
Implement TrapFocus Component
This commit is contained in:
@@ -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";
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user