Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Dropdown): add DropdownSearchInput component #1619

Merged
merged 14 commits into from Jul 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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
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
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
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
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
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
@@ -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
@@ -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