@wordpress/components
Version:
UI components for WordPress.
178 lines (141 loc) • 4.77 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _element = require("@wordpress/element");
var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
var _lodash = require("lodash");
var _dom = require("@wordpress/dom");
/**
* External dependencies
*/
/**
* WordPress dependencies
*/
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 _element.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 ((0, _lodash.isFunction)(forwardedRef)) {
forwardedRef(ref);
} else if (forwardedRef && 'current' in forwardedRef) {
forwardedRef.current = ref;
}
}
getFocusableContext(target) {
const {
onlyBrowserTabstops
} = this.props;
const finder = onlyBrowserTabstops ? _dom.focus.tabbable : _dom.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 = _lodash.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 (0, _element.createElement)("div", (0, _extends2.default)({
ref: this.bindContainer
}, (0, _lodash.omit)(props, ['stopNavigationEvents', 'eventToOffset', 'onNavigate', 'onKeyDown', 'cycle', 'onlyBrowserTabstops', 'forwardedRef'])), children);
}
}
const forwardedNavigableContainer = (props, ref) => {
return (0, _element.createElement)(NavigableContainer, (0, _extends2.default)({}, props, {
forwardedRef: ref
}));
};
forwardedNavigableContainer.displayName = 'NavigableContainer';
var _default = (0, _element.forwardRef)(forwardedNavigableContainer);
exports.default = _default;
//# sourceMappingURL=container.js.map