diff --git a/docs/app/Examples/modules/Dropdown/Usage/DropdownExampleSearchInput.js b/docs/app/Examples/modules/Dropdown/Usage/DropdownExampleSearchInput.js
new file mode 100644
index 0000000000..4aca6d89a4
--- /dev/null
+++ b/docs/app/Examples/modules/Dropdown/Usage/DropdownExampleSearchInput.js
@@ -0,0 +1,21 @@
+import React from 'react'
+import { Dropdown } from 'semantic-ui-react'
+
+const options = [
+ { key: 100, text: '100', value: 100 },
+ { key: 200, text: '200', value: 200 },
+ { key: 300, text: '300', value: 300 },
+ { key: 400, text: '400', value: 400 },
+]
+
+const DropdownExampleSearchInput = () => (
+
+)
+
+export default DropdownExampleSearchInput
diff --git a/docs/app/Examples/modules/Dropdown/Usage/index.js b/docs/app/Examples/modules/Dropdown/Usage/index.js
index 0fd670bb74..25143eb24d 100644
--- a/docs/app/Examples/modules/Dropdown/Usage/index.js
+++ b/docs/app/Examples/modules/Dropdown/Usage/index.js
@@ -76,6 +76,11 @@ const DropdownUsageExamples = () => (
description='A dropdown item can be rendered differently inside the menu.'
examplePath='modules/Dropdown/Usage/DropdownExampleItemContent'
/>
+
, value: string) => Array);
+ /** A shorthand for a search input. */
+ searchInput?: any;
+
/** Define whether the highlighted item should be selected on blur. */
selectOnBlur?: boolean;
@@ -245,6 +249,7 @@ interface DropdownComponent extends React.ComponentClass {
Header: typeof DropdownHeader;
Item: typeof DropdownItem;
Menu: typeof DropdownMenu;
+ SearchInput: typeof DropdownSearchInput;
}
declare const Dropdown: DropdownComponent;
diff --git a/src/modules/Dropdown/Dropdown.js b/src/modules/Dropdown/Dropdown.js
index 5919cadb47..1274d12f0e 100644
--- a/src/modules/Dropdown/Dropdown.js
+++ b/src/modules/Dropdown/Dropdown.js
@@ -23,6 +23,7 @@ import DropdownDivider from './DropdownDivider'
import DropdownItem from './DropdownItem'
import DropdownHeader from './DropdownHeader'
import DropdownMenu from './DropdownMenu'
+import DropdownSearchInput from './DropdownSearchInput'
const debug = makeDebugger('dropdown')
@@ -276,6 +277,13 @@ export default class Dropdown extends Component {
PropTypes.func,
]),
+ /** A shorthand for a search input. */
+ searchInput: PropTypes.oneOfType([
+ PropTypes.array,
+ PropTypes.node,
+ PropTypes.object,
+ ]),
+
// TODO 'searchInMenu' or 'search='in menu' or ??? How to handle this markup and functionality?
/** Define whether the highlighted item should be selected on blur. */
@@ -338,6 +346,7 @@ export default class Dropdown extends Component {
noResultsMessage: 'No results found.',
openOnFocus: true,
renderLabel: ({ text }) => text,
+ searchInput: 'text',
selectOnBlur: true,
}
@@ -356,6 +365,7 @@ export default class Dropdown extends Component {
static Header = DropdownHeader
static Item = DropdownItem
static Menu = DropdownMenu
+ static SearchInput = DropdownSearchInput
componentWillMount() {
debug('componentWillMount()')
@@ -713,14 +723,16 @@ export default class Dropdown extends Component {
this.setState({ focus: false, searchQuery: '' })
}
- handleSearchChange = e => {
- debug('handleSearchChange()', e)
+ handleSearchChange = (e, { value }) => {
+ debug('handleSearchChange()')
+ debug(value)
+
// prevent propagating to this.props.onChange()
e.stopPropagation()
const { minCharacters } = this.props
const { open } = this.state
- const newQuery = _.get(e, 'target.value', '')
+ const newQuery = value
_.invoke(this.props, 'onSearchChange', e, newQuery)
this.setState({
@@ -958,6 +970,40 @@ export default class Dropdown extends Component {
handleRef = c => (this.ref = c)
+ // ----------------------------------------
+ // Helpers
+ // ----------------------------------------
+
+ computeSearchInputTabIndex = () => {
+ const { disabled, tabIndex } = this.props
+
+ if (!_.isNil(tabIndex)) return tabIndex
+ return disabled ? -1 : 0
+ }
+
+ computeSearchInputWidth = () => {
+ const { searchQuery } = this.state
+
+ if (this.sizerRef && searchQuery) {
+ // resize the search input, temporarily show the sizer so we can measure it
+
+ this.sizerRef.style.display = 'inline'
+ this.sizerRef.textContent = searchQuery
+ const searchWidth = Math.ceil(this.sizerRef.getBoundingClientRect().width)
+ this.sizerRef.style.removeProperty('display')
+
+ return searchWidth
+ }
+ }
+
+ computeTabIndex = () => {
+ const { disabled, search, tabIndex } = this.props
+
+ if (!_.isNil(tabIndex)) return tabIndex
+ // don't set a root node tabIndex as the search input has its own tabIndex
+ if (!search) return disabled ? -1 : 0
+ }
+
// ----------------------------------------
// Behavior
// ----------------------------------------
@@ -1052,38 +1098,17 @@ export default class Dropdown extends Component {
}
renderSearchInput = () => {
- const { disabled, search, tabIndex } = this.props
+ const { search, searchInput } = this.props
const { searchQuery } = this.state
if (!search) return null
-
- // tabIndex
- let computedTabIndex
- if (!_.isNil(tabIndex)) computedTabIndex = tabIndex
- else computedTabIndex = disabled ? -1 : 0
-
- // resize the search input, temporarily show the sizer so we can measure it
- let searchWidth
- if (this.sizerRef && searchQuery) {
- this.sizerRef.style.display = 'inline'
- this.sizerRef.textContent = searchQuery
- searchWidth = Math.ceil(this.sizerRef.getBoundingClientRect().width)
- this.sizerRef.style.display = 'none'
- }
-
- return (
-
- )
+ return DropdownSearchInput.create(searchInput, { defaultProps: {
+ inputRef: this.handleSearchRef,
+ onChange: this.handleSearchChange,
+ style: { width: this.computeSearchInputWidth() },
+ tabIndex: this.computeSearchInputTabIndex(),
+ value: searchQuery,
+ } })
}
renderSearchSizer = () => {
@@ -1172,7 +1197,6 @@ export default class Dropdown extends Component {
debug('render()')
debug('props', this.props)
debug('state', this.state)
- const { open } = this.state
const {
basic,
@@ -1194,10 +1218,10 @@ export default class Dropdown extends Component {
selection,
scrolling,
simple,
- tabIndex,
trigger,
upward,
} = this.props
+ const { open } = this.state
// Classes
const classes = cx(
@@ -1227,21 +1251,13 @@ export default class Dropdown extends Component {
useKeyOnly(upward, 'upward'),
useKeyOrValueAndKey(pointing, 'pointing'),
- className,
'dropdown',
+ className,
)
const rest = getUnhandledProps(Dropdown, this.props)
const ElementType = getElementType(Dropdown, this.props)
const ariaOptions = this.getDropdownAriaOptions(ElementType, this.props)
- let computedTabIndex
- if (!_.isNil(tabIndex)) {
- computedTabIndex = tabIndex
- } else if (!search) {
- // don't set a root node tabIndex as the search input has its own tabIndex
- computedTabIndex = disabled ? -1 : 0
- }
-
return (
{this.renderLabels()}
diff --git a/src/modules/Dropdown/DropdownSearchInput.d.ts b/src/modules/Dropdown/DropdownSearchInput.d.ts
new file mode 100644
index 0000000000..bf1191e65a
--- /dev/null
+++ b/src/modules/Dropdown/DropdownSearchInput.d.ts
@@ -0,0 +1,27 @@
+import * as React from 'react';
+
+export interface DropdownSearchInputProps {
+ [key: string]: any;
+
+ /** An element type to render as (string or function). */
+ as?: any;
+
+ /** Additional classes. */
+ className?: string;
+
+ /** A ref handler for input. */
+ inputRef?: (c: HTMLInputElement) => void;
+
+ /** An input can receive focus. */
+ tabIndex?: number | string;
+
+ /** The HTML input type. */
+ type?: string;
+
+ /** Stored value. */
+ value?: number | string;
+}
+
+declare const DropdownSearchInput: React.ComponentClass;
+
+export default DropdownSearchInput;
diff --git a/src/modules/Dropdown/DropdownSearchInput.js b/src/modules/Dropdown/DropdownSearchInput.js
new file mode 100644
index 0000000000..d44bc5aac7
--- /dev/null
+++ b/src/modules/Dropdown/DropdownSearchInput.js
@@ -0,0 +1,84 @@
+import cx from 'classnames'
+import _ from 'lodash'
+import PropTypes from 'prop-types'
+import React, { Component } from 'react'
+
+import {
+ createShorthandFactory,
+ customPropTypes,
+ META,
+ getUnhandledProps,
+} from '../../lib'
+
+/**
+ * A search item sub-component for Dropdown component.
+ */
+class DropdownSearchInput extends Component {
+ static propTypes = {
+ /** An element type to render as (string or function). */
+ as: customPropTypes.as,
+
+ /** Additional classes. */
+ className: PropTypes.string,
+
+ /** A ref handler for input. */
+ inputRef: PropTypes.func,
+
+ /** An input can receive focus. */
+ tabIndex: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string,
+ ]),
+
+ /** The HTML input type. */
+ type: PropTypes.string,
+
+ /** Stored value. */
+ value: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.string,
+ ]),
+ }
+
+ static defaultProps = {
+ type: 'text',
+ }
+
+ static _meta = {
+ name: 'DropdownSearchInput',
+ parent: 'Dropdown',
+ type: META.TYPES.MODULE,
+ }
+
+ handleChange = e => {
+ const value = _.get(e, 'target.value')
+
+ _.invoke(this.props, 'onChange', e, { ...this.props, value })
+ }
+
+ handleRef = c => _.invoke(this.props, 'inputRef', c)
+
+ render() {
+ const { className, tabIndex, type, value } = this.props
+ const classes = cx('search', className)
+ const rest = getUnhandledProps(DropdownSearchInput, this.props)
+
+ return (
+
+ )
+ }
+}
+
+DropdownSearchInput.create = createShorthandFactory(DropdownSearchInput, type => ({ type }))
+
+export default DropdownSearchInput
diff --git a/test/specs/modules/Dropdown/Dropdown-test.js b/test/specs/modules/Dropdown/Dropdown-test.js
index ca5bcff11e..b4e6915a6b 100644
--- a/test/specs/modules/Dropdown/Dropdown-test.js
+++ b/test/specs/modules/Dropdown/Dropdown-test.js
@@ -9,6 +9,7 @@ import DropdownDivider from 'src/modules/Dropdown/DropdownDivider'
import DropdownHeader from 'src/modules/Dropdown/DropdownHeader'
import DropdownItem from 'src/modules/Dropdown/DropdownItem'
import DropdownMenu from 'src/modules/Dropdown/DropdownMenu'
+import DropdownSearchInput from 'src/modules/Dropdown/DropdownSearchInput'
let attachTo
let options
@@ -70,7 +71,7 @@ describe('Dropdown', () => {
common.isConformant(Dropdown)
common.hasUIClassName(Dropdown)
- common.hasSubComponents(Dropdown, [DropdownDivider, DropdownHeader, DropdownItem, DropdownMenu])
+ common.hasSubComponents(Dropdown, [DropdownDivider, DropdownHeader, DropdownItem, DropdownMenu, DropdownSearchInput])
common.implementsIconProp(Dropdown)
common.implementsShorthandProp(Dropdown, {
propKey: 'header',
@@ -186,22 +187,22 @@ describe('Dropdown', () => {
})
it('defaults the search input to 0', () => {
wrapperShallow()
- .find('input.search')
+ .find(DropdownSearchInput)
.should.have.prop('tabIndex', 0)
})
it('defaults the disabled search input to -1', () => {
wrapperShallow()
- .find('input.search')
+ .find(DropdownSearchInput)
.should.have.prop('tabIndex', -1)
})
it('allows explicitly setting the search input value', () => {
wrapperShallow()
- .find('input.search')
+ .find(DropdownSearchInput)
.should.have.prop('tabIndex', 123)
})
it('allows explicitly setting the search input value when disabled', () => {
wrapperShallow()
- .find('input.search')
+ .find(DropdownSearchInput)
.should.have.prop('tabIndex', 123)
})
})
@@ -1623,7 +1624,7 @@ describe('Dropdown', () => {
it('adds a search input when present', () => {
wrapperShallow()
- .should.have.exactly(1).descendants('input.search')
+ .should.have.exactly(1).descendants(DropdownSearchInput)
})
it('sets focus to the search input on open', () => {
diff --git a/test/specs/modules/Dropdown/DropdownSearchInput-test.js b/test/specs/modules/Dropdown/DropdownSearchInput-test.js
new file mode 100644
index 0000000000..56e939f8ed
--- /dev/null
+++ b/test/specs/modules/Dropdown/DropdownSearchInput-test.js
@@ -0,0 +1,96 @@
+import faker from 'faker'
+import React from 'react'
+
+import * as common from 'test/specs/commonTests'
+import { sandbox } from 'test/utils'
+import DropdownSearchInput from 'src/modules/Dropdown/DropdownSearchInput'
+
+describe('DropdownSearchInput', () => {
+ common.hasValidTypings(DropdownSearchInput)
+ common.rendersChildren(DropdownSearchInput)
+
+ describe('aria', () => {
+ it('should have aria-autocomplete', () => {
+ shallow()
+ .should.have.prop('aria-autocomplete', 'list')
+ })
+ })
+
+ describe('autoComplete', () => {
+ it('should have autoComplete', () => {
+ shallow()
+ .should.have.prop('autoComplete', 'off')
+ })
+ })
+
+ describe('onChange', () => {
+ it('is called with (e, data) on change', () => {
+ const onChange = sandbox.spy()
+ const e = { target: { value: 'value' } }
+
+ shallow()
+ .find('input')
+ .simulate('change', e)
+
+ onChange.should.have.been.calledOnce()
+ onChange.should.have.been.calledWithMatch(e, { value: e.target.value })
+ })
+ })
+
+ describe('inputRef', () => {
+ it('maintains ref on input', () => {
+ const inputRef = sandbox.spy()
+ const mountNode = document.createElement('div')
+ document.body.appendChild(mountNode)
+
+ const wrapper = mount(, { attachTo: mountNode })
+ const input = document.querySelector('input')
+
+ inputRef.should.have.been.calledOnce()
+ inputRef.should.have.been.calledWithMatch(input)
+
+ wrapper.detach()
+ document.body.removeChild(mountNode)
+ })
+ })
+
+ describe('tabIndex', () => {
+ it('is not set by default', () => {
+ shallow()
+ .should.not.have.prop('tabIndex')
+ })
+
+ it('can be set explicitly', () => {
+ shallow()
+ .should.have.prop('tabIndex', 123)
+ })
+ })
+
+ describe('type', () => {
+ it('should have text by default', () => {
+ shallow()
+ .should.have.prop('type', 'text')
+ })
+
+ it('can be set explicitly', () => {
+ const type = faker.random.word()
+
+ shallow()
+ .should.have.prop('type', type)
+ })
+ })
+
+ describe('value', () => {
+ it('is not set by default', () => {
+ shallow()
+ .should.not.have.prop('value')
+ })
+
+ it('can be set explicitly', () => {
+ const value = faker.random.word()
+
+ shallow()
+ .should.have.prop('value', value)
+ })
+ })
+})