/* * The MIT License (MIT) * * Copyright (c) 2015 - present Instructure, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ import { ComponentElement, Children, Component } from 'react' import { withStyleLegacy as withStyle } from '@instructure/emotion' import { omitProps, safeCloneElement, matchComponentTypes, withDeterministicId } from '@instructure/ui-react-utils' import { hasVisibleChildren } from '@instructure/ui-a11y-utils' import { MenuItem } from '../MenuItem' import type { OnMenuItemSelect, MenuItemProps } from '../MenuItem/props' import { MenuItemSeparator } from '../MenuItemSeparator' import type { MenuSeparatorProps } from '../MenuItemSeparator/props' import generateStyle from './styles' import generateComponentTheme from './theme' import { allowedProps } from './props' import type { MenuGroupProps, MenuGroupState } from './props' type MenuItemChild = ComponentElement type MenuSeparatorChild = ComponentElement< MenuSeparatorProps, MenuItemSeparator > /** --- parent: Menu id: Menu.Group --- **/ @withDeterministicId() @withStyle(generateStyle, generateComponentTheme) class MenuItemGroup extends Component { static readonly componentId = 'Menu.Group' static allowedProps = allowedProps static defaultProps = { disabled: false, children: null, isTabbable: false, allowMultiple: false, defaultSelected: [] } constructor(props: MenuGroupProps) { super(props) if (typeof props.selected === 'undefined') { this.state = { selected: this.selectedFromChildren(props) || props.defaultSelected! } } } ref: Element | null = null handleRef = (el: Element | null) => { this.ref = el } componentDidMount() { this.props.makeStyles?.() } componentDidUpdate() { this.props.makeStyles?.() } handleSelect: OnMenuItemSelect = (e, value, selected, item) => { if (this.props.disabled) { e.preventDefault() return } if (this.props.selected) { this.updateSelected(e, value, this.props.selected, selected, item) } else { this.setState((state) => { return { selected: this.updateSelected( e, value, state.selected, selected, item ) } }) } } updateSelected = ( e: React.MouseEvent, value: MenuItemProps['value'], items: MenuGroupState['selected'], selected: MenuItemProps['selected'], item: MenuItem ) => { const { allowMultiple } = this.props let updated = allowMultiple ? [...items] : [] const location = updated.indexOf(value!) if (selected === true && location < 0) { updated.push(value!) } else if (selected === false && location !== -1) { updated.splice(location, 1) } else if (!allowMultiple && updated.length < 1) { // don't allow nothing selected if it's not allowMultiple/checkbox updated = [...items] } if (typeof this.props.onSelect === 'function') { this.props.onSelect(e, updated, selected, item) } return updated } selectedFromChildren(props: MenuGroupProps) { const { children, allowMultiple } = props const selected: MenuGroupState['selected'] = [] const items = ( Children.toArray(children) as (MenuItemChild | MenuSeparatorChild)[] ).filter((child) => { return matchComponentTypes(child, [MenuItem]) }) as MenuItemChild[] items.forEach((item, index) => { if ( (selected.length === 0 || allowMultiple) && (item.props.selected || item.props.defaultSelected) ) { selected.push(item.props.value || index) } }) return selected.length > 0 ? selected : null } get selected() { if ( typeof this.props.selected === 'undefined' && typeof this.state.selected === 'undefined' ) { return [] } else { return typeof this.props.selected === 'undefined' ? [...this.state.selected] : [...this.props.selected] } } renderLabel() { const { label } = this.props return hasVisibleChildren(label) ? ( {label} ) : ( label ) } renderChildren() { const { disabled, controls, allowMultiple, isTabbable, onMouseOver } = this.props const children = this.props.children as ( | MenuItemChild | MenuSeparatorChild )[] let index = -1 return Children.map(children, (child) => { if (matchComponentTypes(child, [MenuItem])) { ++index const value = child.props.value || index return safeCloneElement(child, { tabIndex: isTabbable && index === 0 ? 0 : -1, controls, value, children: child.props.children, type: allowMultiple ? 'checkbox' : 'radio', ref: this.props.itemRef, disabled: disabled || child.props.disabled, selected: this.selected.indexOf(value) > -1, onSelect: this.handleSelect, onMouseOver }) } else { return child } }) } render() { const props = omitProps(this.props, MenuItemGroup.allowedProps) return ( {this.renderLabel()}
{this.renderChildren()}
) } } export default MenuItemGroup export { MenuItemGroup }