Fix Select deprecations with CanvaSelect
this commit add the new CanvasSelect which wraps the new controlled-only INSTUI Select (single select only) to provide a nearly drop-in replacement for the deprecated INSTUI Select used in canvas. This new CanvasSelect is then used in people_search.js and TimeZoneSelect/index.js to resolve the deprectation warnings. changes include an upgrade to ui-select 6.8 closes ADMIN-2775, COREFE-186 COREFE-184 test plan: - nav to a course's people page - click on the +People button > expect the Role and Section selects to work as expected - nav to the account's people page - click on the pencil icon to the right of a user > expect the Time Zone select to show a blank line, then 2 groups of time zones > expect the select to work as expected > expect screenreaders to tell you interesting things as you interact with the select Change-Id: I5dcfb2c1c8ca64071ce9dbf0a194777f10c711cf Reviewed-on: https://gerrit.instructure.com/202508 Reviewed-by: Ryan Shaw <ryan@instructure.com> QA-Review: Ryan Shaw <ryan@instructure.com> Tested-by: Jenkins Product-Review: Ed Schiebel <eschiebel@instructure.com>
This commit is contained in:
parent
62ebbad0ae
commit
28ea3000ef
|
@ -314,6 +314,7 @@ export default class AddPeople extends React.Component {
|
|||
shouldCloseOnDocumentClick={false}
|
||||
size="medium"
|
||||
tabIndex="-1"
|
||||
liveRegion={getLiveRegion}
|
||||
>
|
||||
<ModalHeader>
|
||||
<CloseButton
|
||||
|
@ -362,3 +363,7 @@ export default class AddPeople extends React.Component {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
function getLiveRegion() {
|
||||
return document.getElementById('flash_screenreader_holder')
|
||||
}
|
||||
|
|
|
@ -173,16 +173,19 @@ import Button from '@instructure/ui-buttons/lib/components/Button'
|
|||
label={<ScreenReaderContent>{nameLabel}</ScreenReaderContent>}
|
||||
data-address={missing.address}
|
||||
onChange={this.onNewForMissingChange}
|
||||
value={missing.newUserInfo.name || ''}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<TextInput
|
||||
required
|
||||
name="email"
|
||||
type="email"placeholder={emailLabel}
|
||||
type="email"
|
||||
placeholder={emailLabel}
|
||||
label={<ScreenReaderContent>{emailLabel}</ScreenReaderContent>}
|
||||
data-address={missing.address}
|
||||
onChange={this.onNewForMissingChange}
|
||||
value={missing.newUserInfo.email || ''}
|
||||
/>
|
||||
</td>
|
||||
<th scope="row">{missing.address}</th>
|
||||
|
@ -245,7 +248,7 @@ import Button from '@instructure/ui-buttons/lib/components/Button'
|
|||
label={<ScreenReaderContent>{nameLabel}</ScreenReaderContent>}
|
||||
data-address={missing.address}
|
||||
onChange={this.onNewForMissingChange}
|
||||
value={missing.newUserInfo.name || null}
|
||||
value={missing.newUserInfo.name || ''}
|
||||
/>
|
||||
</td>
|
||||
<th scope="row">{missing.address}</th>
|
||||
|
|
|
@ -21,7 +21,7 @@ import React from 'react'
|
|||
import Text from '@instructure/ui-elements/lib/components/Text'
|
||||
import RadioInputGroup from '@instructure/ui-forms/lib/components/RadioInputGroup'
|
||||
import RadioInput from '@instructure/ui-forms/lib/components/RadioInput'
|
||||
import Select from '@instructure/ui-core/lib/components/Select'
|
||||
import CanvasSelect from '../../shared/components/CanvasSelect'
|
||||
import TextArea from '@instructure/ui-forms/lib/components/TextArea'
|
||||
import ScreenReaderContent from '@instructure/ui-a11y/lib/components/ScreenReaderContent'
|
||||
import Checkbox from '@instructure/ui-forms/lib/components/Checkbox'
|
||||
|
@ -63,11 +63,11 @@ import {parseNameList, findEmailInEntry, emailValidator} from '../helpers'
|
|||
this.props.onChange({nameList: event.target.value});
|
||||
}
|
||||
|
||||
onChangeSection = (event) => {
|
||||
this.props.onChange({section: event.target.value});
|
||||
onChangeSection = (event, optionValue) => {
|
||||
this.props.onChange({section: optionValue});
|
||||
}
|
||||
onChangeRole = (event) => {
|
||||
this.props.onChange({role: event.target.value});
|
||||
onChangeRole = (event, optionValue) => {
|
||||
this.props.onChange({role: optionValue});
|
||||
}
|
||||
onChangePrivilege = (event) => {
|
||||
this.props.onChange({limitPrivilege: event.target.checked});
|
||||
|
@ -155,28 +155,28 @@ import {parseNameList, findEmailInEntry, emailValidator} from '../helpers'
|
|||
<fieldset className="peoplesearch__selections">
|
||||
<div>
|
||||
<div className="peoplesearch__selection">
|
||||
<Select
|
||||
id="peoplesearch_select_role"
|
||||
<CanvasSelect
|
||||
label={I18n.t('Role')}
|
||||
value={this.props.role}
|
||||
id="peoplesearch_select_role"
|
||||
value={this.props.role || (this.props.roles.length ? this.props.roles[0].id : null)}
|
||||
onChange={this.onChangeRole}
|
||||
>
|
||||
{
|
||||
this.props.roles.map(r => <option key={`r_${r.name}`} value={r.id}>{r.label}</option>)
|
||||
}
|
||||
</Select>
|
||||
{this.props.roles.map(r => (
|
||||
<CanvasSelect.Option key={r.id} id={r.id} value={r.id}>{r.label}</CanvasSelect.Option>
|
||||
))}
|
||||
</CanvasSelect>
|
||||
</div>
|
||||
<div className="peoplesearch__selection">
|
||||
<Select
|
||||
id="peoplesearch_select_section"
|
||||
<CanvasSelect
|
||||
label={I18n.t('Section')}
|
||||
value={this.props.section}
|
||||
id="peoplesearch_select_section"
|
||||
value={this.props.section || (this.props.sections.length ? this.props.sections[0].id : null)}
|
||||
onChange={this.onChangeSection}
|
||||
>
|
||||
{
|
||||
this.props.sections.map(s => <option key={`s_${s.id}`} value={s.id}>{s.name}</option>)
|
||||
}
|
||||
</Select>
|
||||
{this.props.sections.map(s => (
|
||||
<CanvasSelect.Option key={s.id} id={s.id} value={s.id}>{s.name}</CanvasSelect.Option>
|
||||
))}
|
||||
</CanvasSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{marginTop: '1em'}}>
|
||||
|
|
|
@ -0,0 +1,299 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/*
|
||||
---
|
||||
CanvasSelect is a wrapper on the new (as of instui 5 or 6 or so) controlled-only Select
|
||||
While CanvasSelect is also controlled-only, it has a simpler api and is almost a drop-in
|
||||
replacement for the old instui Select used throughout canvas at this time. One big difference
|
||||
is the need to pass in an options property rather than rendering <Options> children
|
||||
|
||||
It does not currently support old-Select's allowCustom property
|
||||
(see https://instructure.design/#DeprecatedSelect)
|
||||
|
||||
It only handles single-select. Multi-select will likely have to be in a separate component
|
||||
|
||||
<CanvasSelect
|
||||
id="your-id"
|
||||
label="select's label"
|
||||
value={value} // should match the ID of the selected option
|
||||
onChange={handleChange} // function(event, selectedOption)
|
||||
{...otherPropsPassedToTheUnderlyingSelect} // if you need to (width="100%" is a popular one)
|
||||
>
|
||||
<CanvasSelect.Option key="1" id="1" value="1">one</CanvasSelect.Option>
|
||||
<CanvasSelect.Option key="2" id="2" value="2">two</CanvasSelect.Option>
|
||||
<CanvasSelect.Option key="3" id="3" value="3">three</CanvasSelect.Option>
|
||||
</CanvasSelect>
|
||||
---
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {func, node, string} from 'prop-types'
|
||||
import I18n from 'i18n!app_shared_components'
|
||||
import {Select} from '@instructure/ui-select'
|
||||
import {Alert} from '@instructure/ui-alerts'
|
||||
import {matchComponentTypes} from '@instructure/ui-react-utils'
|
||||
|
||||
const noOptionsOptionId = '_noOptionsOption'
|
||||
|
||||
// CanvasSelectOption and CanvasSelectGroup are components our client can create thru CanvasSelect
|
||||
// to pass us our options. They are never rendered themselves, but get transformed into INSTUI's
|
||||
// Select.Option and Select.Group on rendering CanvasSelect. See renderChildren below.
|
||||
function CanvasSelectOption() {
|
||||
return <div />
|
||||
}
|
||||
CanvasSelectOption.propTypes = {
|
||||
id: string.isRequired, // eslint-disable-line react/no-unused-prop-types
|
||||
value: string.isRequired // eslint-disable-line react/no-unused-prop-types
|
||||
}
|
||||
|
||||
function CanvasSelectGroup() {
|
||||
return <div />
|
||||
}
|
||||
CanvasSelectGroup.propTypes = {
|
||||
label: string.isRequired // eslint-disable-line react/no-unused-prop-types
|
||||
}
|
||||
|
||||
export default class CanvasSelect extends React.Component {
|
||||
static Option = CanvasSelectOption
|
||||
|
||||
static Group = CanvasSelectGroup
|
||||
|
||||
static propTypes = {
|
||||
id: string,
|
||||
label: string.isRequired,
|
||||
value: string.isRequired,
|
||||
onChange: func.isRequired,
|
||||
children: node,
|
||||
noOptionsLabel: string // unselectable option to display when there are no options
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
noOptionsLabel: '---'
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
const option = this.getOptionByFieldValue('value', props.value)
|
||||
|
||||
this.state = {
|
||||
inputValue: option ? option.props.children : '',
|
||||
isShowingOptions: false,
|
||||
highlightedOptionId: null,
|
||||
selectedOptionId: option ? option.props.id : null,
|
||||
announcement: null
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {id, label, value, onChange, children, noOptionsLabel, ...otherProps} = this.props
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Select
|
||||
id={id}
|
||||
renderLabel={() => label}
|
||||
assistiveText={I18n.t('Use arrow keys to navigate options.')}
|
||||
inputValue={this.state.inputValue}
|
||||
isShowingOptions={this.state.isShowingOptions}
|
||||
onBlur={this.handleBlur}
|
||||
onRequestShowOptions={this.handleShowOptions}
|
||||
onRequestHideOptions={this.handleHideOptions}
|
||||
onRequestHighlightOption={this.handleHighlightOption}
|
||||
onRequestSelectOption={this.handleSelectOption}
|
||||
{...otherProps}
|
||||
>
|
||||
{this.renderChildren(children)}
|
||||
</Select>
|
||||
<Alert
|
||||
liveRegion={() => document.getElementById('flash_screenreader_holder')}
|
||||
liveRegionPoliteness="assertive"
|
||||
screenReaderOnly
|
||||
>
|
||||
{this.state.announcement}
|
||||
</Alert>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
renderChildren(children) {
|
||||
if (!Array.isArray(children)) {
|
||||
// children is 1 child
|
||||
if (matchComponentTypes(children, [CanvasSelectOption])) {
|
||||
return this.renderOption(children)
|
||||
} else {
|
||||
return this.renderNoOptionsOption()
|
||||
}
|
||||
}
|
||||
|
||||
const opts = children
|
||||
.map(child => {
|
||||
if (Array.isArray(child)) {
|
||||
return this.renderChildren(child)
|
||||
} else if (matchComponentTypes(child, [CanvasSelectOption])) {
|
||||
return this.renderOption(child)
|
||||
} else if (matchComponentTypes(child, [CanvasSelectGroup])) {
|
||||
return this.renderGroup(child)
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter(child => !!child) // instui Select blows up on undefined options
|
||||
|
||||
if (opts.length === 0) {
|
||||
return this.renderNoOptionsOption()
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
backupKey = 0
|
||||
|
||||
renderOption(option) {
|
||||
const {id, children, ...optionProps} = option.props
|
||||
return (
|
||||
<Select.Option
|
||||
id={id}
|
||||
key={option.key || id || ++this.backupKey}
|
||||
isHighlighted={id === this.state.highlightedOptionId}
|
||||
isSelected={id === this.state.selectedOptionId}
|
||||
{...optionProps}
|
||||
>
|
||||
{children}
|
||||
</Select.Option>
|
||||
)
|
||||
}
|
||||
|
||||
renderGroup(group) {
|
||||
const {id, label, ...otherProps} = group.props
|
||||
return (
|
||||
<Select.Group
|
||||
data-testid={`Group:${label}`}
|
||||
renderLabel={() => label}
|
||||
key={group.key || id || ++this.backupKey}
|
||||
{...otherProps}
|
||||
>
|
||||
{group.props.children.map(c => this.renderOption(c))}
|
||||
</Select.Group>
|
||||
)
|
||||
}
|
||||
|
||||
renderNoOptionsOption() {
|
||||
return (
|
||||
<Select.Option id={noOptionsOptionId} isHighlighted={false} isSelected={false}>
|
||||
{this.props.noOptionsLabel}
|
||||
</Select.Option>
|
||||
)
|
||||
}
|
||||
|
||||
handleBlur = _event => {
|
||||
this.setState({highlightedOptionId: null})
|
||||
}
|
||||
|
||||
handleShowOptions = () => {
|
||||
this.setState({
|
||||
isShowingOptions: true
|
||||
})
|
||||
}
|
||||
|
||||
handleHideOptions = _event => {
|
||||
this.setState(state => {
|
||||
const text = this.getOptionLabelById(state.selectedOptionId)
|
||||
return {
|
||||
isShowingOptions: false,
|
||||
highlightedOptionId: null,
|
||||
inputValue: text,
|
||||
announcement: I18n.t('List collapsed.')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* eslint-disable react/no-access-state-in-setstate */
|
||||
// Beause handleShowOptions sets state.isShowingOptions:true
|
||||
// it's already in the value of state passed to the setState(updater)
|
||||
// by the time handleHighlightOption is called we miss the transition,
|
||||
// this.state still has the previous value as of the last render
|
||||
// which is what we need. This is why we use this version of setState.
|
||||
handleHighlightOption = (event, {id}) => {
|
||||
if (id === noOptionsOptionId) return
|
||||
|
||||
const text = this.getOptionLabelById(id)
|
||||
const nowOpen = this.state.isShowingOptions ? '' : I18n.t('List expanded.')
|
||||
const inputValue = event.type === 'keydown' ? text : this.state.inputValue
|
||||
this.setState({
|
||||
highlightedOptionId: id,
|
||||
inputValue,
|
||||
announcement: `${text} ${nowOpen}`
|
||||
})
|
||||
}
|
||||
/* eslint-enable react/no-access-state-in-setstate */
|
||||
|
||||
handleSelectOption = (event, {id}) => {
|
||||
if (id === noOptionsOptionId) {
|
||||
this.setState({
|
||||
isShowingOptions: false,
|
||||
announcement: I18n.t('List collapsed')
|
||||
})
|
||||
} else {
|
||||
const text = this.getOptionLabelById(id)
|
||||
const prevSelection = this.state.selectedOptionId
|
||||
this.setState({
|
||||
selectedOptionId: id,
|
||||
inputValue: text,
|
||||
isShowingOptions: false,
|
||||
announcement: I18n.t('%{option} selected. List collapsed.', {option: text})
|
||||
})
|
||||
const option = this.getOptionByFieldValue('id', id)
|
||||
if (prevSelection !== id) {
|
||||
this.props.onChange(event, option.props.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getOptionLabelById(oid) {
|
||||
const option = this.getOptionByFieldValue('id', oid)
|
||||
return option ? option.props.children : ''
|
||||
}
|
||||
|
||||
getOptionByFieldValue(field, value, options = this.props.children) {
|
||||
if (!this.props.children) return null
|
||||
|
||||
let foundOpt = null
|
||||
for (let i = 0; i < options.length; ++i) {
|
||||
const o = options[i]
|
||||
if (Array.isArray(o)) {
|
||||
foundOpt = this.getOptionByFieldValue(field, value, o)
|
||||
} else if (matchComponentTypes(o, [CanvasSelectOption])) {
|
||||
if (o.props[field] === value) {
|
||||
foundOpt = o
|
||||
}
|
||||
} else if (matchComponentTypes(o, [CanvasSelectGroup])) {
|
||||
for (let j = 0; j < o.props.children.length; ++j) {
|
||||
const o2 = o.props.children[j]
|
||||
if (o2.props[field] === value) {
|
||||
foundOpt = o2
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (foundOpt) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return foundOpt
|
||||
}
|
||||
}
|
|
@ -213,7 +213,7 @@ export default class CreateOrUpdateUserModal extends React.Component {
|
|||
onSubmit={preventDefault(this.onSubmit)}
|
||||
open={this.state.open}
|
||||
onDismiss={this.close}
|
||||
size="small"
|
||||
size="medium"
|
||||
label={
|
||||
this.props.createOrUpdate === 'create' ? (
|
||||
I18n.t('Add a New User')
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import TimeZoneSelect from '../index'
|
||||
import {render} from '@testing-library/react'
|
||||
import isEqual from 'lodash/isEqual'
|
||||
|
||||
let liveRegion = null
|
||||
beforeAll(() => {
|
||||
if (!document.getElementById('flash_screenreader_holder')) {
|
||||
liveRegion = document.createElement('div')
|
||||
liveRegion.id = 'flash_screenreader_holder'
|
||||
liveRegion.setAttribute('role', 'alert')
|
||||
document.body.appendChild(liveRegion)
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
if (liveRegion) {
|
||||
liveRegion.remove()
|
||||
}
|
||||
})
|
||||
|
||||
const timezones = [
|
||||
{
|
||||
name: 'Central',
|
||||
localized_name: 'Central localized'
|
||||
},
|
||||
{
|
||||
name: 'Eastern',
|
||||
localized_name: 'Eastern localized'
|
||||
},
|
||||
{
|
||||
name: 'Mountain',
|
||||
localized_name: 'Mountain localized'
|
||||
},
|
||||
{
|
||||
name: 'Pacific',
|
||||
localized_name: 'Pacific localized'
|
||||
}
|
||||
]
|
||||
const priorityZones = [timezones[0]]
|
||||
|
||||
describe('TimeZoneSelect', () => {
|
||||
it('renders the value', () => {
|
||||
const {getByDisplayValue} = render(
|
||||
<TimeZoneSelect
|
||||
label="the label"
|
||||
onChange={() => {}}
|
||||
value="Mountain"
|
||||
timezones={timezones}
|
||||
priority_zones={priorityZones}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(getByDisplayValue('Mountain localized')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the right zone options', () => {
|
||||
const {getByText} = render(
|
||||
<TimeZoneSelect
|
||||
label="the label"
|
||||
onChange={() => {}}
|
||||
value="Mountain"
|
||||
timezones={timezones}
|
||||
priority_zones={priorityZones}
|
||||
/>
|
||||
)
|
||||
|
||||
// open the select dropdown
|
||||
const label = getByText('the label')
|
||||
label.click()
|
||||
|
||||
const priorityOptions = document.querySelectorAll(
|
||||
'[data-testid="Group:Common Timezones"] span[role="option"]'
|
||||
)
|
||||
isEqual(
|
||||
Array.from(priorityOptions).map(e => ({
|
||||
name: e.getAttribute('value'),
|
||||
localized_name: e.textContent
|
||||
})),
|
||||
priorityZones
|
||||
)
|
||||
|
||||
const allOptions = document.querySelectorAll(
|
||||
'[data-testid="Group:All Timezones"] span[role="option"]'
|
||||
)
|
||||
isEqual(
|
||||
Array.from(allOptions).map(e => ({
|
||||
name: e.getAttribute('value'),
|
||||
localized_name: e.textContent
|
||||
})),
|
||||
timezones
|
||||
)
|
||||
})
|
||||
|
||||
it('calls onChange on a selection', () => {
|
||||
const onChangeTZ = jest.fn()
|
||||
const {getByText} = render(
|
||||
<TimeZoneSelect
|
||||
label="the label"
|
||||
onChange={onChangeTZ}
|
||||
timezones={timezones}
|
||||
priority_zones={priorityZones}
|
||||
/>
|
||||
)
|
||||
|
||||
// open the select dropdown
|
||||
const label = getByText('the label')
|
||||
label.click()
|
||||
|
||||
const eastern = getByText('Eastern localized')
|
||||
eastern.click()
|
||||
|
||||
// onChange's event.target.value === onChanges's 2nd argument
|
||||
expect(onChangeTZ).toHaveBeenCalled()
|
||||
expect(onChangeTZ.mock.calls[0][0].target.value).toEqual('Eastern')
|
||||
expect(onChangeTZ.mock.calls[0][1]).toEqual('Eastern')
|
||||
})
|
||||
})
|
|
@ -18,31 +18,41 @@
|
|||
|
||||
import React from 'react'
|
||||
import {arrayOf, shape, string} from 'prop-types'
|
||||
import Select from '@instructure/ui-core/lib/components/Select'
|
||||
import CanvasSelect from '../CanvasSelect'
|
||||
import I18n from 'i18n!edit_timezone'
|
||||
|
||||
export default function TimeZoneSelect({
|
||||
label,
|
||||
timezones,
|
||||
priority_zones,
|
||||
onChange,
|
||||
...otherPropsToPassOnToSelect
|
||||
}) {
|
||||
let idval = 0 // for setting ids on options, which are necessary for Select's inner workings but don't matter to us
|
||||
|
||||
function onChangeTimezone(event, value) {
|
||||
event.persist()
|
||||
event.target.value = value // this is how our onChange expects the result
|
||||
onChange(event, value) // so it works either way, instui Select callback, or traditional
|
||||
}
|
||||
return (
|
||||
<Select {...otherPropsToPassOnToSelect} label={label}>
|
||||
<option value="" />
|
||||
<CanvasSelect label={label} onChange={onChangeTimezone} {...otherPropsToPassOnToSelect}>
|
||||
<CanvasSelect.Option id={`${++idval}`} value="">
|
||||
|
||||
</CanvasSelect.Option>
|
||||
{[
|
||||
{label: I18n.t('Common Timezones'), timezones: priority_zones},
|
||||
{label: I18n.t('All Timezones'), timezones}
|
||||
].map(({label, timezones}) => (
|
||||
<optgroup key={label} label={label}>
|
||||
{timezones.map(zone => (
|
||||
<option key={zone.name} value={zone.name}>
|
||||
].map(grouping => (
|
||||
<CanvasSelect.Group key={grouping.label} label={grouping.label}>
|
||||
{grouping.timezones.map(zone => (
|
||||
<CanvasSelect.Option id={`${++idval}`} key={zone.name} value={zone.name}>
|
||||
{zone.localized_name}
|
||||
</option>
|
||||
</CanvasSelect.Option>
|
||||
))}
|
||||
</optgroup>
|
||||
</CanvasSelect.Group>
|
||||
))}
|
||||
</Select>
|
||||
</CanvasSelect>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -52,14 +62,14 @@ const timezoneShape = shape({
|
|||
}).isRequired
|
||||
|
||||
TimeZoneSelect.propTypes = {
|
||||
...Select.propTypes, // this accepts any prop you'd pass to InstUI's Select. see it's docs for examples
|
||||
...CanvasSelect.propTypes, // this accepts any prop you'd pass to InstUI's Select. see it's docs for examples
|
||||
timezones: arrayOf(timezoneShape),
|
||||
priority_zones: arrayOf(timezoneShape)
|
||||
}
|
||||
|
||||
let defaultsJSON
|
||||
try {
|
||||
defaultsJSON = require(`./localized-timezone-lists/${ENV.LOCALE || 'en'}.json`)
|
||||
defaultsJSON = require(`./localized-timezone-lists/${ENV.LOCALE || 'en'}.json`) // eslint-disable-line import/no-dynamic-require
|
||||
} catch (e) {
|
||||
// fall back to english if a user has a locale set that we don't have a list for
|
||||
defaultsJSON = require(`./localized-timezone-lists/en.json`)
|
||||
|
|
|
@ -0,0 +1,244 @@
|
|||
/*
|
||||
* Copyright (C) 2019 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
*
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import '@instructure/ui-themes/lib/canvas'
|
||||
import React from 'react'
|
||||
import CanvasSelect from '../CanvasSelect'
|
||||
import {render} from '@testing-library/react'
|
||||
|
||||
function selectProps(override = {}) {
|
||||
return {
|
||||
id: 'sel1',
|
||||
label: 'Choose one',
|
||||
value: {undefined},
|
||||
onChange: () => {},
|
||||
...override
|
||||
}
|
||||
}
|
||||
|
||||
function selectOpts() {
|
||||
return [
|
||||
<CanvasSelect.Option key="1" id="1" value="one">
|
||||
One
|
||||
</CanvasSelect.Option>,
|
||||
<CanvasSelect.Option key="2" id="2" value="two">
|
||||
Two
|
||||
</CanvasSelect.Option>,
|
||||
<CanvasSelect.Option key="3" id="3" value="three">
|
||||
Three
|
||||
</CanvasSelect.Option>
|
||||
]
|
||||
}
|
||||
|
||||
function renderSelect(otherProps) {
|
||||
return render(<CanvasSelect {...selectProps(otherProps)}>{selectOpts()}</CanvasSelect>)
|
||||
}
|
||||
|
||||
let liveRegion = null
|
||||
beforeAll(() => {
|
||||
if (!document.getElementById('flash_screenreader_holder')) {
|
||||
liveRegion = document.createElement('div')
|
||||
liveRegion.id = 'flash_screenreader_holder'
|
||||
liveRegion.setAttribute('role', 'alert')
|
||||
document.body.appendChild(liveRegion)
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
if (liveRegion) {
|
||||
liveRegion.remove()
|
||||
}
|
||||
})
|
||||
|
||||
describe('CanvasSelect component', () => {
|
||||
it('renders', () => {
|
||||
const {getByText} = renderSelect()
|
||||
expect(getByText('Choose one')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows the selected option', () => {
|
||||
const {getByDisplayValue} = renderSelect({value: 'two'})
|
||||
expect(getByDisplayValue('Two')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('calls onChange when selection changes', () => {
|
||||
const handleChange = jest.fn()
|
||||
const {getByText} = renderSelect({onChange: handleChange})
|
||||
|
||||
const label = getByText('Choose one')
|
||||
label.click()
|
||||
|
||||
// the options list is open now
|
||||
const three = getByText('Three')
|
||||
three.click()
|
||||
|
||||
expect(handleChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('forwards the isDisabled prop', () => {
|
||||
const handleChange = jest.fn()
|
||||
const {getByText} = render(
|
||||
<CanvasSelect {...selectProps({onChange: handleChange})}>
|
||||
<CanvasSelect.Option key="1" id="1" value="one">
|
||||
One
|
||||
</CanvasSelect.Option>
|
||||
<CanvasSelect.Option key="2" id="2" value="two">
|
||||
Two
|
||||
</CanvasSelect.Option>
|
||||
<CanvasSelect.Option key="3" id="3" value="three" isDisabled>
|
||||
Three
|
||||
</CanvasSelect.Option>
|
||||
</CanvasSelect>
|
||||
)
|
||||
const label = getByText('Choose one')
|
||||
label.click()
|
||||
|
||||
// the options list is open now
|
||||
const three = getByText('Three')
|
||||
three.click()
|
||||
|
||||
expect(handleChange).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('filters out undefined options', () => {
|
||||
const {getByText} = render(
|
||||
<CanvasSelect {...selectProps()}>
|
||||
<CanvasSelect.Option key="1" id="1" value="one">
|
||||
One
|
||||
</CanvasSelect.Option>
|
||||
undefined
|
||||
<CanvasSelect.Option key="3" id="3" value="three" isDisabled>
|
||||
Three
|
||||
</CanvasSelect.Option>
|
||||
</CanvasSelect>
|
||||
)
|
||||
const label = getByText('Choose one')
|
||||
label.click()
|
||||
|
||||
expect(getByText('One')).toBeInTheDocument()
|
||||
expect(getByText('Three')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handles no children', () => {
|
||||
const {getByText} = render(
|
||||
<CanvasSelect {...selectProps({noOptionsLabel: 'No Options'})}></CanvasSelect>
|
||||
)
|
||||
const label = getByText('Choose one')
|
||||
label.click()
|
||||
|
||||
expect(getByText('No Options')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handles no options', () => {
|
||||
const {getByText} = render(
|
||||
<CanvasSelect {...selectProps({noOptionsLabel: 'No Options'})}>what is this?</CanvasSelect>
|
||||
)
|
||||
const label = getByText('Choose one')
|
||||
label.click()
|
||||
|
||||
expect(getByText('No Options')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
describe('CanvasSelectGroups', () => {
|
||||
it('renders enumerated groups and options', () => {
|
||||
const {getByText} = render(
|
||||
<CanvasSelect {...selectProps()}>
|
||||
<CanvasSelect.Group id="1" label="Group A">
|
||||
<CanvasSelect.Option id="1" value="one">
|
||||
One
|
||||
</CanvasSelect.Option>
|
||||
<CanvasSelect.Option id="2" value="two">
|
||||
Two
|
||||
</CanvasSelect.Option>
|
||||
<CanvasSelect.Option id="3" value="three">
|
||||
Three
|
||||
</CanvasSelect.Option>
|
||||
</CanvasSelect.Group>
|
||||
<CanvasSelect.Group id="2" label="Group B">
|
||||
<CanvasSelect.Option id="4" value="four">
|
||||
Four
|
||||
</CanvasSelect.Option>
|
||||
<CanvasSelect.Option id="5" value="five">
|
||||
Five
|
||||
</CanvasSelect.Option>
|
||||
<CanvasSelect.Option id="6" value="siz">
|
||||
Six
|
||||
</CanvasSelect.Option>
|
||||
</CanvasSelect.Group>
|
||||
</CanvasSelect>
|
||||
)
|
||||
expect(getByText('Choose one')).toBeInTheDocument()
|
||||
const label = getByText('Choose one')
|
||||
label.click()
|
||||
|
||||
expect(getByText('Group A')).toBeInTheDocument()
|
||||
expect(getByText('One')).toBeInTheDocument()
|
||||
expect(getByText('Group B')).toBeInTheDocument()
|
||||
expect(getByText('Four')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('renders generated groups and options', () => {
|
||||
const data = [
|
||||
{
|
||||
label: 'Group A',
|
||||
items: [
|
||||
{id: '1', value: 'one', label: 'One'},
|
||||
{id: '2', value: 'two', label: 'Two'},
|
||||
{id: '3', value: 'three', label: 'Three'}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Group B',
|
||||
items: [
|
||||
{id: '4', value: 'four', label: 'Four'},
|
||||
{id: '5', value: 'five', label: 'Five'},
|
||||
{id: '6', value: 'siz', label: 'Six'}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
let k = 0
|
||||
const {getByText} = render(
|
||||
<CanvasSelect {...selectProps()}>
|
||||
<CanvasSelect.Option id="0" value="0">
|
||||
Zero
|
||||
</CanvasSelect.Option>
|
||||
{data.map(grp => (
|
||||
<CanvasSelect.Group key={`${++k}`} label={grp.label}>
|
||||
{grp.items.map(opt => (
|
||||
<CanvasSelect.Option key={`${++k}`} id={opt.id} value={opt.value}>
|
||||
{opt.label}
|
||||
</CanvasSelect.Option>
|
||||
))}
|
||||
</CanvasSelect.Group>
|
||||
))}
|
||||
</CanvasSelect>
|
||||
)
|
||||
|
||||
expect(getByText('Choose one')).toBeInTheDocument()
|
||||
const label = getByText('Choose one')
|
||||
label.click()
|
||||
|
||||
expect(getByText('Group A')).toBeInTheDocument()
|
||||
expect(getByText('One')).toBeInTheDocument()
|
||||
expect(getByText('Group B')).toBeInTheDocument()
|
||||
expect(getByText('Four')).toBeInTheDocument()
|
||||
})
|
||||
})
|
|
@ -40,6 +40,7 @@
|
|||
"@instructure/ui-number-input": "^5",
|
||||
"@instructure/ui-overlays": "^5",
|
||||
"@instructure/ui-pagination": "^5",
|
||||
"@instructure/ui-select": "^6.8.1",
|
||||
"@instructure/ui-svg-images": "^5",
|
||||
"@instructure/ui-table": "^5",
|
||||
"@instructure/ui-tabs": "^5",
|
||||
|
@ -122,6 +123,7 @@
|
|||
"@sentry/webpack-plugin": "^1.5.2",
|
||||
"@sheerun/mutationobserver-shim": "0.3.2",
|
||||
"@testing-library/dom": "^5",
|
||||
"@testing-library/jest-dom": "^4",
|
||||
"@testing-library/react": "^8.0.5",
|
||||
"@yarnpkg/lockfile": "^1.0.2",
|
||||
"axe-core": "~2.1.7",
|
||||
|
@ -179,7 +181,6 @@
|
|||
"jest": "^24",
|
||||
"jest-canvas-mock": "^1",
|
||||
"jest-config": "^24",
|
||||
"@testing-library/jest-dom": "^4",
|
||||
"jest-fetch-mock": "^2.1.2",
|
||||
"jest-junit": "^6",
|
||||
"jest-localstorage-mock": "^2",
|
||||
|
|
|
@ -23,8 +23,8 @@ import PeopleSearch from 'jsx/add_people/components/people_search'
|
|||
QUnit.module('PeopleSearch')
|
||||
|
||||
const searchProps = {
|
||||
roles: [{id: '1', name: 'Student'}, {id: '2', name: 'TA'}],
|
||||
sections: [{id: '1', name: 'section 2'}, {id: '2', name: 'section 10'}],
|
||||
roles: [{id: '1', label: 'Student'}, {id: '2', label: 'TA'}],
|
||||
sections: [{id: '1', name: 'Section 2'}, {id: '2', name: 'Section 10'}],
|
||||
section: '1',
|
||||
role: '2',
|
||||
limitPrivilege: true,
|
||||
|
@ -51,11 +51,10 @@ test('sets the correct values', () => {
|
|||
equal(loginRadio.checked, true, 'login id radio button is checked')
|
||||
const nameInput = peopleSearch.querySelector('textarea')
|
||||
equal(nameInput.value, searchProps.nameList, 'names are in the textarea')
|
||||
const selects = peopleSearch.querySelectorAll('.peoplesearch__selections select')
|
||||
equal(selects[0].value, '2', 'role 2 is selected')
|
||||
equal(selects[1].value, '1', 'section 1 is selected')
|
||||
const sections = Array.prototype.map.call(selects[1].options, o => o.innerHTML)
|
||||
deepEqual(sections, ['section 2', 'section 10'], 'sections are sorted by name')
|
||||
const roleSelect = peopleSearch.querySelector('#peoplesearch_select_role')
|
||||
equal(roleSelect.value, 'TA', 'correct role is selected')
|
||||
const sectionSelect = peopleSearch.querySelector('#peoplesearch_select_section')
|
||||
equal(sectionSelect.value, 'Section 2', 'correct section is selected')
|
||||
const limitPrivilegeCheckbox = peopleSearch.querySelector('#limit_privileges_to_course_section')
|
||||
equal(limitPrivilegeCheckbox.checked, true, 'limit privileges checkbox is checked')
|
||||
})
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2015 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import {shallow} from 'enzyme'
|
||||
import TimeZoneSelect from 'jsx/shared/components/TimeZoneSelect'
|
||||
|
||||
QUnit.module('TimeZoneSelect')
|
||||
|
||||
test('renders the right zones', () => {
|
||||
const timezones = [
|
||||
{
|
||||
name: 'Central',
|
||||
localized_name: 'Central localized'
|
||||
},
|
||||
{
|
||||
name: 'Eastern',
|
||||
localized_name: 'Eastern localized'
|
||||
},
|
||||
{
|
||||
name: 'Mountain',
|
||||
localized_name: 'Mountain localized'
|
||||
},
|
||||
{
|
||||
name: 'Pacific',
|
||||
localized_name: 'Pacific localized'
|
||||
}
|
||||
]
|
||||
const priorityZones = [timezones[0]]
|
||||
|
||||
const wrapper = shallow(<TimeZoneSelect timezones={timezones} priority_zones={priorityZones} />)
|
||||
|
||||
const prorityOptions = wrapper.find('optgroup[label="Common Timezones"] option')
|
||||
deepEqual(
|
||||
prorityOptions.map(e => ({name: e.prop('value'), localized_name: e.text()})),
|
||||
priorityZones
|
||||
)
|
||||
|
||||
const allOptions = wrapper.find('optgroup[label="All Timezones"] option')
|
||||
deepEqual(
|
||||
allOptions.map(e => ({name: e.prop('value'), localized_name: e.text()})),
|
||||
timezones
|
||||
)
|
||||
})
|
|
@ -122,7 +122,7 @@ describe "add_people" do
|
|||
:enabled => false
|
||||
get "/courses/#{@course.id}/users"
|
||||
f('#addUsers').click
|
||||
expect(ff('#peoplesearch_select_role option').map(&:text)).not_to include 'Student'
|
||||
expect(INSTUI_Select_options('#peoplesearch_select_role').map(&:text)).not_to include 'Student'
|
||||
end
|
||||
|
||||
# CNVS-34781
|
||||
|
@ -152,6 +152,28 @@ describe "add_people" do
|
|||
|
||||
end
|
||||
|
||||
# tests that INSTUI fixed a bug in Select that would close the Modal
|
||||
# when the user uses 'esc' to close the options dropdown
|
||||
it "should not close the modal on 'escape'ing from role Select options", ignore_js_errors: true do
|
||||
get "/courses/#{@course.id}/users"
|
||||
|
||||
# open the add people modal dialog
|
||||
f('a#addUsers').click
|
||||
expect(f(".addpeople")).to be_displayed
|
||||
|
||||
# expand the roles
|
||||
roleselect = f('#peoplesearch_select_role')
|
||||
roleselect.click
|
||||
option_list_id = roleselect.attribute('aria-controls')
|
||||
expect(f('body')).to contain_css("##{option_list_id}")
|
||||
|
||||
optionList = f("##{option_list_id}")
|
||||
roleselect.send_keys(:escape)
|
||||
expect(f('body')).not_to contain_css("##{option_list_id}")
|
||||
|
||||
expect(f(".addpeople")).to be_displayed # still
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context('as an admin') do
|
||||
|
|
|
@ -29,10 +29,10 @@ module NewCourseAddPeopleModal
|
|||
end
|
||||
|
||||
def role_options
|
||||
ff('#peoplesearch_select_role option', add_people_modal).map(&:text)
|
||||
INSTUI_Select_options('#peoplesearch_select_role').map(&:text)
|
||||
end
|
||||
|
||||
def section_options
|
||||
ff('#peoplesearch_select_section option', add_people_modal).map(&:text)
|
||||
INSTUI_Select_options('#peoplesearch_select_section').map(&:text)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -37,8 +37,8 @@ describe "course people" do
|
|||
add_button.click
|
||||
wait_for_ajaximations
|
||||
|
||||
click_option("#peoplesearch_select_role", type)
|
||||
click_option("#peoplesearch_select_section", section_name) if section_name
|
||||
click_INSTUI_Select_option("#peoplesearch_select_role", type)
|
||||
click_INSTUI_Select_option("#peoplesearch_select_section", section_name) if section_name
|
||||
replace_content(f(".addpeople__peoplesearch textarea"), email)
|
||||
|
||||
f("#addpeople_next").click
|
||||
|
@ -356,7 +356,7 @@ describe "course people" do
|
|||
add_button = f('#addUsers')
|
||||
keep_trying_until { expect(add_button).to be_displayed }
|
||||
add_button.click
|
||||
click_option('#role_id', type)
|
||||
click_INSTUI_Select_option('#role_id', type)
|
||||
end
|
||||
|
||||
%w[student teacher ta designer observer].each do |base_type|
|
||||
|
|
|
@ -425,8 +425,8 @@ describe "people" do
|
|||
|
||||
expect(f(".addpeople")).to be_displayed
|
||||
replace_content(f(".addpeople__peoplesearch textarea"),'student@example.com')
|
||||
click_option('#peoplesearch_select_role', ta_role.id.to_s, :value)
|
||||
click_option('#peoplesearch_select_section', 'Unnamed Course', :text)
|
||||
click_INSTUI_Select_option('#peoplesearch_select_role', ta_role.id.to_s, :value)
|
||||
click_INSTUI_Select_option('#peoplesearch_select_section', 'Unnamed Course', :text)
|
||||
f('#addpeople_next').click
|
||||
wait_for_ajaximations
|
||||
|
||||
|
@ -434,11 +434,11 @@ describe "people" do
|
|||
f('#addpeople_back').click
|
||||
wait_for_ajaximations
|
||||
|
||||
#verify form and options have not changed
|
||||
# verify form and options have not changed
|
||||
expect(f('.addpeople__peoplesearch')).to be_displayed
|
||||
expect(f('.addpeople__peoplesearch textarea').text).to eq 'student@example.com'
|
||||
expect(first_selected_option(f('#peoplesearch_select_role')).text).to eq 'TA'
|
||||
expect(first_selected_option(f('#peoplesearch_select_section')).text).to eq 'Unnamed Course'
|
||||
expect(f('#peoplesearch_select_role').attribute('value')).to eq 'TA'
|
||||
expect(f('#peoplesearch_select_section').attribute('value')).to eq 'Unnamed Course'
|
||||
end
|
||||
|
||||
it "should add a student to a section", priority: "1", test_id: 296460 do
|
||||
|
@ -458,16 +458,16 @@ describe "people" do
|
|||
end
|
||||
|
||||
it "should remove a student from a section", priority: "1", test_id: 296461 do
|
||||
@student = user_factory
|
||||
@course.enroll_student(@student, allow_multiple_enrollments: true)
|
||||
@course.enroll_student(@student, section: @section2, allow_multiple_enrollments: true)
|
||||
get "/courses/#{@course.id}/users"
|
||||
f(".StudentEnrollment .icon-more").click
|
||||
fln("Edit Sections").click
|
||||
fln("Remove user from section2").click
|
||||
ff('.ui-button-text')[1].click
|
||||
wait_for_ajaximations
|
||||
expect(ff(".StudentEnrollment")[0]).not_to include_text("section2")
|
||||
@student = user_factory
|
||||
@course.enroll_student(@student, allow_multiple_enrollments: true)
|
||||
@course.enroll_student(@student, section: @section2, allow_multiple_enrollments: true)
|
||||
get "/courses/#{@course.id}/users"
|
||||
f(".StudentEnrollment .icon-more").click
|
||||
fln("Edit Sections").click
|
||||
fln("Remove user from section2").click
|
||||
ff('.ui-button-text')[1].click
|
||||
wait_for_ajaximations
|
||||
expect(ff(".StudentEnrollment")[0]).not_to include_text("section2")
|
||||
end
|
||||
|
||||
it "should edit a designer's sections" do
|
||||
|
|
|
@ -415,6 +415,26 @@ module CustomSeleniumActions
|
|||
select.select_by(select_by, option_text)
|
||||
end
|
||||
|
||||
# implementation of click_option for use with INSTU's Select
|
||||
# (tested with the CanvasSelect wrapper, untested with a raw instui Select)
|
||||
def click_INSTUI_Select_option(select_css, option_text, select_by = :text)
|
||||
cselect = fj(select_css)
|
||||
cselect.click # open the options list
|
||||
option_list_id = cselect.attribute('aria-controls')
|
||||
if select_by == :text
|
||||
fj("##{option_list_id} [role='option']:contains(#{option_text})").click
|
||||
else
|
||||
f("##{option_list_id} [role='option'][#{select_by}='#{option_text}']").click
|
||||
end
|
||||
end
|
||||
|
||||
def INSTUI_Select_options(select_css)
|
||||
cselect = fj(select_css)
|
||||
cselect.click # open the options list
|
||||
option_list_id = cselect.attribute('aria-controls')
|
||||
ff("##{option_list_id} [role='option']")
|
||||
end
|
||||
|
||||
def close_visible_dialog
|
||||
visible_dialog_element = fj('.ui-dialog:visible')
|
||||
visible_dialog_element.find_element(:css, '.ui-dialog-titlebar-close').click
|
||||
|
|
61
yarn.lock
61
yarn.lock
|
@ -2004,6 +2004,22 @@
|
|||
keycode "^2"
|
||||
prop-types "^15"
|
||||
|
||||
"@instructure/ui-options@^6.8.1":
|
||||
version "6.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@instructure/ui-options/-/ui-options-6.8.1.tgz#c212f0c4819c146b095fe9b9f1e350399c45606b"
|
||||
integrity sha512-KrT7S8wIrZXeyjHfMJu/dzR8EzKqirHCvT550Cltse57/JR1tZcM8fm0ZUwV+buUWE4HwmbYKZR/6OzVR3gong==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7"
|
||||
"@instructure/ui-icons" "^6.8.1"
|
||||
"@instructure/ui-layout" "^6.8.1"
|
||||
"@instructure/ui-prop-types" "^6.8.1"
|
||||
"@instructure/ui-react-utils" "^6.8.1"
|
||||
"@instructure/ui-testable" "^6.8.1"
|
||||
"@instructure/ui-themeable" "^6.8.1"
|
||||
classnames "^2"
|
||||
prop-types "^15"
|
||||
react "^15 || ^16"
|
||||
|
||||
"@instructure/ui-overlays@^5", "@instructure/ui-overlays@^5.52.0", "@instructure/ui-overlays@^5.52.3":
|
||||
version "5.52.3"
|
||||
resolved "https://registry.yarnpkg.com/@instructure/ui-overlays/-/ui-overlays-5.52.3.tgz#1a1c863abc2de2b787cd9f0101b2c956d4694b80"
|
||||
|
@ -2190,6 +2206,47 @@
|
|||
"@instructure/ui-utils" "^6.8.1"
|
||||
prop-types "^15"
|
||||
|
||||
"@instructure/ui-select@^6.8.1":
|
||||
version "6.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@instructure/ui-select/-/ui-select-6.8.1.tgz#0ad5086afdbadd9dde6723774c447400e5a90b74"
|
||||
integrity sha512-b0af6oatUGrgFLUoL8T02Xdq/eLr2p3/S1lVHr0Vr2IR54ZBEi+lJ0sleAtksdhEGqb1Hkvu1ozoLDSsImUj5Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7"
|
||||
"@instructure/ui-dom-utils" "^6.8.1"
|
||||
"@instructure/ui-elements" "^6.8.1"
|
||||
"@instructure/ui-form-field" "^6.8.1"
|
||||
"@instructure/ui-icons" "^6.8.1"
|
||||
"@instructure/ui-layout" "^6.8.1"
|
||||
"@instructure/ui-options" "^6.8.1"
|
||||
"@instructure/ui-overlays" "^6.8.1"
|
||||
"@instructure/ui-prop-types" "^6.8.1"
|
||||
"@instructure/ui-react-utils" "^6.8.1"
|
||||
"@instructure/ui-selectable" "^6.8.1"
|
||||
"@instructure/ui-testable" "^6.8.1"
|
||||
"@instructure/ui-text-input" "^6.8.1"
|
||||
"@instructure/ui-themeable" "^6.8.1"
|
||||
"@instructure/ui-utils" "^6.8.1"
|
||||
"@instructure/uid" "^6.8.1"
|
||||
classnames "^2"
|
||||
prop-types "^15"
|
||||
react "^15 || ^16"
|
||||
|
||||
"@instructure/ui-selectable@^6.8.1":
|
||||
version "6.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@instructure/ui-selectable/-/ui-selectable-6.8.1.tgz#ba2813cfac1efe39d6f8fc7f2ded250d01fd39e8"
|
||||
integrity sha512-WtlQihh/UiUt+4GjkHKEYN+muAMG8gFexb1kzellPb4C3dHl0fSfjxO4JlRQW4GWqUyDJeCjYjwemmYpPn2+ZQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7"
|
||||
"@instructure/console" "^6.8.1"
|
||||
"@instructure/ui-dom-utils" "^6.8.1"
|
||||
"@instructure/ui-testable" "^6.8.1"
|
||||
"@instructure/ui-themeable" "^6.8.1"
|
||||
"@instructure/ui-utils" "^6.8.1"
|
||||
"@instructure/uid" "^6.8.1"
|
||||
keycode "^2"
|
||||
prop-types "^15"
|
||||
react "^15 || ^16"
|
||||
|
||||
"@instructure/ui-stylesheet@^5.52.3":
|
||||
version "5.52.3"
|
||||
resolved "https://registry.yarnpkg.com/@instructure/ui-stylesheet/-/ui-stylesheet-5.52.3.tgz#4f35efb7ed6ceb3442a5efbf9ce80d9916e46322"
|
||||
|
@ -11676,7 +11733,7 @@ iterall@^1.1.3, iterall@^1.2.2:
|
|||
resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7"
|
||||
integrity sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA==
|
||||
|
||||
jasmine-core@^2.2.0:
|
||||
jasmine-core@2.6.4, jasmine-core@^2.2.0:
|
||||
version "2.6.4"
|
||||
resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.6.4.tgz#dec926cd0a9fa287fb6db5c755fa487e74cecac5"
|
||||
integrity sha1-3skmzQqfoof7bbXHVfpIfnTOysU=
|
||||
|
@ -14290,7 +14347,7 @@ node-releases@^1.1.25:
|
|||
dependencies:
|
||||
semver "^5.3.0"
|
||||
|
||||
node-sass@^4.5.0, node-sass@^4.7.2:
|
||||
node-sass@4.7.2, node-sass@^4.5.0, node-sass@^4.7.2:
|
||||
version "4.7.2"
|
||||
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.7.2.tgz#9366778ba1469eb01438a9e8592f4262bcb6794e"
|
||||
integrity sha512-CaV+wLqZ7//Jdom5aUFCpGNoECd7BbNhjuwdsX/LkXBrHl8eb1Wjw4HvWqcFvhr5KuNgAk8i/myf/MQ1YYeroA==
|
||||
|
|
Loading…
Reference in New Issue