@wordpress/components
Version:
UI components for WordPress.
164 lines (135 loc) • 4.48 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
import { createElement } from "@wordpress/element";
/**
* External dependencies
*/
import { omit, noop, isFunction } from 'lodash';
/**
* WordPress dependencies
*/
import { Component, forwardRef } from '@wordpress/element';
import { focus } from '@wordpress/dom';
const MENU_ITEM_ROLES = ['menuitem', 'menuitemradio', 'menuitemcheckbox'];
function cycleValue(value, total, offset) {
const nextValue = value + offset;
if (nextValue < 0) {
return total + nextValue;
} else if (nextValue >= total) {
return nextValue - total;
}
return nextValue;
}
class NavigableContainer extends Component {
constructor() {
super(...arguments);
this.onKeyDown = this.onKeyDown.bind(this);
this.bindContainer = this.bindContainer.bind(this);
this.getFocusableContext = this.getFocusableContext.bind(this);
this.getFocusableIndex = this.getFocusableIndex.bind(this);
}
componentDidMount() {
// We use DOM event listeners instead of React event listeners
// because we want to catch events from the underlying DOM tree
// The React Tree can be different from the DOM tree when using
// portals. Block Toolbars for instance are rendered in a separate
// React Trees.
this.container.addEventListener('keydown', this.onKeyDown);
this.container.addEventListener('focus', this.onFocus);
}
componentWillUnmount() {
this.container.removeEventListener('keydown', this.onKeyDown);
this.container.removeEventListener('focus', this.onFocus);
}
bindContainer(ref) {
const {
forwardedRef
} = this.props;
this.container = ref;
if (isFunction(forwardedRef)) {
forwardedRef(ref);
} else if (forwardedRef && 'current' in forwardedRef) {
forwardedRef.current = ref;
}
}
getFocusableContext(target) {
const {
onlyBrowserTabstops
} = this.props;
const finder = onlyBrowserTabstops ? focus.tabbable : focus.focusable;
const focusables = finder.find(this.container);
const index = this.getFocusableIndex(focusables, target);
if (index > -1 && target) {
return {
index,
target,
focusables
};
}
return null;
}
getFocusableIndex(focusables, target) {
const directIndex = focusables.indexOf(target);
if (directIndex !== -1) {
return directIndex;
}
}
onKeyDown(event) {
if (this.props.onKeyDown) {
this.props.onKeyDown(event);
}
const {
getFocusableContext
} = this;
const {
cycle = true,
eventToOffset,
onNavigate = noop,
stopNavigationEvents
} = this.props;
const offset = eventToOffset(event); // eventToOffset returns undefined if the event is not handled by the component
if (offset !== undefined && stopNavigationEvents) {
// Prevents arrow key handlers bound to the document directly interfering
event.stopImmediatePropagation(); // When navigating a collection of items, prevent scroll containers
// from scrolling. The preventDefault also prevents Voiceover from
// 'handling' the event, as voiceover will try to use arrow keys
// for highlighting text.
const targetRole = event.target.getAttribute('role');
if (MENU_ITEM_ROLES.includes(targetRole)) {
event.preventDefault();
}
}
if (!offset) {
return;
}
const context = getFocusableContext(event.target.ownerDocument.activeElement);
if (!context) {
return;
}
const {
index,
focusables
} = context;
const nextIndex = cycle ? cycleValue(index, focusables.length, offset) : index + offset;
if (nextIndex >= 0 && nextIndex < focusables.length) {
focusables[nextIndex].focus();
onNavigate(nextIndex, focusables[nextIndex]);
}
}
render() {
const {
children,
...props
} = this.props;
return createElement("div", _extends({
ref: this.bindContainer
}, omit(props, ['stopNavigationEvents', 'eventToOffset', 'onNavigate', 'onKeyDown', 'cycle', 'onlyBrowserTabstops', 'forwardedRef'])), children);
}
}
const forwardedNavigableContainer = (props, ref) => {
return createElement(NavigableContainer, _extends({}, props, {
forwardedRef: ref
}));
};
forwardedNavigableContainer.displayName = 'NavigableContainer';
export default forwardRef(forwardedNavigableContainer);
//# sourceMappingURL=container.js.map