` elements. Group only requires you provide a label via its `renderLabel` prop. Groups and their associated options also accept icons or other stylistic additions if needed.
+
+- ```javascript
+ class GroupSelectExample extends React.Component {
+ state = {
+ inputValue: this.props.options['Western'][0].label,
+ isShowingOptions: false,
+ highlightedOptionId: null,
+ selectedOptionId: this.props.options['Western'][0].id,
+ announcement: null
+ }
+
+ getOptionById(id) {
+ const { options } = this.props
+ let match = null
+ Object.keys(options).forEach((key, index) => {
+ for (let i = 0; i < options[key].length; i++) {
+ const option = options[key][i]
+ if (id === option.id) {
+ // return group property with the object just to make it easier
+ // to check which group the option belongs to
+ match = { ...option, group: key }
+ break
+ }
+ }
+ })
+ return match
+ }
+
+ getGroupChangedMessage(newOption) {
+ const currentOption = this.getOptionById(this.state.highlightedOptionId)
+ const isNewGroup =
+ !currentOption || currentOption.group !== newOption.group
+ let message = isNewGroup ? `Group ${newOption.group} entered. ` : ''
+ message += newOption.label
+ return message
+ }
+
+ handleShowOptions = (event) => {
+ this.setState({
+ isShowingOptions: true,
+ highlightedOptionId: null
+ })
+ }
+
+ handleHideOptions = (event) => {
+ const { selectedOptionId } = this.state
+ this.setState({
+ isShowingOptions: false,
+ highlightedOptionId: null,
+ inputValue: this.getOptionById(selectedOptionId).label
+ })
+ }
+
+ handleBlur = (event) => {
+ this.setState({
+ highlightedOptionId: null
+ })
+ }
+
+ handleHighlightOption = (event, { id }) => {
+ event.persist()
+ const newOption = this.getOptionById(id)
+ this.setState((state) => ({
+ highlightedOptionId: id,
+ inputValue:
+ event.type === 'keydown' ? newOption.label : state.inputValue,
+ announcement: this.getGroupChangedMessage(newOption)
+ }))
+ }
+
+ handleSelectOption = (event, { id }) => {
+ this.setState({
+ selectedOptionId: id,
+ inputValue: this.getOptionById(id).label,
+ isShowingOptions: false,
+ announcement: `${this.getOptionById(id).label} selected.`
+ })
+ }
+
+ renderLabel(text, variant) {
+ return (
+
+
+ {text}
+
)
- })
+ }
+
+ renderGroup() {
+ const { options } = this.props
+ const { highlightedOptionId, selectedOptionId } = this.state
+
+ return Object.keys(options).map((key, index) => {
+ const badgeVariant = key === 'Eastern' ? 'success' : 'primary'
+ return (
+
+ {options[key].map((option) => (
+
+ {option.label}
+
+ ))}
+
+ )
+ })
+ }
+
+ render() {
+ const {
+ inputValue,
+ isShowingOptions,
+ highlightedOptionId,
+ selectedOptionId,
+ filteredOptions,
+ announcement
+ } = this.state
+
+ return (
+
+
+ }
+ >
+ {this.renderGroup()}
+
+
document.getElementById('flash-messages')}
+ liveRegionPoliteness="assertive"
+ screenReaderOnly
+ >
+ {announcement}
+
+
+ )
+ }
}
- render () {
- const {
- inputValue,
- isShowingOptions,
- highlightedOptionId,
- selectedOptionId,
- filteredOptions,
- announcement
- } = this.state
+ render(
+
+
+
+ )
+ ```
+
+- ```js
+ const GroupSelectExample = ({ options }) => {
+ const [inputValue, setInputValue] = useState(options['Western'][0].label)
+ const [isShowingOptions, setIsShowingOptions] = useState(false)
+ const [highlightedOptionId, setHighlightedOptionId] = useState(null)
+ const [selectedOptionId, setSelectedOptionId] = useState(
+ options['Western'][0].id
+ )
+ const [announcement, setAnnouncement] = useState(null)
+
+ const getOptionById = (id) => {
+ let match = null
+ Object.keys(options).forEach((key, index) => {
+ for (let i = 0; i < options[key].length; i++) {
+ const option = options[key][i]
+ if (id === option.id) {
+ // return group property with the object just to make it easier
+ // to check which group the option belongs to
+ match = { ...option, group: key }
+ break
+ }
+ }
+ })
+ return match
+ }
+
+ const getGroupChangedMessage = (newOption) => {
+ const currentOption = getOptionById(highlightedOptionId)
+ const isNewGroup =
+ !currentOption || currentOption.group !== newOption.group
+ let message = isNewGroup ? `Group ${newOption.group} entered. ` : ''
+ message += newOption.label
+ return message
+ }
+
+ const handleShowOptions = (event) => {
+ setIsShowingOptions(true)
+ setHighlightedOptionId(null)
+ }
+
+ const handleHideOptions = (event) => {
+ setIsShowingOptions(false)
+ setHighlightedOptionId(null)
+ setInputValue(getOptionById(selectedOptionId).label)
+ }
+
+ const handleBlur = (event) => {
+ setHighlightedOptionId(null)
+ }
+
+ const handleHighlightOption = (event, { id }) => {
+ event.persist()
+ const newOption = getOptionById(id)
+ setHighlightedOptionId(id)
+ setInputValue(event.type === 'keydown' ? newOption.label : inputValue)
+ setAnnouncement(getGroupChangedMessage(newOption))
+ }
+
+ const handleSelectOption = (event, { id }) => {
+ setSelectedOptionId(id)
+ setInputValue(getOptionById(id).label)
+ setIsShowingOptions(false)
+ setAnnouncement(`${getOptionById(id).label} selected.`)
+ }
+
+ const renderLabel = (text, variant) => {
+ return (
+
+
+ {text}
+
+ )
+ }
+
+ const renderGroup = () => {
+ return Object.keys(options).map((key, index) => {
+ const badgeVariant = key === 'Eastern' ? 'success' : 'primary'
+ return (
+
+ {options[key].map((option) => (
+
+ {option.label}
+
+ ))}
+
+ )
+ })
+ }
return (
@@ -779,196 +1455,342 @@ class GroupSelectExample extends React.Component {
assistiveText="Type or use arrow keys to navigate options."
inputValue={inputValue}
isShowingOptions={isShowingOptions}
- onBlur={this.handleBlur}
- onRequestShowOptions={this.handleShowOptions}
- onRequestHideOptions={this.handleHideOptions}
- onRequestHighlightOption={this.handleHighlightOption}
- onRequestSelectOption={this.handleSelectOption}
+ onBlur={handleBlur}
+ onRequestShowOptions={handleShowOptions}
+ onRequestHideOptions={handleHideOptions}
+ onRequestHighlightOption={handleHighlightOption}
+ onRequestSelectOption={handleSelectOption}
renderBeforeInput={
}
>
- {this.renderGroup()}
+ {renderGroup()}
document.getElementById('flash-messages')}
liveRegionPoliteness="assertive"
screenReaderOnly
>
- { announcement }
+ {announcement}
)
}
-}
-
-render(
-
-
-
-)
-```
+
+ render(
+
+
+
+ )
+ ```
##### Using groups with autocomplete on Safari
Due to a WebKit bug if you are using `Select.Group` with autocomplete, the screenreader won't announce highlight/selection changes. This only seems to be an issue in Safari. Here is an example how you can work around that:
-```javascript
----
-type: example
----
+- ```javascript
+ class GroupSelectAutocompleteExample extends React.Component {
+ state = {
+ inputValue: '',
+ isShowingOptions: false,
+ highlightedOptionId: null,
+ selectedOptionId: null,
+ filteredOptions: this.props.options,
+ announcement: null
+ }
-class GroupSelectAutocompleteExample extends React.Component {
- state = {
- inputValue: '',
- isShowingOptions: false,
- highlightedOptionId: null,
- selectedOptionId: null,
- filteredOptions: this.props.options,
- announcement: null
- }
+ getOptionById(id) {
+ const options = this.props.options
+ return Object.values(options)
+ .flat()
+ .find((o) => o?.id === id)
+ }
- getOptionById (id) {
- const options = this.props.options
- return Object.values(options)
- .flat()
- .find((o) => o?.id === id);
- }
+ filterOptions(value, options) {
+ const filteredOptions = {}
+ Object.keys(options).forEach((key) => {
+ filteredOptions[key] = options[key]?.filter((option) =>
+ option.label.toLowerCase().includes(value.toLowerCase())
+ )
+ })
+ const optionsWithoutEmptyKeys = Object.keys(filteredOptions)
+ .filter((k) => filteredOptions[k].length > 0)
+ .reduce((a, k) => ({ ...a, [k]: filteredOptions[k] }), {})
+ return optionsWithoutEmptyKeys
+ }
- filterOptions (value, options) {
- const filteredOptions = {};
- Object.keys(options).forEach((key) => {
- filteredOptions[key] = options[key]?.filter(
- (option) =>
- option.label.toLowerCase().includes(value.toLowerCase())
- );
- });
- const optionsWithoutEmptyKeys = Object.keys(filteredOptions)
- .filter((k) => filteredOptions[k].length > 0)
- .reduce((a, k) => ({ ...a, [k]: filteredOptions[k] }), {});
- return optionsWithoutEmptyKeys;
- };
-
- handleShowOptions = (event) => {
- this.setState({
- isShowingOptions: true,
- highlightedOptionId: null
- })
- }
+ handleShowOptions = (event) => {
+ this.setState({
+ isShowingOptions: true,
+ highlightedOptionId: null
+ })
+ }
- handleHideOptions = (event) => {
- const { selectedOptionId } = this.state
- this.setState({
- isShowingOptions: false,
- highlightedOptionId: null
- })
- }
+ handleHideOptions = (event) => {
+ const { selectedOptionId } = this.state
+ this.setState({
+ isShowingOptions: false,
+ highlightedOptionId: null
+ })
+ }
- handleBlur = (event) => {
- this.setState({
- highlightedOptionId: null
- })
- }
+ handleBlur = (event) => {
+ this.setState({
+ highlightedOptionId: null
+ })
+ }
- handleHighlightOption = (event, { id }) => {
- event.persist()
- const option = this.getOptionById(id)
- setTimeout(() => {
+ handleHighlightOption = (event, { id }) => {
+ event.persist()
+ const option = this.getOptionById(id)
+ setTimeout(() => {
+ this.setState((state) => ({
+ announcement: option.label
+ }))
+ }, 0)
this.setState((state) => ({
+ highlightedOptionId: id
+ }))
+ }
+
+ handleSelectOption = (event, { id }) => {
+ const option = this.getOptionById(id)
+ if (!option) return // prevent selecting of empty option
+ this.setState({
+ selectedOptionId: id,
+ inputValue: option.label,
+ isShowingOptions: false,
+ filteredOptions: this.props.options,
announcement: option.label
+ })
+ }
+
+ handleInputChange = (event) => {
+ const value = event.target.value
+ const newOptions = this.filterOptions(value, this.props.options)
+ this.setState((state) => ({
+ inputValue: value,
+ filteredOptions: newOptions,
+ highlightedOptionId: newOptions.length > 0 ? newOptions[0].id : null,
+ isShowingOptions: true,
+ selectedOptionId: value === '' ? null : state.selectedOptionId
}))
- }, 0)
- this.setState((state) => ({
- highlightedOptionId: id,
- }))
- }
+ }
- handleSelectOption = (event, { id }) => {
- const option = this.getOptionById(id)
- if (!option) return // prevent selecting of empty option
- this.setState({
- selectedOptionId: id,
- inputValue: option.label,
- isShowingOptions: false,
- filteredOptions: this.props.options,
- announcement: option.label
- })
- }
+ renderGroup() {
+ const filteredOptions = this.state.filteredOptions
+ const { highlightedOptionId, selectedOptionId } = this.state
- handleInputChange = (event) => {
- const value = event.target.value
- const newOptions = this.filterOptions(value, this.props.options)
- this.setState((state) => ({
- inputValue: value,
- filteredOptions: newOptions,
- highlightedOptionId: newOptions.length > 0 ? newOptions[0].id : null,
- isShowingOptions: true,
- selectedOptionId: value === '' ? null : state.selectedOptionId,
- }))
- }
+ return Object.keys(filteredOptions).map((key, index) => {
+ return (
+
+ {filteredOptions[key].map((option) => (
+
+ {option.label}
+
+ ))}
+
+ )
+ })
+ }
- renderGroup () {
- const filteredOptions = this.state.filteredOptions
- const { highlightedOptionId, selectedOptionId } = this.state
+ renderScreenReaderHelper() {
+ const announcement = this.state.announcement
+ return (
+ window.safari && (
+
+
+ {announcement}
+
+
+ )
+ )
+ }
+
+ render() {
+ const {
+ inputValue,
+ isShowingOptions,
+ highlightedOptionId,
+ selectedOptionId,
+ filteredOptions
+ } = this.state
- return Object.keys(filteredOptions).map((key, index) => {
return (
-
- {filteredOptions[key].map((option) => (
-
- { option.label }
-
- ))}
-
+
+
+ {this.renderGroup()}
+
+ {this.renderScreenReaderHelper()}
+
)
- })
+ }
}
- renderScreenReaderHelper () {
- const announcement = this.state.announcement
- return window.safari && (
-
- {announcement}
-
- )
- }
+ render(
+
+
+
+ )
+ ```
+
+- ```js
+ const GroupSelectAutocompleteExample = ({ options }) => {
+ const [inputValue, setInputValue] = useState('')
+ const [isShowingOptions, setIsShowingOptions] = useState(false)
+ const [highlightedOptionId, setHighlightedOptionId] = useState(null)
+ const [selectedOptionId, setSelectedOptionId] = useState(null)
+ const [filteredOptions, setFilteredOptions] = useState(options)
+ const [announcement, setAnnouncement] = useState(null)
+
+ const getOptionById = (id) => {
+ return Object.values(options)
+ .flat()
+ .find((o) => o?.id === id)
+ }
- render () {
- const {
- inputValue,
- isShowingOptions,
- highlightedOptionId,
- selectedOptionId,
- filteredOptions
- } = this.state
+ const filterOptions = (value, options) => {
+ const filteredOptions = {}
+ Object.keys(options).forEach((key) => {
+ filteredOptions[key] = options[key]?.filter((option) =>
+ option.label.toLowerCase().includes(value.toLowerCase())
+ )
+ })
+ const optionsWithoutEmptyKeys = Object.keys(filteredOptions)
+ .filter((k) => filteredOptions[k].length > 0)
+ .reduce((a, k) => ({ ...a, [k]: filteredOptions[k] }), {})
+ return optionsWithoutEmptyKeys
+ }
+
+ const handleShowOptions = (event) => {
+ setIsShowingOptions(true)
+ setHighlightedOptionId(null)
+ }
+
+ const handleHideOptions = (event) => {
+ setIsShowingOptions(false)
+ setHighlightedOptionId(null)
+ }
+
+ const handleBlur = (event) => {
+ setHighlightedOptionId(null)
+ }
+
+ const handleHighlightOption = (event, { id }) => {
+ event.persist()
+ const option = getOptionById(id)
+ setTimeout(() => {
+ setAnnouncement(option.label)
+ }, 0)
+ setHighlightedOptionId(id)
+ }
+
+ const handleSelectOption = (event, { id }) => {
+ const option = getOptionById(id)
+ if (!option) return // prevent selecting of empty option
+ setSelectedOptionId(id)
+ setInputValue(option.label)
+ setIsShowingOptions(false)
+ setFilteredOptions(options)
+ setAnnouncement(option.label)
+ }
+
+ const handleInputChange = (event) => {
+ const value = event.target.value
+ const newOptions = filterOptions(value, options)
+ setInputValue(value)
+ setFilteredOptions(newOptions)
+ setHighlightedOptionId(newOptions.length > 0 ? newOptions[0].id : null)
+ setIsShowingOptions(true)
+ setSelectedOptionId(value === '' ? null : selectedOptionId)
+ }
+
+ const renderGroup = () => {
+ return Object.keys(filteredOptions).map((key, index) => {
+ return (
+
+ {filteredOptions[key].map((option) => (
+
+ {option.label}
+
+ ))}
+
+ )
+ })
+ }
+
+ const renderScreenReaderHelper = () => {
+ return (
+ window.safari && (
+
+
+ {announcement}
+
+
+ )
+ )
+ }
return (
@@ -978,193 +1800,379 @@ class GroupSelectAutocompleteExample extends React.Component {
assistiveText="Type or use arrow keys to navigate options."
inputValue={inputValue}
isShowingOptions={isShowingOptions}
- onBlur={this.handleBlur}
- onInputChange={this.handleInputChange}
- onRequestShowOptions={this.handleShowOptions}
- onRequestHideOptions={this.handleHideOptions}
- onRequestHighlightOption={this.handleHighlightOption}
- onRequestSelectOption={this.handleSelectOption}
+ onBlur={handleBlur}
+ onInputChange={handleInputChange}
+ onRequestShowOptions={handleShowOptions}
+ onRequestHideOptions={handleHideOptions}
+ onRequestHighlightOption={handleHighlightOption}
+ onRequestSelectOption={handleSelectOption}
>
- {this.renderGroup()}
+ {renderGroup()}
- {this.renderScreenReaderHelper()}
+ {renderScreenReaderHelper()}
)
}
-}
-
-render(
-
-
-
-)
-```
+
+ render(
+
+
+
+ )
+ ```
#### Asynchronous option loading
If no results match the user's search, it's recommended to leave `isShowingOptions` as `true` and to display an "empty option" as a way of communicating that there are no matches. Similarly, it's helpful to display a [Spinner](#Spinner) in an empty option while options load.
-```javascript
----
-type: example
----
-
-class AsyncExample extends React.Component {
- state = {
- inputValue: '',
- isShowingOptions: false,
- isLoading: false,
- highlightedOptionId: null,
- selectedOptionId: null,
- selectedOptionLabel: '',
- filteredOptions: [],
- announcement: null
- }
+- ```javascript
+ class AsyncExample extends React.Component {
+ state = {
+ inputValue: '',
+ isShowingOptions: false,
+ isLoading: false,
+ highlightedOptionId: null,
+ selectedOptionId: null,
+ selectedOptionLabel: '',
+ filteredOptions: [],
+ announcement: null
+ }
- timeoutId = null
+ timeoutId = null
- getOptionById (queryId) {
- return this.state.filteredOptions.find(({ id }) => id === queryId)
- }
+ getOptionById(queryId) {
+ return this.state.filteredOptions.find(({ id }) => id === queryId)
+ }
- filterOptions = (value) => {
- return this.props.options.filter(option => (
- option.label.toLowerCase().startsWith(value.toLowerCase())
- ))
- }
+ filterOptions = (value) => {
+ return this.props.options.filter((option) =>
+ option.label.toLowerCase().startsWith(value.toLowerCase())
+ )
+ }
- matchValue () {
- const {
- filteredOptions,
- inputValue,
- selectedOptionId,
- selectedOptionLabel
- } = this.state
-
- // an option matching user input exists
- if (filteredOptions.length === 1) {
- const onlyOption = filteredOptions[0]
- // automatically select the matching option
- if (onlyOption.label.toLowerCase() === inputValue.toLowerCase()) {
- return {
- inputValue: onlyOption.label,
- selectedOptionId: onlyOption.id
+ matchValue() {
+ const {
+ filteredOptions,
+ inputValue,
+ selectedOptionId,
+ selectedOptionLabel
+ } = this.state
+
+ // an option matching user input exists
+ if (filteredOptions.length === 1) {
+ const onlyOption = filteredOptions[0]
+ // automatically select the matching option
+ if (onlyOption.label.toLowerCase() === inputValue.toLowerCase()) {
+ return {
+ inputValue: onlyOption.label,
+ selectedOptionId: onlyOption.id
+ }
}
}
+ // allow user to return to empty input and no selection
+ if (inputValue.length === 0) {
+ return { selectedOptionId: null, filteredOptions: [] }
+ }
+ // no match found, return selected option label to input
+ if (selectedOptionId) {
+ return { inputValue: selectedOptionLabel }
+ }
}
- // allow user to return to empty input and no selection
- if (inputValue.length === 0) {
- return { selectedOptionId: null, filteredOptions: [] }
- }
- // no match found, return selected option label to input
- if (selectedOptionId) {
- return { inputValue: selectedOptionLabel }
- }
- }
-
- handleShowOptions = (event) => {
- this.setState(({ filteredOptions }) => ({
- isShowingOptions: true
- }))
- }
-
- handleHideOptions = (event) => {
- const { selectedOptionId, inputValue } = this.state
- this.setState({
- isShowingOptions: false,
- highlightedOptionId: null,
- announcement: 'List collapsed.',
- ...this.matchValue()
- })
- }
- handleBlur = (event) => {
- this.setState({ highlightedOptionId: null })
- }
+ handleShowOptions = (event) => {
+ this.setState(({ filteredOptions }) => ({
+ isShowingOptions: true
+ }))
+ }
- handleHighlightOption = (event, { id }) => {
- event.persist()
- const option = this.getOptionById(id)
- if (!option) return // prevent highlighting of empty option
- this.setState((state) => ({
- highlightedOptionId: id,
- inputValue: event.type === 'keydown' ? option.label : state.inputValue,
- announcement: option.label
- }))
- }
+ handleHideOptions = (event) => {
+ const { selectedOptionId, inputValue } = this.state
+ this.setState({
+ isShowingOptions: false,
+ highlightedOptionId: null,
+ announcement: 'List collapsed.',
+ ...this.matchValue()
+ })
+ }
- handleSelectOption = (event, { id }) => {
- const option = this.getOptionById(id)
- if (!option) return // prevent selecting of empty option
- this.setState({
- selectedOptionId: id,
- selectedOptionLabel: option.label,
- inputValue: option.label,
- isShowingOptions: false,
- announcement: `${option.label} selected. List collapsed.`,
- filteredOptions: [this.getOptionById(id)]
- })
- }
+ handleBlur = (event) => {
+ this.setState({ highlightedOptionId: null })
+ }
- handleInputChange = (event) => {
- const value = event.target.value
- clearTimeout(this.timeoutId)
+ handleHighlightOption = (event, { id }) => {
+ event.persist()
+ const option = this.getOptionById(id)
+ if (!option) return // prevent highlighting of empty option
+ this.setState((state) => ({
+ highlightedOptionId: id,
+ inputValue: event.type === 'keydown' ? option.label : state.inputValue,
+ announcement: option.label
+ }))
+ }
- if (!value || value === '') {
+ handleSelectOption = (event, { id }) => {
+ const option = this.getOptionById(id)
+ if (!option) return // prevent selecting of empty option
this.setState({
- isLoading: false,
- inputValue: value,
- isShowingOptions: true,
- selectedOptionId: null,
- selectedOptionLabel: null,
- filteredOptions: [],
- })
- } else {
- this.setState({
- isLoading: true,
- inputValue: value,
- isShowingOptions: true,
- filteredOptions: [],
- highlightedOptionId: null,
- announcement: 'Loading options.'
+ selectedOptionId: id,
+ selectedOptionLabel: option.label,
+ inputValue: option.label,
+ isShowingOptions: false,
+ announcement: `${option.label} selected. List collapsed.`,
+ filteredOptions: [this.getOptionById(id)]
})
+ }
- this.timeoutId = setTimeout(() => {
- const newOptions = this.filterOptions(value)
+ handleInputChange = (event) => {
+ const value = event.target.value
+ clearTimeout(this.timeoutId)
+
+ if (!value || value === '') {
this.setState({
- filteredOptions: newOptions,
isLoading: false,
- announcement: `${newOptions.length} options available.`
+ inputValue: value,
+ isShowingOptions: true,
+ selectedOptionId: null,
+ selectedOptionLabel: null,
+ filteredOptions: []
})
- }, 1500)
+ } else {
+ this.setState({
+ isLoading: true,
+ inputValue: value,
+ isShowingOptions: true,
+ filteredOptions: [],
+ highlightedOptionId: null,
+ announcement: 'Loading options.'
+ })
+
+ this.timeoutId = setTimeout(() => {
+ const newOptions = this.filterOptions(value)
+ this.setState({
+ filteredOptions: newOptions,
+ isLoading: false,
+ announcement: `${newOptions.length} options available.`
+ })
+ }, 1500)
+ }
+ }
+
+ render() {
+ const {
+ inputValue,
+ isShowingOptions,
+ isLoading,
+ highlightedOptionId,
+ selectedOptionId,
+ filteredOptions,
+ announcement
+ } = this.state
+
+ return (
+
+
+ {filteredOptions.length > 0 ? (
+ filteredOptions.map((option) => {
+ return (
+
+ {option.label}
+
+ )
+ })
+ ) : (
+
+ {isLoading ? (
+
+ ) : inputValue !== '' ? (
+ 'No results'
+ ) : (
+ 'Type to search'
+ )}
+
+ )}
+
+
document.getElementById('flash-messages')}
+ liveRegionPoliteness="assertive"
+ screenReaderOnly
+ >
+ {announcement}
+
+
+ )
}
}
- render () {
- const {
- inputValue,
- isShowingOptions,
- isLoading,
- highlightedOptionId,
- selectedOptionId,
- filteredOptions,
- announcement
- } = this.state
+ render(
+
+
+
+ )
+ ```
+
+- ```js
+ const AsyncExample = ({ options }) => {
+ const [inputValue, setInputValue] = useState('')
+ const [isShowingOptions, setIsShowingOptions] = useState(false)
+ const [isLoading, setIsLoading] = useState(false)
+ const [highlightedOptionId, setHighlightedOptionId] = useState(null)
+ const [selectedOptionId, setSelectedOptionId] = useState(null)
+ const [selectedOptionLabel, setSelectedOptionLabel] = useState('')
+ const [filteredOptions, setFilteredOptions] = useState([])
+ const [announcement, setAnnouncement] = useState(null)
+
+ let timeoutId = null
+
+ const getOptionById = (queryId) => {
+ return filteredOptions.find(({ id }) => id === queryId)
+ }
+
+ const filterOptions = (value) => {
+ return options.filter((option) =>
+ option.label.toLowerCase().startsWith(value.toLowerCase())
+ )
+ }
+
+ const matchValue = () => {
+ // an option matching user input exists
+ if (filteredOptions.length === 1) {
+ const onlyOption = filteredOptions[0]
+ // automatically select the matching option
+ if (onlyOption.label.toLowerCase() === inputValue.toLowerCase()) {
+ setInputValue(onlyOption.label)
+ setSelectedOptionId(onlyOption.id)
+ return
+ }
+ }
+ // allow user to return to empty input and no selection
+ if (inputValue.length === 0) {
+ setSelectedOptionId(null)
+ setFilteredOptions([])
+ return
+ }
+ // no match found, return selected option label to input
+ if (selectedOptionId) {
+ setInputValue(selectedOptionLabel)
+ return
+ }
+ }
+
+ const handleShowOptions = (event) => {
+ setIsShowingOptions(true)
+ }
+
+ const handleHideOptions = (event) => {
+ setIsShowingOptions(false)
+ setHighlightedOptionId(null)
+ setAnnouncement('List collapsed.')
+ matchValue()
+ }
+
+ const handleBlur = (event) => {
+ setHighlightedOptionId(null)
+ }
+
+ const handleHighlightOption = (event, { id }) => {
+ event.persist()
+ const option = getOptionById(id)
+ if (!option) return // prevent highlighting of empty option
+
+ setHighlightedOptionId(id)
+ setInputValue(event.type === 'keydown' ? option.label : inputValue)
+ setAnnouncement(option.label)
+ }
+
+ const handleSelectOption = (event, { id }) => {
+ const option = getOptionById(id)
+ if (!option) return // prevent selecting of empty option
+ setSelectedOptionId(id)
+ setSelectedOptionLabel(option.label)
+ setInputValue(option.label)
+ setIsShowingOptions(false)
+ setAnnouncement(`${option.label} selected. List collapsed.`)
+ setFilteredOptions([getOptionById(id)])
+ }
+
+ const handleInputChange = (event) => {
+ const value = event.target.value
+ clearTimeout(timeoutId)
+
+ if (!value || value === '') {
+ setIsLoading(false)
+ setInputValue(value)
+ setIsShowingOptions(true)
+ setSelectedOptionId(null)
+ setSelectedOptionLabel(null)
+ setFilteredOptions([])
+ } else {
+ setIsLoading(true)
+ setInputValue(value)
+ setIsShowingOptions(true)
+ setFilteredOptions([])
+ setHighlightedOptionId(null)
+ setAnnouncement('Loading options.')
+
+ timeoutId = setTimeout(() => {
+ const newOptions = filterOptions(value)
+ setFilteredOptions(newOptions)
+ setIsLoading(false)
+ setAnnouncement(`${newOptions.length} options available.`)
+ }, 1500)
+ }
+ }
return (
@@ -1173,31 +2181,39 @@ class AsyncExample extends React.Component {
assistiveText="Type to search"
inputValue={inputValue}
isShowingOptions={isShowingOptions}
- onBlur={this.handleBlur}
- onInputChange={this.handleInputChange}
- onRequestShowOptions={this.handleShowOptions}
- onRequestHideOptions={this.handleHideOptions}
- onRequestHighlightOption={this.handleHighlightOption}
- onRequestSelectOption={this.handleSelectOption}
+ onBlur={handleBlur}
+ onInputChange={handleInputChange}
+ onRequestShowOptions={handleShowOptions}
+ onRequestHideOptions={handleHideOptions}
+ onRequestHighlightOption={handleHighlightOption}
+ onRequestSelectOption={handleSelectOption}
>
- {filteredOptions.length > 0 ? filteredOptions.map((option) => {
- return (
-
- {option.label}
-
- )
- }) : (
+ {filteredOptions.length > 0 ? (
+ filteredOptions.map((option) => {
+ return (
+
+ {option.label}
+
+ )
+ })
+ ) : (
- {isLoading
- ?
- : inputValue !== '' ? 'No results' : 'Type to search'}
+ {isLoading ? (
+
+ ) : inputValue !== '' ? (
+ 'No results'
+ ) : (
+ 'Type to search'
+ )}
)}
@@ -1206,112 +2222,228 @@ class AsyncExample extends React.Component {
liveRegionPoliteness="assertive"
screenReaderOnly
>
- { announcement }
+ {announcement}
)
}
-}
-
-render(
-
-
-
-)
-```
+
+ render(
+
+
+
+ )
+ ```
### Icons
To display icons (or other elements) before or after an option, pass it via the `renderBeforeLabel` and `renderAfterLabel` prop to `Select.Option`. You can pass a function as well, which will have a `props` parameter, so you can access the properties of that `Select.Option` (e.g. if it is currently `isHighlighted`). The available props are: `[ id, isDisabled, isSelected, isHighlighted, children ]`.
-```javascript
----
-type: example
----
-class SingleSelectExample extends React.Component {
- state = {
- inputValue: this.props.options[0].label,
- isShowingOptions: false,
- highlightedOptionId: null,
- selectedOptionId: this.props.options[0].id,
- announcement: null
- }
+- ```js
+ class SingleSelectExample extends React.Component {
+ state = {
+ inputValue: this.props.options[0].label,
+ isShowingOptions: false,
+ highlightedOptionId: null,
+ selectedOptionId: this.props.options[0].id,
+ announcement: null
+ }
- getOptionById (queryId) {
- return this.props.options.find(({ id }) => id === queryId)
- }
+ getOptionById(queryId) {
+ return this.props.options.find(({ id }) => id === queryId)
+ }
- handleShowOptions = (event) => {
- this.setState({
- isShowingOptions: true
- })
- }
+ handleShowOptions = (event) => {
+ this.setState({
+ isShowingOptions: true
+ })
+ }
- handleHideOptions = (event) => {
- const { selectedOptionId } = this.state
- const option = this.getOptionById(selectedOptionId).label
- this.setState({
- isShowingOptions: false,
- highlightedOptionId: null,
- inputValue: selectedOptionId ? option : '',
- announcement: 'List collapsed.'
- })
- }
+ handleHideOptions = (event) => {
+ const { selectedOptionId } = this.state
+ const option = this.getOptionById(selectedOptionId).label
+ this.setState({
+ isShowingOptions: false,
+ highlightedOptionId: null,
+ inputValue: selectedOptionId ? option : '',
+ announcement: 'List collapsed.'
+ })
+ }
- handleBlur = (event) => {
- this.setState({
- highlightedOptionId: null
- })
- }
+ handleBlur = (event) => {
+ this.setState({
+ highlightedOptionId: null
+ })
+ }
- handleHighlightOption = (event, { id }) => {
- event.persist()
- const optionsAvailable = `${this.props.options.length} options available.`
- const nowOpen = !this.state.isShowingOptions ? `List expanded. ${optionsAvailable}` : ''
- const option = this.getOptionById(id).label
- this.setState((state) => ({
- highlightedOptionId: id,
- inputValue: event.type === 'keydown' ? option : state.inputValue,
- announcement: `${option} ${nowOpen}`
- }))
- }
+ handleHighlightOption = (event, { id }) => {
+ event.persist()
+ const optionsAvailable = `${this.props.options.length} options available.`
+ const nowOpen = !this.state.isShowingOptions
+ ? `List expanded. ${optionsAvailable}`
+ : ''
+ const option = this.getOptionById(id).label
+ this.setState((state) => ({
+ highlightedOptionId: id,
+ inputValue: event.type === 'keydown' ? option : state.inputValue,
+ announcement: `${option} ${nowOpen}`
+ }))
+ }
- handleSelectOption = (event, { id }) => {
- const option = this.getOptionById(id).label
- this.setState({
- selectedOptionId: id,
- inputValue: option,
- isShowingOptions: false,
- announcement: `"${option}" selected. List collapsed.`
- })
+ handleSelectOption = (event, { id }) => {
+ const option = this.getOptionById(id).label
+ this.setState({
+ selectedOptionId: id,
+ inputValue: option,
+ isShowingOptions: false,
+ announcement: `"${option}" selected. List collapsed.`
+ })
+ }
+
+ render() {
+ const {
+ inputValue,
+ isShowingOptions,
+ highlightedOptionId,
+ selectedOptionId,
+ announcement
+ } = this.state
+
+ return (
+
+
+ {this.props.options.map((option) => {
+ return (
+
+ {option.label}
+
+ )
+ })}
+
+
document.getElementById('flash-messages')}
+ liveRegionPoliteness="assertive"
+ screenReaderOnly
+ >
+ {announcement}
+
+
+ )
+ }
}
- render () {
- const {
- inputValue,
- isShowingOptions,
- highlightedOptionId,
- selectedOptionId,
- announcement
- } = this.state
+ render(
+
+
+ },
+ {
+ id: 'opt3',
+ label: 'Colored Icon',
+ renderBeforeLabel: (props) => {
+ let color = 'brand'
+ if (props.isHighlighted) color = 'primary-inverse'
+ if (props.isSelected) color = 'primary'
+ if (props.isDisabled) color = 'warning'
+ return
+ }
+ }
+ ]}
+ />
+
+ )
+ ```
+
+- ```js
+ const SingleSelectExample = ({ options }) => {
+ const [inputValue, setInputValue] = useState(options[0].label)
+ const [isShowingOptions, setIsShowingOptions] = useState(false)
+ const [highlightedOptionId, setHighlightedOptionId] = useState(null)
+ const [selectedOptionId, setSelectedOptionId] = useState(options[0].id)
+ const [announcement, setAnnouncement] = useState(null)
+
+ const getOptionById = (queryId) => {
+ return options.find(({ id }) => id === queryId)
+ }
+
+ const handleShowOptions = (event) => {
+ setIsShowingOptions(true)
+ }
+
+ const handleHideOptions = (event) => {
+ const option = getOptionById(selectedOptionId).label
+ setIsShowingOptions(false)
+ setHighlightedOptionId(null)
+ setInputValue(selectedOptionId ? option : '')
+ setAnnouncement('List collapsed.')
+ }
+
+ const handleBlur = (event) => {
+ setHighlightedOptionId(null)
+ }
+
+ const handleHighlightOption = (event, { id }) => {
+ event.persist()
+ const optionsAvailable = `${options.length} options available.`
+ const nowOpen = !isShowingOptions
+ ? `List expanded. ${optionsAvailable}`
+ : ''
+ const option = getOptionById(id).label
+ setHighlightedOptionId(id)
+ setInputValue(event.type === 'keydown' ? option : inputValue)
+ setAnnouncement(`${option} ${nowOpen}`)
+ }
+
+ const handleSelectOption = (event, { id }) => {
+ const option = getOptionById(id).label
+ setSelectedOptionId(id)
+ setInputValue(option)
+ setIsShowingOptions(false)
+ setAnnouncement(`"${option}" selected. List collapsed.`)
+ }
return (
@@ -1320,13 +2452,13 @@ class SingleSelectExample extends React.Component {
assistiveText="Use arrow keys to navigate options."
inputValue={inputValue}
isShowingOptions={isShowingOptions}
- onBlur={this.handleBlur}
- onRequestShowOptions={this.handleShowOptions}
- onRequestHideOptions={this.handleHideOptions}
- onRequestHighlightOption={this.handleHighlightOption}
- onRequestSelectOption={this.handleSelectOption}
+ onBlur={handleBlur}
+ onRequestShowOptions={handleShowOptions}
+ onRequestHideOptions={handleHideOptions}
+ onRequestHighlightOption={handleHighlightOption}
+ onRequestSelectOption={handleSelectOption}
>
- {this.props.options.map((option) => {
+ {options.map((option) => {
return (
- { option.label }
+ {option.label}
)
})}
@@ -1345,43 +2477,42 @@ class SingleSelectExample extends React.Component {
liveRegionPoliteness="assertive"
screenReaderOnly
>
- { announcement }
+ {announcement}
)
}
-}
-render(
-
-
- },
- {
- id: 'opt3',
- label: 'Colored Icon',
- renderBeforeLabel: (props) => {
- let color = 'brand'
- if (props.isHighlighted) color = 'primary-inverse'
- if (props.isSelected) color = 'primary'
- if (props.isDisabled) color = 'warning'
- return
+ render(
+
+
+ },
+ {
+ id: 'opt3',
+ label: 'Colored Icon',
+ renderBeforeLabel: (props) => {
+ let color = 'brand'
+ if (props.isHighlighted) color = 'primary-inverse'
+ if (props.isSelected) color = 'primary'
+ if (props.isDisabled) color = 'warning'
+ return
+ }
}
- }
- ]}
- />
-
-)
-```
+ ]}
+ />
+
+ )
+ ```
#### Providing assistive text for screen readers
diff --git a/packages/ui-selectable/src/Selectable/README.md b/packages/ui-selectable/src/Selectable/README.md
index 9c7b5632d2..a4a3be2e49 100644
--- a/packages/ui-selectable/src/Selectable/README.md
+++ b/packages/ui-selectable/src/Selectable/README.md
@@ -4,153 +4,384 @@ describes: Selectable
`Selectable` is a low level utility component that can be used to create combobox widgets. Before composing your own component, make sure an existing component, like [Select](#Select), can't be adapted for your use case.
-```javascript
----
-type: example
----
-
-class CustomSelect extends React.Component {
- state = {
- isShowingOptions: false,
- highlightedOptionId: this.props.options[0].id,
- selectedOptionId: this.props.options[0].id,
- inputValue: this.props.options[0].label,
- filteredOptions: this.props.options
- }
+- ```javascript
+ class CustomSelect extends React.Component {
+ state = {
+ isShowingOptions: false,
+ highlightedOptionId: this.props.options[0].id,
+ selectedOptionId: this.props.options[0].id,
+ inputValue: this.props.options[0].label,
+ filteredOptions: this.props.options
+ }
- filterOptions = (value) => {
- return this.props.options.filter(option => (
- option.label.toLowerCase().startsWith(value.toLowerCase())
- ))
- }
+ filterOptions = (value) => {
+ return this.props.options.filter((option) =>
+ option.label.toLowerCase().startsWith(value.toLowerCase())
+ )
+ }
- matchValue () {
- const { filteredOptions, inputValue, selectedOptionId } = this.state
- if (filteredOptions.length === 1) {
- if (filteredOptions[0].label.toLowerCase() === inputValue.toLowerCase()) {
- return {
- inputValue: filteredOptions[0].label,
- selectedOptionId: filteredOptions[0].id
+ matchValue() {
+ const { filteredOptions, inputValue, selectedOptionId } = this.state
+ if (filteredOptions.length === 1) {
+ if (
+ filteredOptions[0].label.toLowerCase() === inputValue.toLowerCase()
+ ) {
+ return {
+ inputValue: filteredOptions[0].label,
+ selectedOptionId: filteredOptions[0].id
+ }
}
}
+ const index = this.getOptionIndex(
+ null,
+ selectedOptionId,
+ this.props.options
+ )
+ return { inputValue: this.props.options[index].label }
}
- const index = this.getOptionIndex(null, selectedOptionId, this.props.options)
- return { inputValue: this.props.options[index].label }
- }
- getInputStyles () {
- return {
- display: 'block',
- width: '250px',
- padding: '5px'
+ getInputStyles() {
+ return {
+ display: 'block',
+ width: '250px',
+ padding: '5px'
+ }
}
- }
- getListStyles () {
- const { isShowingOptions } = this.state
- return {
- background: 'white',
- listStyle: 'none',
- padding: 0,
- margin: 0,
- border: isShowingOptions && 'solid 1px lightgray'
+ getListStyles() {
+ const { isShowingOptions } = this.state
+ return {
+ background: 'white',
+ listStyle: 'none',
+ padding: 0,
+ margin: 0,
+ border: isShowingOptions && 'solid 1px lightgray'
+ }
}
- }
- getOptionStyles (option) {
- const { selectedOptionId, highlightedOptionId } = this.state
- const selected = selectedOptionId === option.id
- const highlighted = highlightedOptionId === option.id
- let background = 'transparent'
- if (selected) {
- background = 'lightgray'
- } else if (highlighted) {
- background = '#eeeeee'
- }
- return {
- background,
- padding: '0 10px'
+ getOptionStyles(option) {
+ const { selectedOptionId, highlightedOptionId } = this.state
+ const selected = selectedOptionId === option.id
+ const highlighted = highlightedOptionId === option.id
+ let background = 'transparent'
+ if (selected) {
+ background = 'lightgray'
+ } else if (highlighted) {
+ background = '#eeeeee'
+ }
+ return {
+ background,
+ padding: '0 10px'
+ }
}
- }
- getOptionIndex (direction, id, from) {
- const { filteredOptions, highlightedOptionId } = this.state
- const options = from ? from : filteredOptions
- let index
-
- for (let i = 0; i <= options.length - 1; i++) {
- if (typeof id === 'undefined') {
- if (highlightedOptionId === options[i].id) {
- index = i + direction
- if (index < 0) {
- index = 0
- } else if (index >= options.length - 1) {
- index = options.length - 1
+ getOptionIndex(direction, id, from) {
+ const { filteredOptions, highlightedOptionId } = this.state
+ const options = from ? from : filteredOptions
+ let index
+
+ for (let i = 0; i <= options.length - 1; i++) {
+ if (typeof id === 'undefined') {
+ if (highlightedOptionId === options[i].id) {
+ index = i + direction
+ if (index < 0) {
+ index = 0
+ } else if (index >= options.length - 1) {
+ index = options.length - 1
+ }
+ break
+ }
+ } else {
+ if (id === options[i].id) {
+ index = i
+ break
}
- break
- }
- } else {
- if (id === options[i].id) {
- index = i
- break
}
}
+ return index
+ }
+
+ getHandlers() {
+ return this.props.isDisabled
+ ? {}
+ : {
+ onRequestShowOptions: (e) =>
+ this.setState((state) => ({
+ isShowingOptions: true,
+ highlightedOptionId: state.filteredOptions[0].id
+ })),
+ onRequestHideOptions: (e) => {
+ const index = this.getOptionIndex(
+ null,
+ this.state.selectedOptionId,
+ this.props.options
+ )
+ this.setState((state) => ({
+ isShowingOptions: false,
+ inputValue: this.props.options[index].label,
+ filteredOptions: this.props.options,
+ highlightedOptionId: null
+ }))
+ },
+ onRequestHighlightOption: (e, { id, direction }) => {
+ let index = this.getOptionIndex(direction, id)
+ this.setState((state) => ({
+ highlightedOptionId: state.filteredOptions[index]
+ ? state.filteredOptions[index].id
+ : null,
+ inputValue:
+ direction && state.filteredOptions[index]
+ ? state.filteredOptions[index].label
+ : state.inputValue
+ }))
+ },
+ onRequestSelectOption: (e, { id }) => {
+ const index = this.getOptionIndex(null, id)
+ this.setState((state) => ({
+ selectedOptionId: id,
+ inputValue: state.filteredOptions[index].label,
+ filteredOptions: this.props.options,
+ isShowingOptions: false,
+ highlightedOptionId: null
+ }))
+ }
+ }
+ }
+
+ render() {
+ const {
+ isShowingOptions,
+ inputValue,
+ highlightedOptionId,
+ selectedOptionId,
+ filteredOptions
+ } = this.state
+
+ return (
+
+ {({
+ getRootProps,
+ getLabelProps,
+ getInputProps,
+ getTriggerProps,
+ getListProps,
+ getOptionProps
+ }) => (
+ (this.rootRef = el) })}
+ >
+ Selectable Example
+ {
+ const newOptions = this.filterOptions(e.target.value)
+ this.setState({
+ inputValue: e.target.value,
+ filteredOptions: newOptions,
+ isShowingOptions: true,
+ highlightedOptionId: newOptions[0]
+ ? newOptions[0].id
+ : null
+ })
+ },
+ onBlur: (e) =>
+ this.setState({
+ filteredOptions: this.props.options,
+ highlightedOptionId: null,
+ isShowingOptions: false,
+ ...this.matchValue()
+ })
+ })}
+ />
+
+ {isShowingOptions &&
+ filteredOptions.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
+ )}
+
+ )
}
- return index
}
- getHandlers () {
- return this.props.isDisabled ? {} : {
- onRequestShowOptions: (e) => this.setState(state => ({
- isShowingOptions: true,
- highlightedOptionId: state.filteredOptions[0].id
- })),
- onRequestHideOptions: (e) => {
- const index = this.getOptionIndex(null, this.state.selectedOptionId, this.props.options)
- this.setState(state => ({
- isShowingOptions: false,
- inputValue: this.props.options[index].label,
- filteredOptions: this.props.options,
- highlightedOptionId: null,
- }))
- },
- onRequestHighlightOption: (e, { id, direction }) => {
- let index = this.getOptionIndex(direction, id)
- this.setState(state => ({
- highlightedOptionId: state.filteredOptions[index] ? state.filteredOptions[index].id : null,
- inputValue: direction && state.filteredOptions[index]
- ? state.filteredOptions[index].label
- : state.inputValue
- }))
- },
- onRequestSelectOption: (e, { id }) => {
- const index = this.getOptionIndex(null, id)
- this.setState(state => ({
- selectedOptionId: id,
- inputValue: state.filteredOptions[index].label,
- filteredOptions: this.props.options,
- isShowingOptions: false,
- highlightedOptionId: null,
- }))
+ render(
+
+
+
+ )
+ ```
+
+- ```js
+ const CustomSelect = ({ options, isDisabled }) => {
+ const [isShowingOptions, setIsShowingOptions] = useState(false)
+ const [highlightedOptionId, setHighlightedOptionId] = useState(
+ options[0].id
+ )
+ const [selectedOptionId, setSelectedOptionId] = useState(options[0].id)
+ const [inputValue, setInputValue] = useState(options[0].label)
+ const [filteredOptions, setFilteredOptions] = useState(options)
+
+ const rootRef = useRef(null)
+
+ const filterOptions = (value) => {
+ return options.filter((option) =>
+ option.label.toLowerCase().startsWith(value.toLowerCase())
+ )
+ }
+
+ const matchValue = () => {
+ if (filteredOptions.length === 1) {
+ if (
+ filteredOptions[0].label.toLowerCase() === inputValue.toLowerCase()
+ ) {
+ setInputValue(filteredOptions[0].label)
+ setsSelectedOptionId(filteredOptions[0].id)
+ return
+ }
}
+ const index = getOptionIndex(null, selectedOptionId, options)
+ setInputValue(options[index].label)
+ }
+
+ const getInputStyles = () => {
+ return {
+ display: 'block',
+ width: '250px',
+ padding: '5px'
+ }
+ }
+
+ const getListStyles = () => {
+ return {
+ background: 'white',
+ listStyle: 'none',
+ padding: 0,
+ margin: 0,
+ border: isShowingOptions && 'solid 1px lightgray'
+ }
+ }
+
+ const getOptionStyles = (option) => {
+ const selected = selectedOptionId === option.id
+ const highlighted = highlightedOptionId === option.id
+ let background = 'transparent'
+ if (selected) {
+ background = 'lightgray'
+ } else if (highlighted) {
+ background = '#eeeeee'
+ }
+ return {
+ background,
+ padding: '0 10px'
+ }
+ }
+
+ const getOptionIndex = (direction, id, from) => {
+ const options = from ? from : filteredOptions
+ let index
+
+ for (let i = 0; i <= options.length - 1; i++) {
+ if (typeof id === 'undefined') {
+ if (highlightedOptionId === options[i].id) {
+ index = i + direction
+ if (index < 0) {
+ index = 0
+ } else if (index >= options.length - 1) {
+ index = options.length - 1
+ }
+ break
+ }
+ } else {
+ if (id === options[i].id) {
+ index = i
+ break
+ }
+ }
+ }
+ return index
}
- }
- render () {
- const {
- isShowingOptions,
- inputValue,
- highlightedOptionId,
- selectedOptionId,
- filteredOptions
- } = this.state
+ const getHandlers = () => {
+ return isDisabled
+ ? {}
+ : {
+ onRequestShowOptions: (e) => {
+ setIsShowingOptions(true)
+ setHighlightedOptionId(filteredOptions[0].id)
+ },
+ onRequestHideOptions: (e) => {
+ const index = getOptionIndex(null, selectedOptionId, options)
+ setIsShowingOptions(false)
+ setInputValue(options[index].label)
+ setFilteredOptions(options)
+ setHighlightedOptionId(null)
+ },
+ onRequestHighlightOption: (e, { id, direction }) => {
+ let index = getOptionIndex(direction, id)
+ setHighlightedOptionId(
+ filteredOptions[index] ? filteredOptions[index].id : null
+ )
+ setInputValue(
+ direction && filteredOptions[index]
+ ? filteredOptions[index].label
+ : inputValue
+ )
+ },
+ onRequestSelectOption: (e, { id }) => {
+ const index = getOptionIndex(null, id)
+ setSelectedOptionId(id)
+ setInputValue(filteredOptions[index].label)
+ setFilteredOptions(options)
+ setIsShowingOptions(false)
+ setHighlightedOptionId(null)
+ }
+ }
+ }
return (
{({
getRootProps,
@@ -161,80 +392,77 @@ class CustomSelect extends React.Component {
getOptionProps
}) => (
this.rootRef = el})}
+ style={{ display: 'inline-block' }}
+ {...getRootProps({ ref: rootRef })}
>
Selectable Example
{
- const newOptions = this.filterOptions(e.target.value)
- this.setState({
- inputValue: e.target.value,
- filteredOptions: newOptions,
- isShowingOptions: true,
- highlightedOptionId: newOptions[0] ? newOptions[0].id : null
- })
+ const newOptions = filterOptions(e.target.value)
+ setInputValue(e.target.valu)
+ setFilteredOptions(newOptions)
+ setIsShowingOptions(true)
+ setHighlightedOptionId(
+ newOptions[0] ? newOptions[0].id : null
+ )
},
- onBlur: (e) => this.setState({
- filteredOptions: this.props.options,
- highlightedOptionId: null,
- isShowingOptions: false,
- ...this.matchValue()
- })
- })
- } />
-
- {isShowingOptions && filteredOptions.map((option) => (
-
- {option.label}
-
- ))}
+ onBlur: (e) => {
+ setFilteredOptions(options)
+ setHighlightedOptionId(null)
+ setIsShowingOptions(false)
+ matchValue()
+ }
+ })}
+ />
+
+ {isShowingOptions &&
+ filteredOptions.map((option) => (
+
+ {option.label}
+
+ ))}
)}
)
}
-}
-
-render(
-
-
-
-)
-```
+
+ render(
+
+
+
+ )
+ ```
Selectable has very few opinions about how a combobox component should be composed. It mostly aims to ensure all the proper WAI-ARIA roles and attributes are set on the right elements at the right times. Selectable uses a combination of controllable props and prop getters to set these attributes and provide accessible behavior.