UNPKG

@salesforce/design-system-react

Version:

Salesforce Lightning Design System for React

728 lines (628 loc) 24.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "ListItemLabel", { enumerable: true, get: function get() { return _itemLabel.default; } }); exports.default = void 0; var _react = _interopRequireDefault(require("react")); var _reactDom = _interopRequireDefault(require("react-dom")); var _createReactClass = _interopRequireDefault(require("create-react-class")); var _propTypes = _interopRequireDefault(require("prop-types")); var _lodash = _interopRequireDefault(require("lodash.isfunction")); var _classnames = _interopRequireDefault(require("classnames")); var _checkProps = _interopRequireDefault(require("./check-props")); var _dialog = _interopRequireDefault(require("../utilities/dialog")); var _icon = _interopRequireDefault(require("../icon")); var _menuList = _interopRequireDefault(require("../utilities/menu-list")); var _itemLabel = _interopRequireDefault(require("../utilities/menu-list/item-label")); var _pill = _interopRequireDefault(require("../utilities/pill")); var _event = _interopRequireDefault(require("../../utilities/event")); var _generateId = _interopRequireDefault(require("../../utilities/generate-id")); var _keyboardNavigate = _interopRequireDefault(require("../../utilities/keyboard-navigate")); var _keyBuffer = _interopRequireDefault(require("../../utilities/key-buffer")); var _keyCode = _interopRequireDefault(require("../../utilities/key-code")); var _constants = require("../../utilities/constants"); var _iconSettings = require("../icon-settings"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /* eslint-disable max-lines */ /* eslint-disable react/no-access-state-in-setstate */ /* eslint-disable no-param-reassign */ /* eslint-disable prefer-destructuring */ /* eslint-disable max-lines */ /* Copyright (c) 2015-present, salesforce.com, inc. All rights reserved */ /* Licensed under BSD 3-Clause - see LICENSE.txt or git.io/sfdc-license */ /* eslint-disable react/prefer-es6-class */ // # Picklist Component [DEPRECATED] // Implements the [Picklist design pattern](https://www.lightningdesignsystem.com/components/menus/#flavor-picklist) in React. // Based on SLDS v2.1.0-rc.2 // ### classNames // [github.com/JedWatson/classnames](https://github.com/JedWatson/classnames) // This project uses `classnames`, "a simple javascript utility for conditionally // joining classNames together." // This component's `checkProps` which issues warnings to developers about properties // when in development mode (similar to React's built in development tools) // ### Children var noop = function noop() {}; var itemIsSelectable = function itemIsSelectable(item) { return item.type !== 'header' && item.type !== 'divider' && !item.disabled; }; var getNavigableItems = function getNavigableItems(items) { var navigableItems = []; navigableItems.indexes = []; navigableItems.keyBuffer = new _keyBuffer.default(); if (Array.isArray(items)) { items.forEach(function (item, index) { if (itemIsSelectable(item)) { // eslint-disable-next-line fp/no-mutating-methods navigableItems.push({ index: index, text: "".concat(item.label).toLowerCase() }); // eslint-disable-next-line fp/no-mutating-methods navigableItems.indexes.push(index); } }); } return navigableItems; }; function getMenuItem(menuItemId) { var context = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : document; var menuItem; if (menuItemId) { menuItem = context.getElementById(menuItemId); } return menuItem; } function getMenu(componentRef) { return _reactDom.default.findDOMNode(componentRef).querySelector('ul.dropdown__list'); // eslint-disable-line react/no-find-dom-node } /** * ** MenuPicklist is deprecated. Please use a read-only Combobox instead.** * * The MenuPicklist component is a variant of the Lightning Design System Menu component. */ var MenuPicklist = (0, _createReactClass.default)({ // ### Display Name // Always use the canonical component name as the React display name. displayName: _constants.MENU_PICKLIST, // ### Prop Types propTypes: { /** * Callback that passes in the DOM reference of the `<button>` DOM node within this component. Primary use is to allow `focus` to be called. You should still test if the node exists, since rendering is asynchronous. `buttonRef={(component) => { if(component) console.log(component); }}` */ buttonRef: _propTypes.default.func, className: _propTypes.default.string, /** * If true, renders checkmark icon on the selected Menu Item. */ checkmark: _propTypes.default.bool, disabled: _propTypes.default.bool, /** * Message to display when the input is in an error state. When this is present, also visually highlights the component as in error. */ errorText: _propTypes.default.string, /** * A unique ID is needed in order to support keyboard navigation, ARIA support, and connect the dropdown to the triggering button. */ id: _propTypes.default.string, /** * Renders menu within the wrapping trigger as a sibling of the button. By default, you will have an absolutely positioned container at an elevated z-index. */ isInline: _propTypes.default.bool, /** * Form element label */ label: _propTypes.default.string, /** * **Text labels for internationalization** * This object is merged with the default props object on every render. * * `multipleOptionsSelected`: Text to be used when multiple items are selected. "2 Options Selected" is a good pattern to use. */ labels: _propTypes.default.shape({ multipleOptionsSelected: _propTypes.default.string }), /** * Custom element that overrides the default Menu Item component. */ listItemRenderer: _propTypes.default.func, /** * Triggered when the trigger button is clicked to open. */ onClick: _propTypes.default.func, /** * Triggered when an item is selected. Passes in the option object that has been selected and a data object in the format: `{ option, optionIndex }`. The first parameter may be deprecated in the future and changed to an event for consistency. Please use the data object. */ onSelect: _propTypes.default.func, /** * Triggered when a pill is removed. Passes in the option object that has been removed and a data object in the format: `{ option, optionIndex }`. The first parameter may be deprecated in the future and changed to an event for consistency. Please use the data object. */ onPillRemove: _propTypes.default.func, /** * Menu item data. */ options: _propTypes.default.array.isRequired, /** * Text present in trigger button if no items are selected. */ placeholder: _propTypes.default.string, /** * Add styling of a required form element. */ required: _propTypes.default.bool, /** * Current selected item. */ value: _propTypes.default.node, /** * Initial selected item index. */ initValueIndex: _propTypes.default.number }, getDefaultProps: function getDefaultProps() { return { inheritTargetWidth: true, placeholder: 'Select an Option', checkmark: true, labels: { multipleOptionsSelected: 'Multiple Options Selected' }, menuPosition: 'absolute' }; }, getInitialState: function getInitialState() { return { focusedIndex: this.props.initValueIndex ? this.props.initValueIndex : -1, selectedIndex: this.props.initValueIndex ? this.props.initValueIndex : -1, selectedIndices: [], currentPillLabel: '' }; }, // eslint-disable-next-line camelcase, react/sort-comp UNSAFE_componentWillMount: function UNSAFE_componentWillMount() { // `checkProps` issues warnings to developers about properties (similar to React's built in development tools) (0, _checkProps.default)(_constants.MENU_PICKLIST, this.props); this.generatedId = (0, _generateId.default)(); if (this.props.errorText) { this.generatedErrorId = (0, _generateId.default)(); } if (typeof window !== 'undefined') { window.addEventListener('click', this.closeOnClick, false); } if (!this.props.multiple) { this.setState({ selectedIndex: this.getIndexByValue(this.props) }); } else { var currentSelectedIndex = this.getIndexByValue(this.props); var currentIndices = this.state.selectedIndices; if (currentSelectedIndex !== -1) { // eslint-disable-next-line fp/no-mutating-methods currentIndices.push(currentSelectedIndex); } this.setState({ selectedIndices: currentIndices }); } this.navigableItems = getNavigableItems(this.props.options); }, // eslint-disable-next-line camelcase, react/sort-comp UNSAFE_componentWillReceiveProps: function UNSAFE_componentWillReceiveProps(nextProps) { if (this.props.value !== nextProps.value || this.props.options.length !== nextProps.length) { if (this.props.multiple !== true) { this.setState({ selectedIndex: this.getIndexByValue(nextProps) }); } else { var currentSelectedIndex = this.getIndexByValue(nextProps); if (currentSelectedIndex !== -1) { var currentIndices = this.state.selectedIndices.concat(currentSelectedIndex); this.setState({ selectedIndices: currentIndices }); } } } if (nextProps.options) { this.navigableItems = getNavigableItems(nextProps.options); } }, componentWillUnmount: function componentWillUnmount() { this.isUnmounting = true; window.removeEventListener('click', this.closeOnClick, false); }, getListItemId: function getListItemId(index) { var menuItemId; if (index !== undefined) { var menuId = (0, _lodash.default)(this.getId) ? this.getId() : this.props.id; menuItemId = "".concat(menuId, "-item-").concat(index); } return menuItemId; }, getId: function getId() { return this.props.id || this.generatedId; }, getErrorId: function getErrorId() { return this.props['aria-describedby'] || this.generatedErrorId; }, getClickEventName: function getClickEventName() { return "SLDS".concat(this.getId(), "ClickEvent"); }, getIndexByValue: function getIndexByValue() { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.props, value = _ref.value, options = _ref.options; var foundIndex = -1; if (options && options.length) { options.some(function (element, index) { if (element && element.value === value) { foundIndex = index; return true; } return false; }); } return foundIndex; }, getValueByIndex: function getValueByIndex(index) { return this.props.options[index]; }, getListItemRenderer: function getListItemRenderer() { return this.props.listItemRenderer ? this.props.listItemRenderer : _itemLabel.default; }, setFocus: function setFocus() { if (!this.isUnmounting && this.button) { this.button.focus(); } }, handleSelect: function handleSelect(index) { if (!this.props.multiple) { this.setState({ selectedIndex: index }); this.handleClose(); this.setFocus(); } else { var currentIndices; if (this.state.selectedIndices.indexOf(index) === -1) { currentIndices = this.state.selectedIndices.concat(index); } else { var deselectIndex = this.state.selectedIndices.indexOf(index); currentIndices = this.state.selectedIndices; // eslint-disable-next-line fp/no-mutating-methods currentIndices.splice(deselectIndex, 1); } this.setState({ selectedIndices: currentIndices }); } if (this.props.onSelect) { var option = this.getValueByIndex(index); this.props.onSelect(option, { option: option, optionIndex: index }); } }, handleClose: function handleClose() { this.setState({ isOpen: false }); }, handleClick: function handleClick(event) { if (event) { event.nativeEvent[this.getClickEventName()] = true; } if (!this.state.isOpen) { this.setState({ isOpen: true }); this.setFocus(); if (this.props.onClick) { this.props.onClick(event); } } else { this.handleClose(); } }, handleMouseDown: function handleMouseDown(event) { if (event) { _event.default.trapImmediate(event); event.nativeEvent[this.getClickEventName()] = true; } }, handleKeyDown: function handleKeyDown(event) { if (event.keyCode) { if (event.keyCode === _keyCode.default.ENTER || event.keyCode === _keyCode.default.SPACE || event.keyCode === _keyCode.default.DOWN || event.keyCode === _keyCode.default.UP) { _event.default.trap(event); } if (event.keyCode !== _keyCode.default.TAB) { // The outer div with onKeyDown is overriding button onClick so we need to add it here. var openMenuKeys = event.keyCode === _keyCode.default.ENTER || event.keyCode === _keyCode.default.DOWN || event.keyCode === _keyCode.default.UP; var isTrigger = event.target.tagName === 'BUTTON'; if (openMenuKeys && isTrigger && this.props.onClick) { this.props.onClick(event); } this.handleKeyboardNavigate({ isOpen: this.state.isOpen || false, keyCode: event.keyCode, onSelect: this.handleSelect, toggleOpen: this.toggleOpen }); } else { this.handleCancel(); } } }, handleCancel: function handleCancel() { this.setFocus(); this.handleClose(); }, // Handling open / close toggling is optional, and a default implementation is provided for handling focus, but selection _must_ be handled handleKeyboardNavigate: function handleKeyboardNavigate(_ref2) { var event = _ref2.event, _ref2$isOpen = _ref2.isOpen, isOpen = _ref2$isOpen === void 0 ? true : _ref2$isOpen, keyCode = _ref2.keyCode, _ref2$onFocus = _ref2.onFocus, onFocus = _ref2$onFocus === void 0 ? this.handleKeyboardFocus : _ref2$onFocus, onSelect = _ref2.onSelect, target = _ref2.target, _ref2$toggleOpen = _ref2.toggleOpen, toggleOpen = _ref2$toggleOpen === void 0 ? noop : _ref2$toggleOpen; (0, _keyboardNavigate.default)({ componentContext: this, currentFocusedIndex: this.state.focusedIndex, event: event, isOpen: isOpen, keyCode: keyCode, navigableItems: this.navigableItems, onFocus: onFocus, onSelect: onSelect, target: target, toggleOpen: toggleOpen }); }, // This is a bit of an anti-pattern, but it has the upside of being a nice default. Component authors can always override to only set state and do their own focusing in their subcomponents. handleKeyboardFocus: function handleKeyboardFocus(focusedIndex) { if (this.state.focusedIndex !== focusedIndex) { this.setState({ focusedIndex: focusedIndex }); } var menu = (0, _lodash.default)(this.getMenu) ? this.getMenu() : getMenu(this); var menuItem = (0, _lodash.default)(this.getMenuItem) ? this.getMenuItem(focusedIndex, menu) : getMenuItem(this.getListItemId(focusedIndex)); if (menuItem) { this.focusMenuItem(menuItem); this.scrollToMenuItem(menu, menuItem); } }, focusMenuItem: function focusMenuItem(menuItem) { menuItem.getElementsByTagName('a')[0].focus(); }, scrollToMenuItem: function scrollToMenuItem(menu, menuItem) { if (menu && menuItem) { var menuHeight = menu.offsetHeight; var menuTop = menu.scrollTop; var menuItemTop = menuItem.offsetTop - menu.offsetTop; if (menuItemTop < menuTop) { menu.scrollTop = menuItemTop; } else { var menuBottom = menuTop + menuHeight + menu.offsetTop; var menuItemBottom = menuItemTop + menuItem.offsetHeight + menu.offsetTop; if (menuItemBottom > menuBottom) { menu.scrollTop = menuItemBottom - menuHeight - menu.offsetTop; } } } }, closeOnClick: function closeOnClick(event) { if (!event[this.getClickEventName()] && this.state.isOpen) { this.handleClose(); } }, toggleOpen: function toggleOpen() { this.setState({ isOpen: !this.state.isOpen }); }, saveRefToList: function saveRefToList(list) { this.list = list; }, saveRefToListItem: function saveRefToListItem(listItem, index) { if (!this.listItems) { this.listItems = {}; } this.listItems[index] = listItem; if (index === this.state.focusedIndex) { this.handleKeyboardFocus(this.state.focusedIndex); } }, // Trigger opens, closes, and recieves focus on close saveRefToTrigger: function saveRefToTrigger(trigger) { this.button = trigger; if (this.props.buttonRef) { this.props.buttonRef(this.button); } if (!this.state.triggerRendered) { this.setState({ triggerRendered: true }); } }, renderMenuContent: function renderMenuContent() { return /*#__PURE__*/_react.default.createElement(_menuList.default, { checkmark: this.props.checkmark, getListItemId: this.getListItemId, itemRefs: this.saveRefToListItem, itemRenderer: this.getListItemRenderer(), onCancel: this.handleCancel, onSelect: this.handleSelect, options: this.props.options, ref: this.saveRefToList, selectedIndex: !this.props.multiple ? this.state.selectedIndex : undefined, selectedIndices: this.props.multiple ? this.state.selectedIndices : undefined, triggerId: this.getId() }); }, renderInlineMenu: function renderInlineMenu() { return !this.props.disabled && this.state.isOpen ? /*#__PURE__*/_react.default.createElement("div", { className: "slds-dropdown slds-dropdown_left" // inline style override , style: { maxHeight: '20em', overflowX: 'hidden', minWidth: '100%' } }, this.renderMenuContent()) : null; }, renderDialog: function renderDialog() { var _this = this; return !this.props.disabled && this.state.isOpen ? /*#__PURE__*/_react.default.createElement(_dialog.default, { closeOnTabKey: true, constrainToScrollParent: this.props.constrainToScrollParent, contentsClassName: "slds-dropdown slds-dropdown_left", context: this.context, flippable: true, onClose: this.handleCancel, onKeyDown: this.handleKeyDown, onRequestTargetElement: function onRequestTargetElement() { return _this.button; }, inheritWidthOf: this.props.inheritTargetWidth ? 'target' : 'none', position: this.props.menuPosition }, this.renderMenuContent()) : null; }, renderTrigger: function renderTrigger() { var isInline; /* eslint-disable react/prop-types */ if (this.props.isInline) { isInline = true; } else if (this.props.modal !== undefined) { isInline = !this.props.modal; } /* eslint-enable react/prop-types */ var inputValue; if (this.props.multiple && this.state.selectedIndices.length === 0) { inputValue = this.props.placeholder; } else if (this.props.multiple && this.state.selectedIndices.length === 1) { var option = this.props.options[this.state.selectedIndices]; inputValue = option.label; } else if (this.props.multiple && this.state.selectedIndices.length > 1) { inputValue = this.props.labels.multipleOptionsSelected; } else { var _option = this.props.options[this.state.selectedIndex]; inputValue = _option && _option.label ? _option.label : this.props.placeholder; } // TODO: make use of <Button> return ( /*#__PURE__*/ // eslint-disable-next-line jsx-a11y/no-static-element-interactions _react.default.createElement("div", { className: (0, _classnames.default)('slds-picklist slds-dropdown-trigger slds-dropdown-trigger_click', { 'slds-is-open': this.state.isOpen }, this.props.className), onKeyDown: this.handleKeyDown, onMouseDown: this.handleMouseDown }, /*#__PURE__*/_react.default.createElement("button", { "aria-describedby": this.getErrorId(), "aria-expanded": this.state.isOpen, "aria-haspopup": "true", className: "slds-button slds-button_neutral slds-picklist__label", disabled: this.props.disabled, id: this.getId(), onClick: !this.props.disabled ? this.handleClick : undefined, ref: this.saveRefToTrigger, tabIndex: this.state.isOpen ? -1 : 0, type: "button" }, /*#__PURE__*/_react.default.createElement("span", { className: "slds-truncate" }, inputValue), /*#__PURE__*/_react.default.createElement(_icon.default, { name: "down", category: "utility" })), isInline ? this.renderInlineMenu() : this.renderDialog()) ); }, renderPills: function renderPills() { var _this2 = this; var selectedPills = this.state.selectedIndices.map(function (selectedPill) { var pillLabel = _this2.getValueByIndex(selectedPill).label; return /*#__PURE__*/_react.default.createElement("li", { className: "slds-listbox__item", key: "pill-".concat(selectedPill), role: "presentation" }, /*#__PURE__*/_react.default.createElement(_pill.default, { eventData: { item: _this2.props.options[selectedPill], index: selectedPill }, events: { onRequestFocus: function onRequestFocus() {}, onRequestFocusOnNextPill: function onRequestFocusOnNextPill() {}, onRequestFocusOnPreviousPill: function onRequestFocusOnPreviousPill() {}, onRequestRemove: function onRequestRemove(event, data) { var newData = _this2.state.selectedIndices; var index = data.index; // eslint-disable-next-line fp/no-mutating-methods newData.splice(_this2.state.selectedIndices.indexOf(index), 1); _this2.setState({ selectedIndices: newData }); if (_this2.props.onPillRemove) { var option = _this2.getValueByIndex(index); _this2.props.onPillRemove(option, { option: option, optionIndex: index }); } } }, labels: { label: pillLabel } })); }); return /*#__PURE__*/_react.default.createElement("div", { id: "listbox-selections-unique-id", orientation: "horizontal", role: "listbox" }, /*#__PURE__*/_react.default.createElement("ul", { className: "slds-listbox slds-listbox_inline slds-p-top_xxx-small", role: "group", "aria-label": "Selected Options:" }, selectedPills)); }, render: function render() { var _this$props = this.props, className = _this$props.className, errorText = _this$props.errorText, label = _this$props.label, required = _this$props.required; var requiredElem = required ? /*#__PURE__*/ // eslint-disable-next-line react/jsx-curly-brace-presence _react.default.createElement("span", { style: { color: 'red' } }, '* ') : null; return /*#__PURE__*/_react.default.createElement("div", { className: (0, _classnames.default)('slds-form-element', { 'slds-has-error': errorText }, className) }, this.props.label ? /*#__PURE__*/_react.default.createElement("label", { className: "slds-form-element__label", htmlFor: this.getId() // inline style override , style: { width: '100%' } }, requiredElem, label) : null, this.renderTrigger(), this.renderPills(), errorText && /*#__PURE__*/_react.default.createElement("div", { id: this.getErrorId(), className: "slds-form-element__help" }, errorText)); } }); MenuPicklist.contextType = _iconSettings.IconSettingsContext; var _default = MenuPicklist; exports.default = _default;