@fluentui/react-northstar
Version:
A themable React component library.
514 lines (496 loc) • 23.8 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
exports.__esModule = true;
exports.toolbarClassName = exports.Toolbar = void 0;
var _get2 = _interopRequireDefault(require("lodash/get"));
var _map2 = _interopRequireDefault(require("lodash/map"));
var _debounce2 = _interopRequireDefault(require("lodash/debounce"));
var _invoke2 = _interopRequireDefault(require("lodash/invoke"));
var _forEachRight2 = _interopRequireDefault(require("lodash/forEachRight"));
var _accessibility = require("@fluentui/accessibility");
var _reactBindings = require("@fluentui/react-bindings");
var _reactComponentEventListener = require("@fluentui/react-component-event-listener");
var _reactComponentRef = require("@fluentui/react-component-ref");
var _reactIconsNorthstar = require("@fluentui/react-icons-northstar");
var customPropTypes = _interopRequireWildcard(require("@fluentui/react-proptypes"));
var PropTypes = _interopRequireWildcard(require("prop-types"));
var React = _interopRequireWildcard(require("react"));
var _utils = require("../../utils");
var _ToolbarCustomItem = require("./ToolbarCustomItem");
var _ToolbarDivider = require("./ToolbarDivider");
var _ToolbarItem = require("./ToolbarItem");
var _ToolbarItemWrapper = require("./ToolbarItemWrapper");
var _ToolbarItemIcon = require("./ToolbarItemIcon");
var _ToolbarMenu = require("./ToolbarMenu");
var _ToolbarMenuDivider = require("./ToolbarMenuDivider");
var _ToolbarMenuItem = require("./ToolbarMenuItem");
var _ToolbarMenuRadioGroup = require("./ToolbarMenuRadioGroup");
var _ToolbarMenuRadioGroupWrapper = require("./ToolbarMenuRadioGroupWrapper");
var _ToolbarRadioGroup = require("./ToolbarRadioGroup");
var _toolbarVariablesContext = require("./toolbarVariablesContext");
var _ToolbarMenuItemSubmenuIndicator = require("./ToolbarMenuItemSubmenuIndicator");
var _ToolbarMenuItemIcon = require("./ToolbarMenuItemIcon");
var _ToolbarMenuItemActiveIndicator = require("./ToolbarMenuItemActiveIndicator");
var _toolbarMenuContext = require("./toolbarMenuContext");
var _Box = require("../Box/Box");
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
var WAS_FOCUSABLE_ATTRIBUTE = 'data-was-focusable';
var toolbarClassName = 'ui-toolbar';
/**
* A Toolbar is a container for grouping a set of controls, often action controls (e.g. buttons) or input controls (e.g. checkboxes).
*
* @accessibility
* * Implements [ARIA Toolbar](https://www.w3.org/TR/wai-aria-practices-1.1/#toolbar) design pattern.
* @accessibilityIssues
* [Issue 988424: VoiceOver narrates selected for button in toolbar](https://bugs.chromium.org/p/chromium/issues/detail?id=988424)
* [In toolbars that can toggle items in a menu, VoiceOver narrates "1" for menuitemcheckbox/radio when checked.](https://github.com/microsoft/fluentui/issues/14064)
* [NVDA could narrate "checked" stated for radiogroup in toolbar #12678](https://github.com/nvaccess/nvda/issues/12678)
* [JAWS narrates wrong instruction message for radiogroup in toolbar #556](https://github.com/FreedomScientific/VFO-standards-support/issues/556)
* [JAWS could narrate "checked" stated for radiogroup in toolbar #557](https://github.com/FreedomScientific/VFO-standards-support/issues/557)
*/
exports.toolbarClassName = toolbarClassName;
var Toolbar = (0, _reactBindings.compose)(function (props, ref, composeOptions) {
var context = (0, _reactBindings.useFluentContext)();
var _useTelemetry = (0, _reactBindings.useTelemetry)(composeOptions.displayName, context.telemetry),
setStart = _useTelemetry.setStart,
setEnd = _useTelemetry.setEnd;
setStart();
var accessibility = props.accessibility,
className = props.className,
children = props.children,
design = props.design,
getOverflowItems = props.getOverflowItems,
items = props.items,
overflow = props.overflow,
overflowItem = props.overflowItem,
overflowOpen = props.overflowOpen,
overflowSentinel = props.overflowSentinel,
styles = props.styles,
variables = props.variables;
var overflowContainerRef = React.useRef();
var overflowItemWrapperRef = React.useRef();
var overflowSentinelRef = React.useRef();
var offsetMeasureRef = React.useRef();
var containerRef = React.useRef();
// index of the last visible item in Toolbar, the rest goes to overflow menu
var lastVisibleItemIndex = React.useRef();
var animationFrameId = React.useRef();
var getA11Props = (0, _reactBindings.useAccessibility)(accessibility, {
debugName: composeOptions.displayName,
rtl: context.rtl
});
var _useStyles = (0, _reactBindings.useStyles)(composeOptions.displayName, {
className: toolbarClassName,
composeOptions: composeOptions,
mapPropsToStyles: function mapPropsToStyles() {
return {
overflowOpen: overflowOpen
};
},
mapPropsToInlineStyles: function mapPropsToInlineStyles() {
return {
className: className,
design: design,
styles: styles,
variables: variables
};
},
rtl: context.rtl,
unstable_props: props
}),
classes = _useStyles.classes;
var ElementType = (0, _reactBindings.getElementType)(props);
var slotProps = composeOptions.resolveSlotProps(props);
var unhandledProps = (0, _reactBindings.useUnhandledProps)(composeOptions.handledProps, props);
var hide = function hide(el) {
if (el.style.visibility === 'hidden') {
return;
}
if (context.target.activeElement === el || el.contains(context.target.activeElement)) {
if (containerRef.current) {
var firstFocusableItem = (0, _reactBindings.getFirstFocusable)(containerRef.current, containerRef.current.firstElementChild);
if (firstFocusableItem) {
firstFocusableItem.focus();
}
}
}
el.style.visibility = 'hidden';
var wasFocusable = el.getAttribute(_accessibility.IS_FOCUSABLE_ATTRIBUTE);
if (wasFocusable) {
el.setAttribute(WAS_FOCUSABLE_ATTRIBUTE, wasFocusable);
}
el.setAttribute(_accessibility.IS_FOCUSABLE_ATTRIBUTE, 'false');
};
var show = function show(el) {
if (el.style.visibility !== 'hidden') {
return false;
}
el.style.visibility = '';
var wasFocusable = el.getAttribute(WAS_FOCUSABLE_ATTRIBUTE);
if (wasFocusable) {
el.setAttribute(_accessibility.IS_FOCUSABLE_ATTRIBUTE, wasFocusable);
el.removeAttribute(WAS_FOCUSABLE_ATTRIBUTE);
} else {
el.removeAttribute(_accessibility.IS_FOCUSABLE_ATTRIBUTE);
}
return true;
};
/**
* Checks if `item` overflows a `container`.
* TODO: check and fix all margin combination
*/
var isItemOverflowing = function isItemOverflowing(itemBoundingRect, containerBoundingRect) {
return itemBoundingRect.right > containerBoundingRect.right || itemBoundingRect.left < containerBoundingRect.left;
};
/**
* Checks if `item` would collide with eventual position of `overflowItem`.
*/
var wouldItemCollide = function wouldItemCollide($item, itemBoundingRect, overflowItemBoundingRect, containerBoundingRect) {
var actualWindow = context.target.defaultView;
var wouldCollide;
if (context.rtl) {
var itemLeftMargin = parseFloat(actualWindow.getComputedStyle($item).marginLeft) || 0;
wouldCollide = itemBoundingRect.left - overflowItemBoundingRect.width - itemLeftMargin < containerBoundingRect.left;
// console.log('Collision [RTL]', {
// wouldCollide,
// 'itemBoundingRect.left': itemBoundingRect.left,
// 'overflowItemBoundingRect.width': overflowItemBoundingRect.width,
// itemRightMargin: itemLeftMargin,
// sum: itemBoundingRect.left - overflowItemBoundingRect.width - itemLeftMargin,
// 'overflowContainerBoundingRect.left': containerBoundingRect.left,
// })
} else {
var itemRightMargin = parseFloat(actualWindow.getComputedStyle($item).marginRight) || 0;
wouldCollide = itemBoundingRect.right + overflowItemBoundingRect.width + itemRightMargin > containerBoundingRect.right;
// console.log('Collision', {
// wouldCollide,
// 'itemBoundingRect.right': itemBoundingRect.right,
// 'overflowItemBoundingRect.width': overflowItemBoundingRect.width,
// itemRightMargin,
// sum: itemBoundingRect.right + overflowItemBoundingRect.width + itemRightMargin,
// 'overflowContainerBoundingRect.right': containerBoundingRect.right,
// })
}
return wouldCollide;
};
/**
* Positions overflowItem next to lastVisible item
* TODO: consider overflowItem margin
*/
var setOverflowPosition = function setOverflowPosition($overflowItem, $lastVisibleItem, lastVisibleItemRect, containerBoundingRect, absolutePositioningOffset) {
var actualWindow = context.target.defaultView;
if ($lastVisibleItem) {
if (context.rtl) {
var lastVisibleItemMarginLeft = parseFloat(actualWindow.getComputedStyle($lastVisibleItem).marginLeft) || 0;
$overflowItem.style.right = containerBoundingRect.right - lastVisibleItemRect.left + lastVisibleItemMarginLeft + absolutePositioningOffset.horizontal + "px";
} else {
var lastVisibleItemRightMargin = parseFloat(actualWindow.getComputedStyle($lastVisibleItem).marginRight) || 0;
$overflowItem.style.left = lastVisibleItemRect.right - containerBoundingRect.left + lastVisibleItemRightMargin + absolutePositioningOffset.horizontal + "px";
}
} else {
// there is no last visible item -> position the overflow as the first item
lastVisibleItemIndex.current = -1;
if (context.rtl) {
$overflowItem.style.right = absolutePositioningOffset.horizontal + "px";
} else {
$overflowItem.style.left = absolutePositioningOffset.horizontal + "px";
}
}
};
var hideOverflowItems = function hideOverflowItems() {
var $overflowContainer = overflowContainerRef.current;
var $overflowItem = overflowItemWrapperRef.current;
var $overflowSentinel = overflowSentinelRef.current;
var $offsetMeasure = offsetMeasureRef.current;
if (!$overflowContainer || !$overflowItem || !$offsetMeasure) {
return;
}
// workaround: when resizing window with popup opened the container contents scroll for some reason
if (context.rtl) {
$overflowContainer.scrollTo(Number.MAX_SAFE_INTEGER, 0);
} else {
$overflowContainer.scrollTop = 0;
$overflowContainer.scrollLeft = 0;
}
var $items = $overflowContainer.children;
var overflowContainerBoundingRect = $overflowContainer.getBoundingClientRect();
var overflowItemBoundingRect = $overflowItem.getBoundingClientRect();
var offsetMeasureBoundingRect = $offsetMeasure.getBoundingClientRect();
// Absolute positioning offset
// Overflow menu is absolutely positioned relative to root slot
// If there is padding set on the root slot boundingClientRect computations use inner content box,
// but absolute position is relative to root slot's PADDING box.
// We compute absolute positioning offset
// By measuring position of an offsetMeasure element absolutely positioned to 0,0.
// TODO: replace by getComputedStyle('padding')
var absolutePositioningOffset = {
horizontal: context.rtl ? offsetMeasureBoundingRect.right - overflowContainerBoundingRect.right : overflowContainerBoundingRect.left - offsetMeasureBoundingRect.left,
vertical: overflowContainerBoundingRect.top - offsetMeasureBoundingRect.top
};
var isOverflowing = false;
var $lastVisibleItem;
var lastVisibleItemRect;
// check all items from the last one back
(0, _forEachRight2.default)($items, function ($item, i) {
if ($item === $overflowItem || $item === $overflowSentinel) {
return true;
}
var itemBoundingRect = $item.getBoundingClientRect();
// if the item is out of the crop rectangle, hide it
if (isItemOverflowing(itemBoundingRect, overflowContainerBoundingRect)) {
isOverflowing = true;
// console.log('Overflow', i, {
// item: [itemBoundingRect.left, itemBoundingRect.right],
// crop: [
// overflowContainerBoundingRect.left,
// overflowContainerBoundingRect.right,
// overflowContainerBoundingRect.width,
// ],
// container: $overflowContainer,
// })
hide($item);
return true;
}
// if there is an overflow, check collision of remaining items with eventual overflow position
if (isOverflowing && !$lastVisibleItem && wouldItemCollide($item, itemBoundingRect, overflowItemBoundingRect, overflowContainerBoundingRect)) {
hide($item);
return true;
}
// Remember the last visible item
if (!$lastVisibleItem) {
$lastVisibleItem = $item;
lastVisibleItemRect = itemBoundingRect;
lastVisibleItemIndex.current = i;
}
return show($item); // exit the loop when first visible item is found
});
// if there is an overflow, position and show overflow item, otherwise hide it
if (isOverflowing || overflowOpen) {
$overflowItem.style.position = 'absolute';
setOverflowPosition($overflowItem, $lastVisibleItem, lastVisibleItemRect, overflowContainerBoundingRect, absolutePositioningOffset);
show($overflowItem);
} else {
lastVisibleItemIndex.current = items.length - 1;
hide($overflowItem);
}
(0, _invoke2.default)(props, 'onOverflow', lastVisibleItemIndex.current + 1);
};
var collectOverflowItems = function collectOverflowItems() {
// console.log('getOverflowItems()', items.slice(lastVisibleItemIndex.current + 1))
return getOverflowItems ? getOverflowItems(lastVisibleItemIndex.current + 1) : items.slice(lastVisibleItemIndex.current + 1);
};
var getVisibleItems = function getVisibleItems() {
// console.log('allItems()', items)
var end = overflowOpen ? lastVisibleItemIndex.current + 1 : items.length;
// console.log('getVisibleItems()', items.slice(0, end))
return items.slice(0, end);
};
var handleWindowResize = (0, _debounce2.default)(function (e) {
hideOverflowItems();
if (overflowOpen) {
(0, _invoke2.default)(props, 'onOverflowOpenChange', e, Object.assign({}, props, {
overflowOpen: false
}));
}
}, 16);
var renderItems = function renderItems(items) {
return (0, _map2.default)(items, function (item) {
var kind = (0, _get2.default)(item, 'kind', 'item');
switch (kind) {
case 'divider':
return (0, _utils.createShorthand)(composeOptions.slots.divider, item, {
defaultProps: function defaultProps() {
return slotProps.divider;
}
});
case 'group':
return (0, _utils.createShorthand)(composeOptions.slots.group, item, {
defaultProps: function defaultProps() {
return slotProps.group;
}
});
case 'toggle':
return (0, _utils.createShorthand)(composeOptions.slots.toggle, item, {
defaultProps: function defaultProps() {
return slotProps.toggle;
}
});
case 'custom':
return (0, _utils.createShorthand)(composeOptions.slots.customItem, item, {
defaultProps: function defaultProps() {
return slotProps.customItem;
}
});
default:
return (0, _utils.createShorthand)(composeOptions.slots.item, item, {
defaultProps: function defaultProps() {
return slotProps.item;
}
});
}
});
};
var renderOverflowItem = function renderOverflowItem(overflowItem) {
return (0, _utils.createShorthand)(composeOptions.slots.overflowItem, overflowItem, {
defaultProps: function defaultProps() {
return slotProps.overflowItem;
},
overrideProps: function overrideProps(predefinedProps) {
var _predefinedProps$menu;
return {
menu: {
items: overflowOpen ? collectOverflowItems() : [],
popper: Object.assign({
positionFixed: true
}, (_predefinedProps$menu = predefinedProps.menu) == null ? void 0 : _predefinedProps$menu.popper)
},
menuOpen: overflowOpen,
onMenuOpenChange: function onMenuOpenChange(e, _ref) {
var menuOpen = _ref.menuOpen;
(0, _invoke2.default)(props, 'onOverflowOpenChange', e, Object.assign({}, props, {
overflowOpen: menuOpen
}));
},
wrapper: {
ref: overflowItemWrapperRef
}
};
}
});
};
// renders a sentinel div that maintains the toolbar dimensions when the the overflow menu is open
// hidden elements are removed from the DOM
var renderOverflowSentinel = function renderOverflowSentinel() {
return /*#__PURE__*/React.createElement(_reactComponentRef.Ref, {
innerRef: function innerRef(element) {
overflowSentinelRef.current = element;
}
}, _Box.Box.create(overflowSentinel, {
defaultProps: function defaultProps() {
return {
id: 'sentinel',
className: classes.overflowSentinel
};
}
}));
};
React.useEffect(function () {
var actualWindow = context.target.defaultView;
actualWindow.cancelAnimationFrame(animationFrameId.current);
// Heads up! There are cases (like opening a portal and rendering the Toolbar there immediately) when rAF is necessary
animationFrameId.current = actualWindow.requestAnimationFrame(function () {
hideOverflowItems();
});
return function () {
if (animationFrameId.current !== undefined) {
var _context$target$defau;
(_context$target$defau = context.target.defaultView) == null ? void 0 : _context$target$defau.cancelAnimationFrame(animationFrameId.current);
animationFrameId.current = undefined;
}
};
});
var element = overflow ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(_reactComponentRef.Ref, {
innerRef: function innerRef(node) {
containerRef.current = node;
(0, _reactComponentRef.handleRef)(ref, node);
}
}, getA11Props.unstable_wrapWithFocusZone( /*#__PURE__*/React.createElement(ElementType, getA11Props('root', Object.assign({
className: classes.root
}, unhandledProps)), /*#__PURE__*/React.createElement("div", {
className: classes.overflowContainer,
ref: overflowContainerRef
}, /*#__PURE__*/React.createElement(_toolbarMenuContext.ToolbarMenuContextProvider, {
value: {
slots: {
menu: composeOptions.slots.menu
}
}
}, /*#__PURE__*/React.createElement(_toolbarVariablesContext.ToolbarVariablesProvider, {
value: variables
}, (0, _utils.childrenExist)(children) ? children : renderItems(getVisibleItems()), overflowSentinel && renderOverflowSentinel(), renderOverflowItem(overflowItem)))), /*#__PURE__*/React.createElement("div", {
className: classes.offsetMeasure,
ref: offsetMeasureRef
})))), /*#__PURE__*/React.createElement(_reactComponentEventListener.EventListener, {
listener: handleWindowResize,
target: context.target.defaultView,
type: "resize"
})) : /*#__PURE__*/React.createElement(_reactComponentRef.Ref, {
innerRef: function innerRef(node) {
containerRef.current = node;
(0, _reactComponentRef.handleRef)(ref, node);
}
}, getA11Props.unstable_wrapWithFocusZone( /*#__PURE__*/React.createElement(ElementType, getA11Props('root', Object.assign({
className: classes.root
}, unhandledProps)), /*#__PURE__*/React.createElement(_toolbarMenuContext.ToolbarMenuContextProvider, {
value: {
slots: {
menu: composeOptions.slots.menu
}
}
}, /*#__PURE__*/React.createElement(_toolbarVariablesContext.ToolbarVariablesProvider, {
value: variables
}, (0, _utils.childrenExist)(children) ? children : renderItems(items))))));
setEnd();
return element;
}, {
className: toolbarClassName,
displayName: 'Toolbar',
slots: {
customItem: _ToolbarCustomItem.ToolbarCustomItem,
divider: _ToolbarDivider.ToolbarDivider,
item: _ToolbarItem.ToolbarItem,
group: _ToolbarRadioGroup.ToolbarRadioGroup,
toggle: _ToolbarItem.ToolbarItem,
overflowItem: _ToolbarItem.ToolbarItem,
menu: _ToolbarMenu.ToolbarMenu
},
slotProps: function slotProps() {
return {
toggle: {
accessibility: _accessibility.toggleButtonBehavior
},
overflowItem: {
icon: /*#__PURE__*/React.createElement(_reactIconsNorthstar.MoreIcon, {
outline: true
})
}
};
},
shorthandConfig: {
mappedProp: 'content'
},
handledProps: ['accessibility', 'as', 'children', 'className', 'content', 'design', 'getOverflowItems', 'items', 'onOverflow', 'onOverflowOpenChange', 'overflow', 'overflowItem', 'overflowOpen', 'overflowSentinel', 'styles', 'variables']
});
exports.Toolbar = Toolbar;
Toolbar.propTypes = Object.assign({}, _utils.commonPropTypes.createCommon(), {
items: customPropTypes.collectionShorthandWithKindProp(['divider', 'item', 'group', 'toggle', 'custom']),
overflow: PropTypes.bool,
overflowOpen: PropTypes.bool,
overflowSentinel: customPropTypes.shorthandAllowingChildren,
overflowItem: customPropTypes.shorthandAllowingChildren,
onOverflow: PropTypes.func,
onOverflowOpenChange: PropTypes.func,
getOverflowItems: PropTypes.func
});
Toolbar.defaultProps = {
accessibility: _accessibility.toolbarBehavior,
items: [],
overflowItem: {},
overflowSentinel: {}
};
Toolbar.CustomItem = _ToolbarCustomItem.ToolbarCustomItem;
Toolbar.Divider = _ToolbarDivider.ToolbarDivider;
Toolbar.Item = _ToolbarItem.ToolbarItem;
Toolbar.ItemWrapper = _ToolbarItemWrapper.ToolbarItemWrapper;
Toolbar.ItemIcon = _ToolbarItemIcon.ToolbarItemIcon;
Toolbar.Menu = _ToolbarMenu.ToolbarMenu;
Toolbar.MenuDivider = _ToolbarMenuDivider.ToolbarMenuDivider;
Toolbar.MenuItem = _ToolbarMenuItem.ToolbarMenuItem;
Toolbar.MenuItemIcon = _ToolbarMenuItemIcon.ToolbarMenuItemIcon;
Toolbar.MenuItemSubmenuIndicator = _ToolbarMenuItemSubmenuIndicator.ToolbarMenuItemSubmenuIndicator;
Toolbar.MenuItemActiveIndicator = _ToolbarMenuItemActiveIndicator.ToolbarMenuItemActiveIndicator;
Toolbar.MenuRadioGroup = _ToolbarMenuRadioGroup.ToolbarMenuRadioGroup;
Toolbar.MenuRadioGroupWrapper = _ToolbarMenuRadioGroupWrapper.ToolbarMenuRadioGroupWrapper;
Toolbar.RadioGroup = _ToolbarRadioGroup.ToolbarRadioGroup;
//# sourceMappingURL=Toolbar.js.map