UNPKG

d2-ui

Version:
441 lines (386 loc) 12 kB
import React from 'react'; import Transitions from '../styles/transitions'; import DropDownArrow from '../svg-icons/navigation/arrow-drop-down'; import Menu from '../menus/menu'; import MenuItem from '../menus/menu-item'; import ClearFix from '../clearfix'; import getMuiTheme from '../styles/getMuiTheme'; import Popover from '../popover/popover'; import PopoverAnimationFromTop from '../popover/popover-animation-from-top'; import {mergeStyles, prepareStyles} from '../utils/styles'; import warning from 'warning'; import deprecated from '../utils/deprecatedPropType'; const DropDownMenu = React.createClass({ // The nested styles for drop-down-menu are modified by toolbar and possibly // other user components, so it will give full access to its js styles rather // than just the parent. propTypes: { /** * The width will automatically be set according to the items inside the menu. * To control this width in css instead, set this prop to false. */ autoWidth: React.PropTypes.bool, /** * The `MenuItem`s to populate the `Menu` with. If the `MenuItems` have the * prop `label` that value will be used to render the representation of that * item within the field. */ children: React.PropTypes.node, /** * The css class name of the root element. */ className: React.PropTypes.string, /** * Disables the menu. */ disabled: React.PropTypes.bool, /** * `DropDownMenu` will use this member to display * the name of the item. */ displayMember: deprecated(React.PropTypes.string, 'Instead, use composability.'), /** * Overrides the styles of icon element. */ iconStyle: React.PropTypes.object, /** * `DropDownMenu` will use this member to display * the name of the item on the label. */ labelMember: deprecated(React.PropTypes.string, 'Instead, use composability.'), /** * Overrides the styles of label when the `DropDownMenu` is inactive. */ labelStyle: React.PropTypes.object, /** * The maximum height of the `Menu` when it is displayed. */ maxHeight: React.PropTypes.number, /** * JSON data representing all menu items in the dropdown. */ menuItems: deprecated(React.PropTypes.array, 'Instead, use composability.'), /** * Overrides the styles of `Menu` when the `DropDownMenu` is displayed. */ menuStyle: React.PropTypes.object, /** * Fired when a menu item is clicked that is not the one currently selected. */ onChange: React.PropTypes.func, /** * Set to true to have the `DropDownMenu` automatically open on mount. */ openImmediately: React.PropTypes.bool, /** * Index of the item selected. */ selectedIndex: deprecated(React.PropTypes.number, 'Use value instead to control the component.'), /** * Override the inline-styles of the root element. */ style: React.PropTypes.object, /** * Overrides the inline-styles of the underline. */ underlineStyle: React.PropTypes.object, /** * The value that is currently selected. */ value: React.PropTypes.any, /** * Two-way binding link. */ valueLink: deprecated(React.PropTypes.object, 'It\'s deprecated by React too.'), /** * `DropDownMenu` will use this member as the value representing an item. */ valueMember: deprecated(React.PropTypes.string, 'Instead, use composability.'), }, contextTypes: { muiTheme: React.PropTypes.object, }, //for passing default theme context to children childContextTypes: { muiTheme: React.PropTypes.object, }, getDefaultProps() { return { autoWidth: true, disabled: false, openImmediately: false, maxHeight: 500, }; }, getInitialState() { return { open: this.props.openImmediately, selectedIndex: this._isControlled() ? null : (this.props.selectedIndex || 0), muiTheme: this.context.muiTheme || getMuiTheme(), }; }, getChildContext() { return { muiTheme: this.state.muiTheme, }; }, componentDidMount() { if (this.props.autoWidth) this._setWidth(); if (this.props.hasOwnProperty('selectedIndex')) this._setSelectedIndex(this.props.selectedIndex); }, componentWillReceiveProps(nextProps, nextContext) { const newMuiTheme = nextContext.muiTheme ? nextContext.muiTheme : this.state.muiTheme; this.setState({muiTheme: newMuiTheme}); if (this.props.autoWidth) this._setWidth(); if (nextProps.hasOwnProperty('value') || nextProps.hasOwnProperty('valueLink')) { return; } else if (nextProps.hasOwnProperty('selectedIndex')) { this._setSelectedIndex(nextProps.selectedIndex); } }, getStyles() { const {disabled} = this.props; const spacing = this.state.muiTheme.rawTheme.spacing; const palette = this.state.muiTheme.rawTheme.palette; const accentColor = this.state.muiTheme.dropDownMenu.accentColor; return { control: { cursor: disabled ? 'not-allowed' : 'pointer', height: '100%', position: 'relative', width: '100%', }, icon: { fill: accentColor, position: 'absolute', right: spacing.desktopGutterLess, top: ((spacing.desktopToolbarHeight - 24) / 2), }, label: { color: disabled ? palette.disabledColor : palette.textColor, lineHeight: `${spacing.desktopToolbarHeight}px`, opacity: 1, position: 'relative', paddingLeft: spacing.desktopGutter, paddingRight: spacing.iconSize + spacing.desktopGutterLess + spacing.desktopGutterMini, top: 0, }, labelWhenOpen: { opacity: 0, top: (spacing.desktopToolbarHeight / 8), }, rootWhenOpen: { opacity: 1, }, root: { display: 'inline-block', fontSize: spacing.desktopDropDownMenuFontSize, height: spacing.desktopSubheaderHeight, fontFamily: this.state.muiTheme.rawTheme.fontFamily, outline: 'none', position: 'relative', transition: Transitions.easeOut(), }, underline: { borderTop: `solid 1px ${accentColor}`, bottom: 1, left: 0, margin: `-1px ${spacing.desktopGutter}px`, right: 0, position: 'absolute', }, }; }, /** * This method is deprecated but still here because the TextField * need it in order to work. That will be addressed later. */ getInputNode() { const root = this.refs.root; const item = this.props.menuItems && this.props.menuItems[this.state.selectedIndex]; if (item) { root.value = item[this.props.displayMember || 'text']; } root.focus = () => { if (!this.props.disabled) { this.setState({ open: !this.state.open, anchorEl: this.refs.root, }); } }; return root; }, _setWidth() { const el = this.refs.root; if (!this.props.style || !this.props.style.hasOwnProperty('width')) { el.style.width = 'auto'; } }, _setSelectedIndex(index) { warning(index >= 0, 'Cannot set selectedIndex to a negative index.'); this.setState({selectedIndex: (index >= 0) ? index : 0}); }, _onControlTouchTap(event) { event.preventDefault(); if (!this.props.disabled) { this.setState({ open: !this.state.open, anchorEl: this.refs.root, }); } }, _onMenuItemTouchTap(key, payload, e) { const { onChange, menuItems, value, valueLink, valueMember, } = this.props; if (menuItems && (this.state.selectedIndex !== key || e.target.value !== value)) { const selectedItem = menuItems[key]; if (selectedItem) { e.target.value = selectedItem[valueMember || 'payload']; } this._onMenuRequestClose(); } if (valueLink) { valueLink.requestChange(e.target.value); } else if (onChange) { onChange(e, key, payload); } this.setState({ selectedIndex: key, open: false, }); }, _onMenuRequestClose() { this.setState({ open: false, anchorEl: null, }); }, _isControlled() { return this.props.hasOwnProperty('value') || this.props.hasOwnProperty('valueLink'); }, render() { const { autoWidth, children, className, displayMember, iconStyle, labelMember, labelStyle, maxHeight, menuItems, menuStyle, style, underlineStyle, valueLink, valueMember = 'payload', ...other, } = this.props; const { anchorEl, open, muiTheme, } = this.state; const styles = this.getStyles(); let value; let selectedIndex = this._isControlled() ? null : this.state.selectedIndex; if (menuItems && typeof selectedIndex === 'number') { warning(menuItems[selectedIndex], `SelectedIndex of ${selectedIndex} does not exist in menuItems.`); } if (valueMember && this._isControlled()) { value = this.props.hasOwnProperty('value') ? this.props.value : valueLink.value; if (menuItems && value !== null && value !== undefined) { for (let i = 0; i < menuItems.length; i++) { if (menuItems[i][valueMember] === value) { selectedIndex = i; } } } } let displayValue = ''; if (menuItems) { const selectedItem = menuItems[selectedIndex]; if (selectedItem) { displayValue = selectedItem[labelMember || 'text'] || selectedItem[displayMember || 'text']; } } else { React.Children.forEach(children, child => { if (value === child.props.value) { // This will need to be improved (in case primaryText is a node) displayValue = child.props.label || child.props.primaryText; } }); } let index = 0; const menuItemElements = menuItems ? menuItems.map((item, idx) => ( <MenuItem key={idx} primaryText={item[displayMember || 'text']} value={item[valueMember]} onTouchTap={this._onMenuItemTouchTap.bind(this, idx, item)} /> )) : React.Children.map(children, child => { const clone = React.cloneElement(child, { onTouchTap: this._onMenuItemTouchTap.bind(this, index, child.props.value), }, child.props.children); index += 1; return clone; }); let popoverStyle; if (anchorEl && !autoWidth) { popoverStyle = {width: anchorEl.clientWidth}; } return ( <div {...other} ref="root" className={className} style={prepareStyles(muiTheme, mergeStyles(styles.root, open && styles.rootWhenOpen, style))} > <ClearFix style={mergeStyles(styles.control)} onTouchTap={this._onControlTouchTap}> <div style={prepareStyles(muiTheme, mergeStyles(styles.label, open && styles.labelWhenOpen, labelStyle))}> {displayValue} </div> <DropDownArrow style={mergeStyles(styles.icon, iconStyle)}/> <div style={prepareStyles(muiTheme, mergeStyles(styles.underline, underlineStyle))}/> </ClearFix> <Popover anchorOrigin={{horizontal: 'left', vertical: 'top'}} anchorEl={anchorEl} style={popoverStyle} animation={PopoverAnimationFromTop} open={open} onRequestClose={this._onMenuRequestClose} > <Menu maxHeight={maxHeight} desktop={true} value={value} style={menuStyle} > {menuItemElements} </Menu> </Popover> </div> ); }, }); export default DropDownMenu;