@instructure/ui-select
Advanced tools
Comparing version 6.4.1-rc.8 to 6.4.1-rc.9
@@ -158,11 +158,19 @@ import _defineProperty from "@babel/runtime/helpers/esm/defineProperty"; | ||
}, { | ||
key: "componentDidUpdate", | ||
value: function componentDidUpdate() { | ||
// scroll option into view if needed | ||
this.scrollToOption(this.highlightedOptionId); | ||
} | ||
}, { | ||
key: "scrollToOption", | ||
value: function scrollToOption(id) { | ||
if (this._listView) { | ||
var item = this._listView.querySelector("[id=\"".concat(id, "\"]")).parentNode; | ||
var option = this._listView.querySelector("[id=\"".concat(id, "\"]")); | ||
if (!option) return; | ||
var listItem = option.parentNode; | ||
var parentTop = getBoundingClientRect(this._listView).top; | ||
var elemTop = getBoundingClientRect(item).top; | ||
var elemTop = getBoundingClientRect(listItem).top; | ||
var parentBottom = parentTop + this._listView.clientHeight; | ||
var elemBottom = elemTop + item.clientHeight; | ||
var elemBottom = elemTop + listItem.clientHeight; | ||
@@ -185,3 +193,2 @@ if (elemBottom > parentBottom) { | ||
}); | ||
this.scrollToOption(id); // scroll into view if needed | ||
} | ||
@@ -188,0 +195,0 @@ } |
@@ -181,11 +181,19 @@ "use strict"; | ||
}, { | ||
key: "componentDidUpdate", | ||
value: function componentDidUpdate() { | ||
// scroll option into view if needed | ||
this.scrollToOption(this.highlightedOptionId); | ||
} | ||
}, { | ||
key: "scrollToOption", | ||
value: function scrollToOption(id) { | ||
if (this._listView) { | ||
var item = this._listView.querySelector("[id=\"".concat(id, "\"]")).parentNode; | ||
var option = this._listView.querySelector("[id=\"".concat(id, "\"]")); | ||
if (!option) return; | ||
var listItem = option.parentNode; | ||
var parentTop = (0, _getBoundingClientRect.getBoundingClientRect)(this._listView).top; | ||
var elemTop = (0, _getBoundingClientRect.getBoundingClientRect)(item).top; | ||
var elemTop = (0, _getBoundingClientRect.getBoundingClientRect)(listItem).top; | ||
var parentBottom = parentTop + this._listView.clientHeight; | ||
var elemBottom = elemTop + item.clientHeight; | ||
var elemBottom = elemTop + listItem.clientHeight; | ||
@@ -208,3 +216,2 @@ if (elemBottom > parentBottom) { | ||
}); | ||
this.scrollToOption(id); // scroll into view if needed | ||
} | ||
@@ -211,0 +218,0 @@ } |
{ | ||
"name": "@instructure/ui-select", | ||
"version": "6.4.1-rc.8+3b91145d", | ||
"version": "6.4.1-rc.9+fd917148", | ||
"description": "A component for select and autocomplete behavior.", | ||
@@ -23,23 +23,23 @@ "author": "Instructure, Inc. Engineering and Product Design", | ||
"devDependencies": { | ||
"@instructure/ui-babel-preset": "6.4.1-rc.8+3b91145d", | ||
"@instructure/ui-color-utils": "6.4.1-rc.8+3b91145d", | ||
"@instructure/ui-test-utils": "6.4.1-rc.8+3b91145d" | ||
"@instructure/ui-babel-preset": "6.4.1-rc.9+fd917148", | ||
"@instructure/ui-color-utils": "6.4.1-rc.9+fd917148", | ||
"@instructure/ui-test-utils": "6.4.1-rc.9+fd917148" | ||
}, | ||
"dependencies": { | ||
"@babel/runtime": "^7", | ||
"@instructure/ui-dom-utils": "6.4.1-rc.8+3b91145d", | ||
"@instructure/ui-elements": "6.4.1-rc.8+3b91145d", | ||
"@instructure/ui-form-field": "6.4.1-rc.8+3b91145d", | ||
"@instructure/ui-icons": "6.4.1-rc.8+3b91145d", | ||
"@instructure/ui-layout": "6.4.1-rc.8+3b91145d", | ||
"@instructure/ui-options": "6.4.1-rc.8+3b91145d", | ||
"@instructure/ui-overlays": "6.4.1-rc.8+3b91145d", | ||
"@instructure/ui-prop-types": "6.4.1-rc.8+3b91145d", | ||
"@instructure/ui-react-utils": "6.4.1-rc.8+3b91145d", | ||
"@instructure/ui-selectable": "6.4.1-rc.8+3b91145d", | ||
"@instructure/ui-testable": "6.4.1-rc.8+3b91145d", | ||
"@instructure/ui-text-input": "6.4.1-rc.8+3b91145d", | ||
"@instructure/ui-themeable": "6.4.1-rc.8+3b91145d", | ||
"@instructure/ui-utils": "6.4.1-rc.8+3b91145d", | ||
"@instructure/uid": "6.4.1-rc.8+3b91145d", | ||
"@instructure/ui-dom-utils": "6.4.1-rc.9+fd917148", | ||
"@instructure/ui-elements": "6.4.1-rc.9+fd917148", | ||
"@instructure/ui-form-field": "6.4.1-rc.9+fd917148", | ||
"@instructure/ui-icons": "6.4.1-rc.9+fd917148", | ||
"@instructure/ui-layout": "6.4.1-rc.9+fd917148", | ||
"@instructure/ui-options": "6.4.1-rc.9+fd917148", | ||
"@instructure/ui-overlays": "6.4.1-rc.9+fd917148", | ||
"@instructure/ui-prop-types": "6.4.1-rc.9+fd917148", | ||
"@instructure/ui-react-utils": "6.4.1-rc.9+fd917148", | ||
"@instructure/ui-selectable": "6.4.1-rc.9+fd917148", | ||
"@instructure/ui-testable": "6.4.1-rc.9+fd917148", | ||
"@instructure/ui-text-input": "6.4.1-rc.9+fd917148", | ||
"@instructure/ui-themeable": "6.4.1-rc.9+fd917148", | ||
"@instructure/ui-utils": "6.4.1-rc.9+fd917148", | ||
"@instructure/uid": "6.4.1-rc.9+fd917148", | ||
"classnames": "^2", | ||
@@ -53,3 +53,3 @@ "prop-types": "^15", | ||
"sideEffects": false, | ||
"gitHead": "3b91145d394306bb2ee84cac56259d81f3de9b1d" | ||
"gitHead": "fd9171482e89c4ebd549bac7d1388d043b6d4352" | ||
} |
@@ -315,13 +315,21 @@ /* | ||
handleInputContainerRef= (node) => { | ||
handleInputContainerRef = (node) => { | ||
this._inputContainer = node | ||
} | ||
componentDidUpdate () { | ||
// scroll option into view if needed | ||
this.scrollToOption(this.highlightedOptionId) | ||
} | ||
scrollToOption (id) { | ||
if (this._listView) { | ||
const item = this._listView.querySelector(`[id="${id}"]`).parentNode | ||
const option = this._listView.querySelector(`[id="${id}"]`) | ||
if (!option) return | ||
const listItem = option.parentNode | ||
const parentTop = getBoundingClientRect(this._listView).top | ||
const elemTop = getBoundingClientRect(item).top | ||
const elemTop = getBoundingClientRect(listItem).top | ||
const parentBottom = parentTop + this._listView.clientHeight | ||
const elemBottom = elemTop + item.clientHeight | ||
const elemBottom = elemTop + listItem.clientHeight | ||
@@ -340,3 +348,2 @@ if (elemBottom > parentBottom) { | ||
onRequestHighlightOption(event, { id }) | ||
this.scrollToOption(id) // scroll into view if needed | ||
} | ||
@@ -343,0 +350,0 @@ } |
@@ -153,2 +153,4 @@ --- | ||
> Note: Select makes some conditional assumptions about keyboard behavior. For example, if the list is NOT showing, up/down arrow keys and the space key, will show the list. Otherwise, the arrows will navigate options and the space key will type a space character. | ||
```javascript | ||
@@ -259,7 +261,8 @@ --- | ||
event.persist() | ||
const option = this.getOptionById(id).label | ||
const option = this.getOptionById(id) | ||
if (!option) return // prevent highlighting of empty option | ||
this.setState((state) => ({ | ||
highlightedOptionId: id, | ||
inputValue: event.type === 'keydown' ? option : state.inputValue, | ||
announcement: option | ||
inputValue: event.type === 'keydown' ? option.label : state.inputValue, | ||
announcement: option.label | ||
})) | ||
@@ -269,9 +272,10 @@ } | ||
handleSelectOption = (event, { id }) => { | ||
const option = this.getOptionById(id).label | ||
const option = this.getOptionById(id) | ||
if (!option) return // prevent selecting of empty option | ||
this.setState({ | ||
selectedOptionId: id, | ||
inputValue: option, | ||
inputValue: option.label, | ||
isShowingOptions: false, | ||
filteredOptions: this.props.options, | ||
announcement: `${option} selected. List collapsed.` | ||
announcement: `${option.label} selected. List collapsed.` | ||
}) | ||
@@ -340,3 +344,2 @@ } | ||
key="empty-option" | ||
isDisabled | ||
> | ||
@@ -481,7 +484,8 @@ --- | ||
event.persist() | ||
const option = this.getOptionById(id).label | ||
const option = this.getOptionById(id) | ||
if (!option) return // prevent highlighting empty option | ||
this.setState((state) => ({ | ||
highlightedOptionId: id, | ||
inputValue: event.type === 'keydown' ? option : state.inputValue, | ||
announcement: option | ||
inputValue: event.type === 'keydown' ? option.label : state.inputValue, | ||
announcement: option.label | ||
})) | ||
@@ -491,2 +495,4 @@ } | ||
handleSelectOption = (event, { id }) => { | ||
const option = this.getOptionById(id) | ||
if (!option) return // prevent selecting of empty option | ||
this.setState((state) => ({ | ||
@@ -498,3 +504,3 @@ selectedOptionId: [...state.selectedOptionId, id], | ||
isShowingOptions: false, | ||
announcement: `${this.getOptionById(id).label} selected. List collapsed.` | ||
announcement: `${option.label} selected. List collapsed.` | ||
})) | ||
@@ -600,3 +606,2 @@ } | ||
key="empty-option" | ||
isDisabled | ||
> | ||
@@ -836,3 +841,226 @@ --- | ||
#### 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 | ||
--- | ||
example: true | ||
render: false | ||
--- | ||
class AsyncExample extends React.Component { | ||
state = { | ||
inputValue: '', | ||
isShowingOptions: false, | ||
isLoading: false, | ||
highlightedOptionId: null, | ||
selectedOptionId: null, | ||
selectedOptionLabel: '', | ||
filteredOptions: [], | ||
announcement: null | ||
} | ||
timeoutId = null | ||
getOptionById (queryId) { | ||
return this.state.filteredOptions.find(({ id }) => id === queryId) | ||
} | ||
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 | ||
} | ||
} | ||
} | ||
// 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 }) | ||
} | ||
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 | ||
})) | ||
} | ||
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)] | ||
}) | ||
} | ||
handleInputChange = (event) => { | ||
const value = event.target.value | ||
clearTimeout(this.timeoutId) | ||
if (!value || value === '') { | ||
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.' | ||
}) | ||
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 ( | ||
<div> | ||
<Select | ||
renderLabel="Async Select" | ||
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} | ||
> | ||
{filteredOptions.length > 0 ? filteredOptions.map((option) => { | ||
return ( | ||
<Select.Option | ||
id={option.id} | ||
key={option.id} | ||
isHighlighted={option.id === highlightedOptionId} | ||
isSelected={option.id === selectedOptionId} | ||
isDisabled={option.disabled} | ||
renderBeforeLabel={!option.disabled ? IconUserSolid : IconUserLine} | ||
> | ||
{option.label} | ||
</Select.Option> | ||
) | ||
}) : ( | ||
<Select.Option id="empty-option" key="empty-option"> | ||
{isLoading | ||
? <Spinner renderTitle="Loading" size="x-small" /> | ||
: inputValue !== '' ? 'No results' : 'Type to search'} | ||
</Select.Option> | ||
)} | ||
</Select> | ||
<Alert | ||
liveRegion={() => document.getElementById('flash-messages')} | ||
liveRegionPoliteness="assertive" | ||
screenReaderOnly | ||
> | ||
{ announcement } | ||
</Alert> | ||
</div> | ||
) | ||
} | ||
} | ||
render( | ||
<View> | ||
<AsyncExample | ||
options={[ | ||
{ id: '0', label: 'Aaron Aaronson' }, | ||
{ id: '1', label: 'Amber Murphy' }, | ||
{ id: '2', label: 'Andrew Miller' }, | ||
{ id: '3', label: 'Barbara Ward' }, | ||
{ id: '4', label: 'Byron Cranston', disabled: true }, | ||
{ id: '5', label: 'Dennis Reynolds' }, | ||
{ id: '6', label: 'Dee Reynolds' }, | ||
{ id: '7', label: 'Ezra Betterthan' }, | ||
{ id: '8', label: 'Jeff Spicoli' }, | ||
{ id: '9', label: 'Joseph Smith' }, | ||
{ id: '10', label: 'Jasmine Diaz' }, | ||
{ id: '11', label: 'Martin Harris' }, | ||
{ id: '12', label: 'Michael Morgan', disabled: true }, | ||
{ id: '13', label: 'Michelle Rodriguez' }, | ||
{ id: '14', label: 'Ziggy Stardust' }, | ||
]} | ||
/> | ||
</View> | ||
) | ||
``` | ||
#### Providing assistive text for screen readers | ||
It's important to ensure screen reader users receive instruction and feedback while interacting with a `Select`, but screen reader support for the `combobox` role varies. The `assistiveText` prop should always be used to explain how a keyboard user can make a selection. Additionally, a live region should be updated with feedback as the component is interacted with, such as when options are filtered or highlighted. Using an [Alert](#Alert) with the `screenReaderOnly` prop is the easiest way to do this. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
144145
2839