Iterate to get rid of the magic group and just provide a generic functional render wrapper

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2020-01-15 11:37:14 +00:00
parent 5252cf4c45
commit dedf1eab31
4 changed files with 184 additions and 182 deletions

View file

@ -31,7 +31,7 @@ import PropTypes from 'prop-types';
import RoomTile from "../views/rooms/RoomTile"; import RoomTile from "../views/rooms/RoomTile";
import LazyRenderList from "../views/elements/LazyRenderList"; import LazyRenderList from "../views/elements/LazyRenderList";
import {_t} from "../../languageHandler"; import {_t} from "../../languageHandler";
import {RovingTabIndex, RovingTabIndexGroup} from "../../contexts/RovingTabIndexContext"; import {RovingTabIndexWrapper} from "../../contexts/RovingTabIndexContext";
// turn this on for drop & drag console debugging galore // turn this on for drop & drag console debugging galore
const debug = false; const debug = false;
@ -264,45 +264,6 @@ export default class RoomSubList extends React.PureComponent {
const subListNotifCount = subListNotifications.count; const subListNotifCount = subListNotifications.count;
const subListNotifHighlight = subListNotifications.highlight; const subListNotifHighlight = subListNotifications.highlight;
let badge;
if (!this.props.collapsed) {
const badgeClasses = classNames({
'mx_RoomSubList_badge': true,
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
});
// Wrap the contents in a div and apply styles to the child div so that the browser default outline works
if (subListNotifCount > 0) {
badge = (
<RovingTabIndex
component={AccessibleButton}
useInputRef
className={badgeClasses}
onClick={this._onNotifBadgeClick}
aria-label={_t("Jump to first unread room.")}
>
<div>
{ FormattingUtils.formatCount(subListNotifCount) }
</div>
</RovingTabIndex>
);
} else if (this.props.isInvite && this.props.list.length) {
// no notifications but highlight anyway because this is an invite badge
badge = (
<RovingTabIndex
component={AccessibleButton}
useInputRef
className={badgeClasses}
onClick={this._onInviteBadgeClick}
aria-label={_t("Jump to first invite.")}
>
<div>
{ this.props.list.length }
</div>
</RovingTabIndex>
);
}
}
// When collapsed, allow a long hover on the header to show user // When collapsed, allow a long hover on the header to show user
// the full tag name and room count // the full tag name and room count
let title; let title;
@ -318,19 +279,6 @@ export default class RoomSubList extends React.PureComponent {
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />; <IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
} }
let addRoomButton;
if (this.props.onAddRoom) {
addRoomButton = (
<RovingTabIndex
component={AccessibleTooltipButton}
useInputRef
onClick={this.onAddRoom}
className="mx_RoomSubList_addRoom"
title={this.props.addRoomLabel || _t("Add room")}
/>
);
}
const len = this.props.list.length + this.props.extraTiles.length; const len = this.props.list.length + this.props.extraTiles.length;
let chevron; let chevron;
if (len) { if (len) {
@ -342,26 +290,81 @@ export default class RoomSubList extends React.PureComponent {
chevron = (<div className={chevronClasses} />); chevron = (<div className={chevronClasses} />);
} }
return <RovingTabIndexGroup> return <RovingTabIndexWrapper inputRef={this._headerButton}>
<div className="mx_RoomSubList_labelContainer" title={title} ref={this._header} onKeyDown={this.onHeaderKeyDown}> {({onFocus, isActive, ref}) => {
<RovingTabIndex const tabIndex = isActive ? 0 : -1;
component={AccessibleButton}
useInputRef let badge;
onClick={this.onClick} if (!this.props.collapsed) {
className="mx_RoomSubList_label" const badgeClasses = classNames({
aria-expanded={!isCollapsed} 'mx_RoomSubList_badge': true,
inputRef={this._headerButton} 'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
role="treeitem" });
aria-level="1" // Wrap the contents in a div and apply styles to the child div so that the browser default outline works
> if (subListNotifCount > 0) {
{ chevron } badge = (
<span>{this.props.label}</span> <AccessibleButton
{ incomingCall } tabIndex={tabIndex}
</RovingTabIndex> className={badgeClasses}
{ badge } onClick={this._onNotifBadgeClick}
{ addRoomButton } aria-label={_t("Jump to first unread room.")}
</div> >
</RovingTabIndexGroup>; <div>
{ FormattingUtils.formatCount(subListNotifCount) }
</div>
</AccessibleButton>
);
} else if (this.props.isInvite && this.props.list.length) {
// no notifications but highlight anyway because this is an invite badge
badge = (
<AccessibleButton
tabIndex={tabIndex}
className={badgeClasses}
onClick={this._onInviteBadgeClick}
aria-label={_t("Jump to first invite.")}
>
<div>
{ this.props.list.length }
</div>
</AccessibleButton>
);
}
}
let addRoomButton;
if (this.props.onAddRoom) {
addRoomButton = (
<AccessibleTooltipButton
tabIndex={tabIndex}
onClick={this.onAddRoom}
className="mx_RoomSubList_addRoom"
title={this.props.addRoomLabel || _t("Add room")}
/>
);
}
return (
<div className="mx_RoomSubList_labelContainer" title={title} ref={this._header} onKeyDown={this.onHeaderKeyDown}>
<AccessibleButton
onFocus={onFocus}
tabIndex={tabIndex}
inputRef={ref}
onClick={this.onClick}
className="mx_RoomSubList_label"
aria-expanded={!isCollapsed}
role="treeitem"
aria-level="1"
>
{ chevron }
<span>{this.props.label}</span>
{ incomingCall }
</AccessibleButton>
{ badge }
{ addRoomButton }
</div>
);
} }
</RovingTabIndexWrapper>;
} }
checkOverflow = () => { checkOverflow = () => {

View file

@ -26,7 +26,7 @@ import classNames from 'classnames';
import MatrixClientPeg from "../../../MatrixClientPeg"; import MatrixClientPeg from "../../../MatrixClientPeg";
import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu"; import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {RovingTabIndex, RovingTabIndexGroup} from "../../../contexts/RovingTabIndexContext"; import {RovingTabIndexWrapper} from "../../../contexts/RovingTabIndexContext";
// XXX this class copies a lot from RoomTile.js // XXX this class copies a lot from RoomTile.js
export default createReactClass({ export default createReactClass({
@ -138,18 +138,6 @@ export default createReactClass({
}); });
const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!'; const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!';
const badge = (
<RovingTabIndex
component={ContextMenuButton}
useInputRef
className={badgeClasses}
onClick={this.onContextMenuButtonClick}
label={_t("Options")}
isExpanded={isMenuDisplayed}
>
{ badgeContent }
</RovingTabIndex>
);
let tooltip; let tooltip;
if (this.props.collapsed && this.state.hover) { if (this.props.collapsed && this.state.hover) {
@ -173,27 +161,40 @@ export default createReactClass({
); );
} }
return <RovingTabIndexGroup> return <React.Fragment>
<RovingTabIndex <RovingTabIndexWrapper>
component={AccessibleButton} {({onFocus, isActive, ref}) =>
useInputRef <AccessibleButton
className={classes} onFocus={onFocus}
onClick={this.onClick} tabIndex={isActive ? 0 : -1}
onMouseEnter={this.onMouseEnter} inputRef={ref}
onMouseLeave={this.onMouseLeave} className={classes}
onContextMenu={this.onContextMenu} onClick={this.onClick}
> onMouseEnter={this.onMouseEnter}
<div className="mx_RoomTile_avatar"> onMouseLeave={this.onMouseLeave}
{ av } onContextMenu={this.onContextMenu}
</div> >
<div className="mx_RoomTile_nameContainer"> <div className="mx_RoomTile_avatar">
{ label } { av }
{ badge } </div>
</div> <div className="mx_RoomTile_nameContainer">
{ tooltip } { label }
</RovingTabIndex> <ContextMenuButton
className={badgeClasses}
onClick={this.onContextMenuButtonClick}
label={_t("Options")}
isExpanded={isMenuDisplayed}
tabIndex={isActive ? 0 : -1}
>
{ badgeContent }
</ContextMenuButton>
</div>
{ tooltip }
</AccessibleButton>
}
</RovingTabIndexWrapper>
{ contextMenu } { contextMenu }
</RovingTabIndexGroup>; </React.Fragment>;
}, },
}); });

