Keyboard shortcuts and accessibility

This commit is contained in:
Scott Nonnenberg 2019-11-07 13:36:16 -08:00
parent 8590a047c7
commit 20a892247f
87 changed files with 3652 additions and 711 deletions

View file

@ -40,10 +40,15 @@ type PropsHousekeepingType = {
id: string;
unreadCount?: number;
typingContact?: Object;
selectedMessageId?: string;
i18n: LocalizerType;
renderItem: (id: string, actions: Object) => JSX.Element;
renderItem: (
id: string,
conversationId: string,
actions: Object
) => JSX.Element;
renderLastSeenIndicator: (id: string) => JSX.Element;
renderLoadingRow: (id: string) => JSX.Element;
renderTypingBubble: (id: string) => JSX.Element;
@ -60,8 +65,10 @@ type PropsActionsType = {
loadAndScroll: (messageId: string) => unknown;
loadOlderMessages: (messageId: string) => unknown;
loadNewerMessages: (messageId: string) => unknown;
loadNewestMessages: (messageId: string) => unknown;
loadNewestMessages: (messageId: string, setFocus?: boolean) => unknown;
markMessageRead: (messageId: string) => unknown;
selectMessage: (messageId: string, conversationId: string) => unknown;
clearSelectedMessage: () => unknown;
} & MessageActionsType &
SafetyNumberActionsType;
@ -547,7 +554,7 @@ export class Timeline extends React.PureComponent<Props, State> {
style={styleWithWidth}
role="row"
>
{renderItem(messageId, this.props)}
{renderItem(messageId, id, this.props)}
</div>
);
}
@ -662,7 +669,15 @@ export class Timeline extends React.PureComponent<Props, State> {
}
};
public scrollToBottom = () => {
public scrollToBottom = (setFocus?: boolean) => {
const { selectMessage, id, items } = this.props;
if (setFocus && items && items.length > 0) {
const lastIndex = items.length - 1;
const lastMessageId = items[lastIndex];
selectMessage(lastMessageId, id);
}
this.setState({
propScrollToIndex: undefined,
oneTimeScrollRow: undefined,
@ -671,20 +686,31 @@ export class Timeline extends React.PureComponent<Props, State> {
};
public onClickScrollDownButton = () => {
this.scrollDown(false);
};
public scrollDown = (setFocus?: boolean) => {
const {
haveNewest,
id,
isLoadingMessages,
items,
loadNewestMessages,
oldestUnreadIndex,
selectMessage,
} = this.props;
if (!items || items.length < 1) {
return;
}
const lastId = items[items.length - 1];
const lastSeenIndicatorRow = this.getLastSeenIndicatorRow();
if (!this.visibleRows) {
if (haveNewest) {
this.scrollToBottom();
this.scrollToBottom(setFocus);
} else if (!isLoadingMessages) {
loadNewestMessages(lastId);
loadNewestMessages(lastId, setFocus);
}
return;
@ -697,13 +723,17 @@ export class Timeline extends React.PureComponent<Props, State> {
isNumber(lastSeenIndicatorRow) &&
newest.row < lastSeenIndicatorRow
) {
if (setFocus && isNumber(oldestUnreadIndex)) {
const messageId = items[oldestUnreadIndex];
selectMessage(messageId, id);
}
this.setState({
oneTimeScrollRow: lastSeenIndicatorRow,
});
} else if (haveNewest) {
this.scrollToBottom();
this.scrollToBottom(setFocus);
} else if (!isLoadingMessages) {
loadNewestMessages(lastId);
loadNewestMessages(lastId, setFocus);
}
};
@ -743,6 +773,10 @@ export class Timeline extends React.PureComponent<Props, State> {
prevProps.items.length === 0 ||
resetCounter !== prevProps.resetCounter
) {
if (prevProps.items && prevProps.items.length > 0) {
this.resize();
}
const oneTimeScrollRow = this.getLastSeenIndicatorRow();
this.setState({
oneTimeScrollRow,
@ -751,10 +785,6 @@ export class Timeline extends React.PureComponent<Props, State> {
prevPropScrollToIndex: scrollToIndex,
});
if (prevProps.items && prevProps.items.length > 0) {
this.resize();
}
return;
}
@ -881,6 +911,93 @@ export class Timeline extends React.PureComponent<Props, State> {
return scrollToBottom;
};
public handleBlur = (event: React.FocusEvent) => {
const { clearSelectedMessage } = this.props;
const { currentTarget } = event;
// Thanks to https://gist.github.com/pstoica/4323d3e6e37e8a23dd59
setTimeout(() => {
if (!currentTarget.contains(document.activeElement)) {
clearSelectedMessage();
}
}, 0);
};
public handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
const { selectMessage, selectedMessageId, items, id } = this.props;
const commandOrCtrl = event.metaKey || event.ctrlKey;
if (!items || items.length < 1) {
return;
}
if (selectedMessageId && !commandOrCtrl && event.key === 'ArrowUp') {
const selectedMessageIndex = items.findIndex(
item => item === selectedMessageId
);
if (selectedMessageIndex < 0) {
return;
}
const targetIndex = selectedMessageIndex - 1;
if (targetIndex < 0) {
return;
}
const messageId = items[targetIndex];
selectMessage(messageId, id);
event.preventDefault();
event.stopPropagation();
return;
}
if (selectedMessageId && !commandOrCtrl && event.key === 'ArrowDown') {
const selectedMessageIndex = items.findIndex(
item => item === selectedMessageId
);
if (selectedMessageIndex < 0) {
return;
}
const targetIndex = selectedMessageIndex + 1;
if (targetIndex >= items.length) {
return;
}
const messageId = items[targetIndex];
selectMessage(messageId, id);
event.preventDefault();
event.stopPropagation();
return;
}
if (commandOrCtrl && event.key === 'ArrowUp') {
this.setState({ oneTimeScrollRow: 0 });
const firstMessageId = items[0];
selectMessage(firstMessageId, id);
event.preventDefault();
event.stopPropagation();
return;
}
if (commandOrCtrl && event.key === 'ArrowDown') {
this.scrollDown(true);
event.preventDefault();
event.stopPropagation();
return;
}
};
public render() {
const { i18n, id, items } = this.props;
const {
@ -896,7 +1013,13 @@ export class Timeline extends React.PureComponent<Props, State> {
}
return (
<div className="module-timeline">
<div
className="module-timeline"
role="group"
tabIndex={-1}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
>
<AutoSizer>
{({ height, width }) => {
if (this.mostRecentWidth && this.mostRecentWidth !== width) {
@ -925,6 +1048,7 @@ export class Timeline extends React.PureComponent<Props, State> {
rowRenderer={this.rowRenderer}
scrollToAlignment="start"
scrollToIndex={scrollToIndex}
tabIndex={-1}
width={width}
/>
);