UNPKG

@workday/canvas-kit-preview-react

Version:

Canvas Kit Preview is made up of components that have the full design and a11y review, are part of the DS ecosystem and are approved for use in product. The API's could be subject to change, but not without strong communication and migration strategies.

279 lines (278 loc) 12.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DeprecatedMenu = void 0; const React = __importStar(require("react")); const styled_1 = __importDefault(require("@emotion/styled")); const card_1 = require("@workday/canvas-kit-react/card"); const tokens_1 = require("@workday/canvas-kit-react/tokens"); const common_1 = require("@workday/canvas-kit-react/common"); const List = (0, styled_1.default)('ul')({ background: tokens_1.commonColors.background, borderRadius: tokens_1.borderRadius.m, padding: 0, margin: `${tokens_1.space.xxs} 0`, '&:focus-visible, &.focus': { outline: 'none', }, }); /** * `DeprecatedMenu` renders a styled `<ul role="menu">` element within a {@link Card} and follows * the [Active Menu * pattern](https://www.w3.org/TR/wai-aria-practices/examples/menu-button/menu-button-actions-active-descendant.html) * using `aria-activedescendant`. * * Undocumented props are spread to the underlying `<ul>` element. * * @deprecated ⚠️ Deprecated Menu has been deprecated and will be removed in a future major version. Please use [Menu in Main](https://workday.github.io/canvas-kit/?path=/docs/components-popups-menu--basic) instead. */ class DeprecatedMenu extends React.Component { constructor(props) { super(props); this.id = (0, common_1.generateUniqueId)(); this.getNormalizedItemIndex = (index) => { const itemCount = React.Children.count(this.props.children); const firstItem = 0; const lastItem = itemCount - 1; if (!index) { return firstItem; } return index < 0 ? firstItem : index >= itemCount ? lastItem : index; }; this.setNormalizedItemIndex = (index) => { this.setState({ selectedItemIndex: this.getNormalizedItemIndex(index) }); }; this.handleKeyboardShortcuts = (event) => { if (event.ctrlKey || event.altKey || event.metaKey) { return; } const children = React.Children.toArray(this.props.children); let nextSelectedIndex = 0; let isShortcut = false; const interactiveItems = children.filter(child => { var _a; return !((_a = child === null || child === void 0 ? void 0 : child.props) === null || _a === void 0 ? void 0 : _a.isHeader); }); const interactiveItemCount = interactiveItems.length; const firstIndex = 0; const lastIndex = interactiveItemCount - 1; if (event.key.length === 1 && event.key.match(/\S/)) { let start = this.state.selectedItemIndex + 1; let searchIndex; if (start === children.length) { start = 0; } searchIndex = this.getIndexFirstChars(start, event.key.toLowerCase()); if (searchIndex === -1) { searchIndex = this.getIndexFirstChars(0, event.key.toLowerCase(), start); } if (searchIndex > -1) { isShortcut = true; nextSelectedIndex = searchIndex; } } else { switch (event.key) { case 'ArrowUp': case 'ArrowDown': const direction = event.key === 'ArrowUp' ? -1 : 1; isShortcut = true; const nextIndex = this.state.selectedItemIndex + direction; nextSelectedIndex = nextIndex < 0 ? lastIndex : nextIndex >= interactiveItemCount ? firstIndex : nextIndex; break; case 'Home': case 'End': const skipTo = event.key === 'Home' ? firstIndex : lastIndex; isShortcut = true; nextSelectedIndex = skipTo; break; case 'Tab': if (this.props.onClose) { this.props.onClose(); } break; case 'Escape': case 'Esc': // IE/Edge specific value isShortcut = true; if (this.props.onClose) { this.props.onClose(); } break; case 'Spacebar': case ' ': case 'Enter': nextSelectedIndex = this.state.selectedItemIndex; const child = interactiveItems[this.state.selectedItemIndex]; this.handleClick(event, child.props); isShortcut = true; break; default: } } if (isShortcut) { this.setNormalizedItemIndex(nextSelectedIndex); event.stopPropagation(); event.preventDefault(); } }; this.handleClick = (event, menuItemProps) => { /* istanbul ignore next line for coverage */ if (menuItemProps.isDisabled) { // You should only hit this point if you are using a custom DeprecatedMenuItem implementation. return; } if (menuItemProps.onClick) { menuItemProps.onClick(event); } if (this.props.onSelect) { this.props.onSelect(); } if (this.props.onClose) { if (menuItemProps.shouldClose) { this.props.onClose(); } } }; this.getIndexFirstChars = (startIndex, character, lastIndex = this.firstCharacters.length) => { for (let i = startIndex; i < lastIndex; i++) { if (character === this.firstCharacters[i]) { return i; } } return -1; }; this.setFirstCharacters = () => { const getFirstCharacter = (child) => { let character = ''; if (!child || typeof child === 'boolean') { character = ''; } else if (typeof child === 'string' || typeof child === 'number') { character = child.toString().trim().substring(0, 1).toLowerCase(); } else if (Array.isArray(child) && child[0]) { // TODO test React.ReactNodeArray character = getFirstCharacter(child[0]); } else if ('props' in child) { const { children } = child.props; if (Array.isArray(children)) { character = getFirstCharacter(children[0]); } else { character = getFirstCharacter(children); } } return character; }; const firstCharacters = React.Children.map(this.props.children, child => { var _a; if ((_a = child === null || child === void 0 ? void 0 : child.props) === null || _a === void 0 ? void 0 : _a.isHeader) { return; } return getFirstCharacter(child); }); this.firstCharacters = firstCharacters; }; this.getInitialSelectedItem = () => { let selected = this.props.initialSelectedItem || 0; selected = selected < 0 ? React.Children.count(this.props.children) + selected : selected; if (selected < 0) { selected = 0; } else if (selected > React.Children.count(this.props.children) - 1) { selected = React.Children.count(this.props.children) - 1; } return selected; }; this.setInitialSelectedItem = () => { const selected = this.getInitialSelectedItem(); this.setState({ selectedItemIndex: selected }); }; this.menuRef = React.createRef(); const selected = this.getInitialSelectedItem(); // We track the active menu item by index so we can avoid setting a bunch of refs // for doing things like selecting an item by first character (or really calling .focus() at all) // It allows us to use the activedescendant design pattern // https://www.w3.org/TR/wai-aria-practices/examples/menu-button/menu-button-actions-active-descendant.html this.state = { selectedItemIndex: selected, }; } componentDidUpdate(prevProps) { if (this.props.children !== prevProps.children) { this.setFirstCharacters(); this.setInitialSelectedItem(); } if (this.props.isOpen && !prevProps.isOpen) { this.setInitialSelectedItem(); } this.animateId = requestAnimationFrame(() => { if (this.props.isOpen && this.menuRef.current) { this.menuRef.current.focus(); } }); } componentDidMount() { console.warn(`This component is being deprecated and will be removed in a future major version.\n For more information, please see the V8 upgrade guide:\n https://workday.github.io/canvas-kit/?path=/docs/guides-upgrade-guides-v8-0--docs#menu-preview `); this.setFirstCharacters(); this.setInitialSelectedItem(); } componentWillUnmount() { cancelAnimationFrame(this.animateId); } render() { // TODO: Standardize on prop spread location (see #150) const { id = this.id, isOpen = true, children, 'aria-labelledby': ariaLabelledby, grow, width, onSelect, onClose, initialSelectedItem, ...elemProps } = this.props; const { selectedItemIndex } = this.state; const cardWidth = grow ? '100%' : width; let interactiveItemIndex = null; return (React.createElement(card_1.Card, { display: "inline-block", padding: tokens_1.space.zero, width: cardWidth, depth: 3 }, React.createElement(card_1.Card.Body, null, React.createElement(List, { role: "menu", tabIndex: 0, id: id, "aria-labelledby": ariaLabelledby, "aria-activedescendant": `${id}-${selectedItemIndex}`, onKeyDown: this.handleKeyboardShortcuts, ref: this.menuRef, ...elemProps }, React.Children.map(children, menuItem => { if (!React.isValidElement(menuItem)) { return; } let itemId; if (!menuItem.props.isHeader) { interactiveItemIndex = (interactiveItemIndex !== null && interactiveItemIndex !== void 0 ? interactiveItemIndex : -1) + 1; itemId = `${id}-${interactiveItemIndex}`; } return (React.createElement(React.Fragment, { key: itemId }, React.cloneElement(menuItem, { onClick: (event) => this.handleClick(event, menuItem.props), id: itemId, isFocused: selectedItemIndex === interactiveItemIndex && !menuItem.props.isHeader, }))); }))))); } } exports.DeprecatedMenu = DeprecatedMenu;