UNPKG

@fluentui/react-northstar

Version:
508 lines (491 loc) 22.2 kB
import _get from "lodash/get"; import _map from "lodash/map"; import _debounce from "lodash/debounce"; import _invoke from "lodash/invoke"; import _forEachRight from "lodash/forEachRight"; import { toolbarBehavior, toggleButtonBehavior, IS_FOCUSABLE_ATTRIBUTE } from '@fluentui/accessibility'; import { compose, getElementType, getFirstFocusable, useFluentContext, useAccessibility, useStyles, useTelemetry, useUnhandledProps } from '@fluentui/react-bindings'; import { EventListener } from '@fluentui/react-component-event-listener'; import { handleRef, Ref } from '@fluentui/react-component-ref'; import { MoreIcon } from '@fluentui/react-icons-northstar'; import * as customPropTypes from '@fluentui/react-proptypes'; import * as PropTypes from 'prop-types'; import * as React from 'react'; import { childrenExist, createShorthand, commonPropTypes } from '../../utils'; import { ToolbarCustomItem } from './ToolbarCustomItem'; import { ToolbarDivider } from './ToolbarDivider'; import { ToolbarItem } from './ToolbarItem'; import { ToolbarItemWrapper } from './ToolbarItemWrapper'; import { ToolbarItemIcon } from './ToolbarItemIcon'; import { ToolbarMenu } from './ToolbarMenu'; import { ToolbarMenuDivider } from './ToolbarMenuDivider'; import { ToolbarMenuItem } from './ToolbarMenuItem'; import { ToolbarMenuRadioGroup } from './ToolbarMenuRadioGroup'; import { ToolbarMenuRadioGroupWrapper } from './ToolbarMenuRadioGroupWrapper'; import { ToolbarRadioGroup } from './ToolbarRadioGroup'; import { ToolbarVariablesProvider } from './toolbarVariablesContext'; import { ToolbarMenuItemSubmenuIndicator } from './ToolbarMenuItemSubmenuIndicator'; import { ToolbarMenuItemIcon } from './ToolbarMenuItemIcon'; import { ToolbarMenuItemActiveIndicator } from './ToolbarMenuItemActiveIndicator'; import { ToolbarMenuContextProvider } from './toolbarMenuContext'; import { Box } from '../Box/Box'; var WAS_FOCUSABLE_ATTRIBUTE = 'data-was-focusable'; export 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) */ export var Toolbar = /*#__PURE__*/function () { var Toolbar = compose(function (props, ref, composeOptions) { var context = useFluentContext(); var _useTelemetry = 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 = useAccessibility(accessibility, { debugName: composeOptions.displayName, rtl: context.rtl }); var _useStyles = 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 = getElementType(props); var slotProps = composeOptions.resolveSlotProps(props); var unhandledProps = 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 = getFirstFocusable(containerRef.current, containerRef.current.firstElementChild); if (firstFocusableItem) { firstFocusableItem.focus(); } } } el.style.visibility = 'hidden'; var wasFocusable = el.getAttribute(IS_FOCUSABLE_ATTRIBUTE); if (wasFocusable) { el.setAttribute(WAS_FOCUSABLE_ATTRIBUTE, wasFocusable); } el.setAttribute(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(IS_FOCUSABLE_ATTRIBUTE, wasFocusable); el.removeAttribute(WAS_FOCUSABLE_ATTRIBUTE); } else { el.removeAttribute(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 _forEachRight($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); } _invoke(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 = _debounce(function (e) { hideOverflowItems(); if (overflowOpen) { _invoke(props, 'onOverflowOpenChange', e, Object.assign({}, props, { overflowOpen: false })); } }, 16); var renderItems = function renderItems(items) { return _map(items, function (item) { var kind = _get(item, 'kind', 'item'); switch (kind) { case 'divider': return createShorthand(composeOptions.slots.divider, item, { defaultProps: function defaultProps() { return slotProps.divider; } }); case 'group': return createShorthand(composeOptions.slots.group, item, { defaultProps: function defaultProps() { return slotProps.group; } }); case 'toggle': return createShorthand(composeOptions.slots.toggle, item, { defaultProps: function defaultProps() { return slotProps.toggle; } }); case 'custom': return createShorthand(composeOptions.slots.customItem, item, { defaultProps: function defaultProps() { return slotProps.customItem; } }); default: return createShorthand(composeOptions.slots.item, item, { defaultProps: function defaultProps() { return slotProps.item; } }); } }); }; var renderOverflowItem = function renderOverflowItem(overflowItem) { return 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; _invoke(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(Ref, { innerRef: function innerRef(element) { overflowSentinelRef.current = element; } }, 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(Ref, { innerRef: function innerRef(node) { containerRef.current = node; 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(ToolbarMenuContextProvider, { value: { slots: { menu: composeOptions.slots.menu } } }, /*#__PURE__*/React.createElement(ToolbarVariablesProvider, { value: variables }, childrenExist(children) ? children : renderItems(getVisibleItems()), overflowSentinel && renderOverflowSentinel(), renderOverflowItem(overflowItem)))), /*#__PURE__*/React.createElement("div", { className: classes.offsetMeasure, ref: offsetMeasureRef })))), /*#__PURE__*/React.createElement(EventListener, { listener: handleWindowResize, target: context.target.defaultView, type: "resize" })) : /*#__PURE__*/React.createElement(Ref, { innerRef: function innerRef(node) { containerRef.current = node; handleRef(ref, node); } }, getA11Props.unstable_wrapWithFocusZone( /*#__PURE__*/React.createElement(ElementType, getA11Props('root', Object.assign({ className: classes.root }, unhandledProps)), /*#__PURE__*/React.createElement(ToolbarMenuContextProvider, { value: { slots: { menu: composeOptions.slots.menu } } }, /*#__PURE__*/React.createElement(ToolbarVariablesProvider, { value: variables }, childrenExist(children) ? children : renderItems(items)))))); setEnd(); return element; }, { className: toolbarClassName, displayName: 'Toolbar', slots: { customItem: ToolbarCustomItem, divider: ToolbarDivider, item: ToolbarItem, group: ToolbarRadioGroup, toggle: ToolbarItem, overflowItem: ToolbarItem, menu: ToolbarMenu }, slotProps: function slotProps() { return { toggle: { accessibility: toggleButtonBehavior }, overflowItem: { icon: /*#__PURE__*/React.createElement(MoreIcon, { outline: true }) } }; }, shorthandConfig: { mappedProp: 'content' }, handledProps: ['accessibility', 'as', 'children', 'className', 'content', 'design', 'getOverflowItems', 'items', 'onOverflow', 'onOverflowOpenChange', 'overflow', 'overflowItem', 'overflowOpen', 'overflowSentinel', 'styles', 'variables'] }); Toolbar.propTypes = Object.assign({}, 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: toolbarBehavior, items: [], overflowItem: {}, overflowSentinel: {} }; Toolbar.CustomItem = ToolbarCustomItem; Toolbar.Divider = ToolbarDivider; Toolbar.Item = ToolbarItem; Toolbar.ItemWrapper = ToolbarItemWrapper; Toolbar.ItemIcon = ToolbarItemIcon; Toolbar.Menu = ToolbarMenu; Toolbar.MenuDivider = ToolbarMenuDivider; Toolbar.MenuItem = ToolbarMenuItem; Toolbar.MenuItemIcon = ToolbarMenuItemIcon; Toolbar.MenuItemSubmenuIndicator = ToolbarMenuItemSubmenuIndicator; Toolbar.MenuItemActiveIndicator = ToolbarMenuItemActiveIndicator; Toolbar.MenuRadioGroup = ToolbarMenuRadioGroup; Toolbar.MenuRadioGroupWrapper = ToolbarMenuRadioGroupWrapper; Toolbar.RadioGroup = ToolbarRadioGroup; return Toolbar; }(); //# sourceMappingURL=Toolbar.js.map