Skip to content

Commit

Permalink
[SpeedDial] Implement navigation behavior
Browse files Browse the repository at this point in the history
Following the proposed spec with alternative B
  • Loading branch information
eps1lon committed Sep 2, 2018
1 parent 4a2e65f commit 8c02338
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 21 deletions.
60 changes: 39 additions & 21 deletions packages/material-ui-lab/src/SpeedDial/SpeedDial.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Zoom from '@material-ui/core/Zoom';
import { duration } from '@material-ui/core/styles/transitions';
import Button from '@material-ui/core/Button';
import { isMuiElement } from '@material-ui/core/utils/reactHelpers';
import * as utils from './utils';

export const styles = {
/* Styles applied to the root element. */
Expand Down Expand Up @@ -51,26 +52,38 @@ export const styles = {

class SpeedDial extends React.Component {
/**
* refs to all children SpeedDialActions
* refs to the Button in all children SpeedDialActions
* @type {HTMLButtonElement[]}
*/
actions = [];

state = {
/**
* an index in this.actions
*/
focusedAction: -1,
/**
* pressing this key while the focus is on a child SpeedDialAction focuses
* the next SpeedDialAction.
* It is equal to the first arrow key pressed while focus is on the SpeedDial
* that is not orthogonal to the direction.
* @type {utils.ArrowKey}
*/
nextItemArrowKey: 'up',
};

handleButtonKeyDown = event => {
const key = keycode(event);
const { direction, onKeyDown, open } = this.props;

if (open && key === direction) {
if (open && utils.sameOrientation(key, direction)) {
event.preventDefault();

const focusedAction = 0;
const firstActionRef = this.actions[focusedAction];
if (firstActionRef != null) {
firstActionRef.focus();
this.setState({ focusedAction });
this.setState({ focusedAction, nextItemArrowKey: key });
}
}

Expand All @@ -82,24 +95,19 @@ class SpeedDial extends React.Component {
handleActionButtonKeyDown = event => {
const key = keycode(event);
const { direction, onClose, onKeyDown } = this.props;
const { focusedAction } = this.state;

const directionAxis = direction === 'up' || direction === 'down' ? 'vertical' : 'horizontal';
const isNavigating =
(directionAxis === 'vertical' && (key === 'up' || key === 'down')) ||
(directionAxis === 'horizontal' && (key === 'left' || key === 'right'));
const { focusedAction, nextItemArrowKey } = this.state;

if (key === 'esc') {
this.fabRef.focus();
if (onClose) {
onClose(event, key);
}
} else if (isNavigating) {
} else if (utils.sameOrientation(key, direction)) {
event.preventDefault();

const actionStep = key === direction ? 1 : -1;
const actionStep = key === nextItemArrowKey ? 1 : -1;

// consider the actions as a ring
// wrap at beginning/end
const nextAction = (focusedAction + actionStep + this.actions.length) % this.actions.length;
const nextActionRef = this.actions[nextAction];
nextActionRef.focus();
Expand All @@ -111,12 +119,21 @@ class SpeedDial extends React.Component {
}
};

createHandleSpeedDialActionButtonRef = (dialActionIndex, origButtonRef) => ref => {
this.actions[dialActionIndex] = ref;
if (origButtonRef) {
origButtonRef(ref);
}
};
/**
* creates a ref callback for the Button in a SpeedDialAction
* Is called before the original ref callback for Button that was set in buttonProps
*
* @param dialActionIndex {number}
* @param origButtonRef {React.RefObject?}
*/
createHandleSpeedDialActionButtonRef(dialActionIndex, origButtonRef) {
return ref => {
this.actions[dialActionIndex] = ref;
if (origButtonRef) {
origButtonRef(ref);
}
};
}

render() {
const {
Expand All @@ -142,6 +159,8 @@ class SpeedDial extends React.Component {
// Filter the label for valid id characters.
const id = ariaLabel.replace(/^[^a-z]+|[^\w:.-]+/gi, '');

const orientation = utils.getOrientation(direction);

let totalValidChildren = 0;
React.Children.forEach(childrenProp, child => {
if (React.isValidElement(child)) totalValidChildren += 1;
Expand Down Expand Up @@ -222,14 +241,13 @@ class SpeedDial extends React.Component {
</TransitionComponent>
<div
id={`${id}-actions`}
role="menu"
aria-orientation={orientation}
className={classNames(
classes.actions,
{ [classes.actionsClosed]: !open },
actionsPlacementClass,
)}
ref={ref => {
this.actionsRef = ref;
}}
>
{children}
</div>
Expand Down
28 changes: 28 additions & 0 deletions packages/material-ui-lab/src/SpeedDial/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* An arrow key on the keyboard
* @typedef {'up'|'right'|'down'|'left'} ArrowKey
*/

/**
*
* @param direction {string}
* @returns value useable in aria-orientation or undefined if no ArrowKey given
*/
export function getOrientation(direction) {
if (direction === 'up' || direction === 'down') {
return 'vertical';
}
if (direction === 'right' || direction === 'left') {
return 'horizontal';
}
return undefined;
}

/**
* @param {string} directionA
* @param {string} directionB
* @returns {boolean}
*/
export function sameOrientation(directionA, directionB) {
return getOrientation(directionA) === getOrientation(directionB);
}

0 comments on commit 8c02338

Please sign in to comment.