Skip to content

Commit

Permalink
feat(Dropdown): add DropdownSearchInput component (#1619)
Browse files Browse the repository at this point in the history
* feat(Dropdown): add DropdownSearchInput component

* feat(Dropdown): add DropdownSearchInput component

* feat(Dropdown): add DropdownSearchInput component

* docs(Dropdown): add DropdownSearchInput example

* style(typings): fix lint issue

* style(Dropdown): style updates

* style(Dropdown): remove style and width props

* fix(Dropdown): update after merge

* fix(Dropdown): update after merge
  • Loading branch information
layershifter authored and levithomason committed Jul 7, 2017
1 parent 7bcf191 commit e6cdac6
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 50 deletions.
Original file line number Diff line number Diff line change
@@ -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 = () => (
<Dropdown
search
searchInput={{ type: 'number' }}
selection
options={options}
placeholder='Select amount...'
/>
)

export default DropdownExampleSearchInput
5 changes: 5 additions & 0 deletions docs/app/Examples/modules/Dropdown/Usage/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ const DropdownUsageExamples = () => (
description='A dropdown item can be rendered differently inside the menu.'
examplePath='modules/Dropdown/Usage/DropdownExampleItemContent'
/>
<ComponentExample
title='Search Input'
description='A dropdown implements a search input shorthand.'
examplePath='modules/Dropdown/Usage/DropdownExampleSearchInput'
/>
<ComponentExample
title='Upward'
description='A dropdown can open its menu upward.'
Expand Down
4 changes: 4 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ export { default as DropdownDivider, DropdownDividerProps } from './dist/commonj
export { default as DropdownHeader, DropdownHeaderProps } from './dist/commonjs/modules/Dropdown/DropdownHeader';
export { default as DropdownItem, DropdownItemProps } from './dist/commonjs/modules/Dropdown/DropdownItem';
export { default as DropdownMenu, DropdownMenuProps } from './dist/commonjs/modules/Dropdown/DropdownMenu';
export {
default as DropdownSearchInput,
DropdownSearchInputProps
} from './dist/commonjs/modules/Dropdown/DropdownSearchInput';

export { default as Embed, EmbedProps } from './dist/commonjs/modules/Embed';

Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export { default as DropdownDivider } from './modules/Dropdown/DropdownDivider'
export { default as DropdownHeader } from './modules/Dropdown/DropdownHeader'
export { default as DropdownItem } from './modules/Dropdown/DropdownItem'
export { default as DropdownMenu } from './modules/Dropdown/DropdownMenu'
export { default as DropdownSearchInput } from './modules/Dropdown/DropdownSearchInput'

export { default as Embed } from './modules/Embed'

Expand Down
5 changes: 5 additions & 0 deletions src/modules/Dropdown/Dropdown.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { default as DropdownDivider } from './DropdownDivider';
import { default as DropdownHeader } from './DropdownHeader';
import { default as DropdownItem, DropdownItemProps } from './DropdownItem';
import { default as DropdownMenu } from './DropdownMenu';
import { default as DropdownSearchInput } from './DropdownSearchInput';

export interface DropdownProps {
[key: string]: any;
Expand Down Expand Up @@ -212,6 +213,9 @@ export interface DropdownProps {
*/
search?: boolean | ((options: Array<DropdownItemProps>, value: string) => Array<DropdownItemProps>);

/** A shorthand for a search input. */
searchInput?: any;

/** Define whether the highlighted item should be selected on blur. */
selectOnBlur?: boolean;

Expand Down Expand Up @@ -245,6 +249,7 @@ interface DropdownComponent extends React.ComponentClass<DropdownProps> {
Header: typeof DropdownHeader;
Item: typeof DropdownItem;
Menu: typeof DropdownMenu;
SearchInput: typeof DropdownSearchInput;
}

declare const Dropdown: DropdownComponent;
Expand Down
104 changes: 60 additions & 44 deletions src/modules/Dropdown/Dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -338,6 +346,7 @@ export default class Dropdown extends Component {
noResultsMessage: 'No results found.',
openOnFocus: true,
renderLabel: ({ text }) => text,
searchInput: 'text',
selectOnBlur: true,
}

Expand All @@ -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()')
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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
// ----------------------------------------
Expand Down Expand Up @@ -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 (
<input
value={searchQuery}
type='text'
aria-autocomplete='list'
onChange={this.handleSearchChange}
className='search'
autoComplete='off'
tabIndex={computedTabIndex}
style={{ width: searchWidth }}
ref={this.handleSearchRef}
/>
)
return DropdownSearchInput.create(searchInput, { defaultProps: {
inputRef: this.handleSearchRef,
onChange: this.handleSearchChange,
style: { width: this.computeSearchInputWidth() },
tabIndex: this.computeSearchInputTabIndex(),
value: searchQuery,
} })
}

renderSearchSizer = () => {
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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 (
<ElementType
{...rest}
Expand All @@ -1252,7 +1268,7 @@ export default class Dropdown extends Component {
onMouseDown={this.handleMouseDown}
onFocus={this.handleFocus}
onChange={this.handleChange}
tabIndex={computedTabIndex}
tabIndex={this.computeTabIndex()}
ref={this.handleRef}
>
{this.renderLabels()}
Expand Down
27 changes: 27 additions & 0 deletions src/modules/Dropdown/DropdownSearchInput.d.ts
Original file line number Diff line number Diff line change
@@ -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<DropdownSearchInputProps>;

export default DropdownSearchInput;
84 changes: 84 additions & 0 deletions src/modules/Dropdown/DropdownSearchInput.js
Original file line number Diff line number Diff line change
@@ -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 (
<input
{...rest}
aria-autocomplete='list'
autoComplete='off'
className={classes}
onChange={this.handleChange}
ref={this.handleRef}
tabIndex={tabIndex}
type={type}
value={value}
/>
)
}
}

DropdownSearchInput.create = createShorthandFactory(DropdownSearchInput, type => ({ type }))

export default DropdownSearchInput
Loading

0 comments on commit e6cdac6

Please sign in to comment.