View file

@ -32,7 +32,7 @@ import ActiveRoomObserver from '../../../ActiveRoomObserver';
import RoomViewStore from '../../../stores/RoomViewStore'; import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import {RovingTabIndex} from "../../../contexts/RovingTabIndexContext"; import {RovingTabIndexWrapper} from "../../../contexts/RovingTabIndexContext";
module.exports = createReactClass({ module.exports = createReactClass({
displayName: 'RoomTile', displayName: 'RoomTile',
@ -433,37 +433,42 @@ module.exports = createReactClass({
} }
return <React.Fragment> return <React.Fragment>
<RovingTabIndex <RovingTabIndexWrapper>
component={AccessibleButton} {({onFocus, isActive, ref}) =>
useInputRef <AccessibleButton
className={classes} onFocus={onFocus}
onClick={this.onClick} tabIndex={isActive ? 0 : -1}
onMouseEnter={this.onMouseEnter} inputRef={ref}
onMouseLeave={this.onMouseLeave} className={classes}
onContextMenu={this.onContextMenu} onClick={this.onClick}
aria-label={ariaLabel} onMouseEnter={this.onMouseEnter}
aria-selected={this.state.selected} onMouseLeave={this.onMouseLeave}
role="treeitem" onContextMenu={this.onContextMenu}
> aria-label={ariaLabel}
<div className={avatarClasses}> aria-selected={this.state.selected}
<div className="mx_RoomTile_avatar_container"> role="treeitem"
<RoomAvatar room={this.props.room} width={24} height={24} /> >
{ dmIndicator } <div className={avatarClasses}>
</div> <div className="mx_RoomTile_avatar_container">
</div> <RoomAvatar room={this.props.room} width={24} height={24} />
{ privateIcon } { dmIndicator }
<div className="mx_RoomTile_nameContainer"> </div>
<div className="mx_RoomTile_labelContainer"> </div>
{ label } { privateIcon }
{ subtextLabel } <div className="mx_RoomTile_nameContainer">
</div> <div className="mx_RoomTile_labelContainer">
{ dmOnline } { label }
{ contextMenuButton } { subtextLabel }
{ badge } </div>
</div> { dmOnline }
{ /* { incomingCallBox } */ } { contextMenuButton }
{ tooltip } { badge }
</RovingTabIndex> </div>
{ /* { incomingCallBox } */ }
{ tooltip }
</AccessibleButton>
}
</RovingTabIndexWrapper>
{ contextMenu } { contextMenu }
</React.Fragment>; </React.Fragment>;

View file

@ -25,11 +25,9 @@ import React, {
useRef, useRef,
useReducer, useReducer,
} from "react"; } from "react";
import PropTypes from "prop-types";
import {Key} from "../Keyboard"; import {Key} from "../Keyboard";
const DOCUMENT_POSITION_PRECEDING = 2; const DOCUMENT_POSITION_PRECEDING = 2;
const ANY = Symbol();
const RovingTabIndexContext = createContext({ const RovingTabIndexContext = createContext({
state: { state: {
@ -119,15 +117,42 @@ export const RovingTabIndexContextWrapper = ({children}) => {
const context = useMemo(() => ({state, dispatch}), [state]); const context = useMemo(() => ({state, dispatch}), [state]);
return <RovingTabIndexContext.Provider value={context}> const onKeyDown = useCallback((ev) => {
{children} if (state.refs.length <= 0) return;
</RovingTabIndexContext.Provider>;
let handled = true;
switch (ev.key) {
case Key.HOME:
setImmediate(() => state.refs[0].current.focus());
break;
case Key.END:
state.refs[state.refs.length - 1].current.focus();
break;
default:
handled = false;
}
if (handled) {
ev.preventDefault();
ev.stopPropagation();
}
}, [state]);
return <div onKeyDown={onKeyDown}>
<RovingTabIndexContext.Provider value={context}>
{children}
</RovingTabIndexContext.Provider>
</div>;
}; };
export const useRovingTabIndex = () => { export const useRovingTabIndex = (inputRef) => {
const ref = useRef(null); let ref = useRef(null);
const context = useContext(RovingTabIndexContext); const context = useContext(RovingTabIndexContext);
if (inputRef) {
ref = inputRef;
}
// setup/teardown // setup/teardown
// add ref to the context // add ref to the context
useLayoutEffect(() => { useLayoutEffect(() => {
@ -149,45 +174,13 @@ export const useRovingTabIndex = () => {
payload: {ref}, payload: {ref},
}); });
}, [ref, context]); }, [ref, context]);
const isActive = context.state.activeRef === ref || context.state.activeRef === ANY;
const isActive = context.state.activeRef === ref;
return [onFocus, isActive, ref]; return [onFocus, isActive, ref];
}; };
export const RovingTabIndexGroup = ({children}) => { export const RovingTabIndexWrapper = ({children, inputRef}) => {
const [onFocus, isActive, ref] = useRovingTabIndex(); const [onFocus, isActive, ref] = useRovingTabIndex(inputRef);
return children({onFocus, isActive, ref});
// fake reducer dispatch to catch SET_FOCUS calls and pass them to parent as a focus of the group
const dispatch = useCallback(({type}) => {
if (type === types.SET_FOCUS) {
onFocus();
}
}, [onFocus]);
const context = useMemo(() => ({
state: {activeRef: isActive ? ANY : undefined},
dispatch,
}), [isActive, dispatch]);
return <div ref={ref}>
<RovingTabIndexContext.Provider value={context}>
{children}
</RovingTabIndexContext.Provider>
</div>;
};
// Wraps a given element to attach it to the roving context, props onFocus and tabIndex overridden
export const RovingTabIndex = ({component: E, useInputRef, ...props}) => {
const [onFocus, isActive, ref] = useRovingTabIndex();
const refProps = {};
if (useInputRef) {
refProps.inputRef = ref;
} else {
refProps.ref = ref;
}
return <E {...props} {...refProps} onFocus={onFocus} tabIndex={isActive ? 0 : -1} />;
};
RovingTabIndex.propTypes = {
component: PropTypes.elementType.isRequired,
useInputRef: PropTypes.bool, // whether to pass inputRef instead of ref like for AccessibleButton
}; };