diff --git a/docs/app/Examples/modules/Dropdown/States/DisabledItem.js b/docs/app/Examples/modules/Dropdown/States/DisabledItem.js new file mode 100644 index 0000000000..80b9a3ea7f --- /dev/null +++ b/docs/app/Examples/modules/Dropdown/States/DisabledItem.js @@ -0,0 +1,15 @@ +import _ from 'lodash' +import faker from 'faker' +import React, { Component } from 'react' +import { Dropdown } from 'stardust' + +const options = _.times(10, (i) => { + const name = faker.name.findName() + return { text: name, value: _.snakeCase(name), disabled: i % 3 == 0 } +}) + +const DropdownItemDisabledExample = () => ( + +) + +export default DropdownItemDisabledExample diff --git a/docs/app/Examples/modules/Dropdown/index.js b/docs/app/Examples/modules/Dropdown/index.js index 2a8a5c6200..cf5782d5c7 100644 --- a/docs/app/Examples/modules/Dropdown/index.js +++ b/docs/app/Examples/modules/Dropdown/index.js @@ -43,9 +43,14 @@ const DropdownExamples = () => ( + ) diff --git a/src/modules/Dropdown/Dropdown.js b/src/modules/Dropdown/Dropdown.js index e48abbcc01..7013f7f7df 100644 --- a/src/modules/Dropdown/Dropdown.js +++ b/src/modules/Dropdown/Dropdown.js @@ -386,9 +386,10 @@ export default class Dropdown extends Component { selectHighlightedItem = (e) => { const { multiple, onAddItem, options } = this.props const value = _.get(this.getSelectedItem(), 'value') + const disabled = _.get(this.getSelectedItem(), 'disabled') // prevent selecting null if there was no selected item value - if (!value) return + if (!value || disabled) return // notify the onAddItem prop if this is a new value if (onAddItem && !_.some(options, { text: value })) onAddItem(value) @@ -631,6 +632,9 @@ export default class Dropdown extends Component { const options = this.getMenuOptions() const lastIndex = options.length - 1 + // Prevent infinite loop + if (_.every(options, 'disabled')) return + // next is after last, wrap to beginning // next is before first, wrap to end let nextIndex = selectedIndex + offset @@ -638,6 +642,9 @@ export default class Dropdown extends Component { else if (nextIndex < 0) nextIndex = lastIndex this.setState({ selectedIndex: nextIndex }) + + if (options[nextIndex].disabled) return this.moveSelectionBy(offset) + this.scrollSelectedItemIntoView() } diff --git a/src/modules/Dropdown/DropdownItem.js b/src/modules/Dropdown/DropdownItem.js index 329b4d7ebd..d3331aca0e 100644 --- a/src/modules/Dropdown/DropdownItem.js +++ b/src/modules/Dropdown/DropdownItem.js @@ -16,6 +16,7 @@ function DropdownItem(props) { active, children, className, + disabled, description, icon, onClick, @@ -25,11 +26,18 @@ function DropdownItem(props) { } = props const handleClick = (e) => { + if (props.disabled) { + e.stopPropagation() + e.nativeEvent.stopImmediatePropagation() + return + } + if (onClick) onClick(e, value) } const classes = cx( useKeyOnly(active, 'active'), + useKeyOnly(disabled, 'disabled'), useKeyOnly(selected, 'selected'), 'item', className, @@ -78,6 +86,9 @@ DropdownItem.propTypes = { /** Additional text with less emphasis. */ description: PropTypes.string, + /** A dropdown item can be disabled. */ + disabled: PropTypes.bool, + /** Add an icon to the item. */ icon: PropTypes.string, diff --git a/test/specs/modules/Dropdown/Dropdown-test.js b/test/specs/modules/Dropdown/Dropdown-test.js index 9392707147..e5fc059278 100644 --- a/test/specs/modules/Dropdown/Dropdown-test.js +++ b/test/specs/modules/Dropdown/Dropdown-test.js @@ -193,6 +193,29 @@ describe('Dropdown Component', () => { .find('.selected') .should.contain.text('a2') }) + it('skips over disabled items', () => { + const opts = [ + { text: 'a1', value: 'a1' }, + { text: 'skip this one', value: 'skip this one', disabled: true }, + { text: 'a2', value: 'a2' }, + ] + // search for 'a' + wrapperMount() + .simulate('click') + .find('input.search') + .simulate('change', { target: { value: 'a' } }) + + wrapper + .find('.selected') + .should.contain.text('a1') + + // move selection down + domEvent.keyDown(document, { key: 'ArrowDown' }) + + wrapper + .find('.selected') + .should.contain.text('a2') + }) it('scrolls the selected item into view', () => { // get enough options to make the menu scrollable const opts = getOptions(20) @@ -260,6 +283,23 @@ describe('Dropdown Component', () => { item.should.have.prop('active', true) }) + it('does not become active on enter when disabled', () => { + const disabledOptions = _.map(options, (o) => ({ ...o, disabled: true })) + const item = wrapperMount() + .simulate('click') + .find('DropdownItem') + .at(0) + + // initial item props + item.should.have.prop('selected', true) + item.should.have.prop('active', false) + + // attempt to make active + domEvent.keyDown(document, { key: 'Enter' }) + + item.should.have.prop('active', false) + dropdownMenuIsOpen() + }) it('closes the menu', () => { wrapperMount() .simulate('click')