Default to first non-disabled option for select elements (#10456)

* Default to first non-disabled option for select

Instead of defaulting to the first option for a select element, which could be a disabled option, we find the first non-disabled option available while we looping through the options. If no option matches, set the first non-disabled option as selected.

* Add ReactDOMSelect test for defaulting to non-disabled options

* Add test fixtures to cover disabled selected options

* Fix bad merge
This commit is contained in:
Brandon Dail
2017-09-01 13:34:28 -07:00
committed by Dan Abramov
parent f6ceacdd44
commit 80411ea9b4
6 changed files with 138 additions and 39 deletions
+26
View File
@@ -0,0 +1,26 @@
const React = window.React;
function csv(string) {
return string.split(/\s*,\s*/);
}
export default function IssueList({issues}) {
if (!issues) {
return null;
}
if (typeof issues === 'string') {
issues = csv(issues);
}
let links = issues.reduce((memo, issue, i) => {
return memo.concat(
i > 0 && i < issues.length ? ', ' : null,
<a href={'https://github.com/facebook/react/issues/' + issue} key={issue}>
{issue}
</a>
);
}, []);
return <span>{links}</span>;
}
+7 -1
View File
@@ -1,10 +1,12 @@
import cn from 'classnames';
import semver from 'semver';
import React from 'react';
import PropTypes from 'prop-types';
import IssueList from './IssueList';
import {parse} from 'query-string';
import {semverString} from './propTypes';
const React = window.React;
const propTypes = {
children: PropTypes.node.isRequired,
title: PropTypes.node.isRequired,
@@ -36,6 +38,7 @@ class TestCase extends React.Component {
resolvedIn,
resolvedBy,
affectedBrowsers,
relatedIssues,
children,
} = this.props;
@@ -93,6 +96,9 @@ class TestCase extends React.Component {
{affectedBrowsers && <dt>Affected browsers: </dt>}
{affectedBrowsers && <dd>{affectedBrowsers}</dd>}
{relatedIssues && <dt>Related Issues: </dt>}
{relatedIssues && <dd><IssueList issues={relatedIssues} /></dd>}
</dl>
<p className="test-case__desc">
@@ -1,3 +1,6 @@
import FixtureSet from '../../FixtureSet';
import TestCase from '../../TestCase';
const React = window.React;
const ReactDOM = window.ReactDOM;
@@ -31,35 +34,73 @@ class SelectFixture extends React.Component {
render() {
return (
<form>
<fieldset>
<legend>Controlled</legend>
<select value={this.state.value} onChange={this.onChange}>
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
</select>
<span className="hint">Value: {this.state.value}</span>
</fieldset>
<fieldset>
<legend>Uncontrolled</legend>
<select defaultValue="">
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
</select>
<span className="hint" />
</fieldset>
<fieldset>
<legend>Controlled in nested subtree</legend>
<div ref={node => (this._nestedDOMNode = node)} />
<span className="hint">
This should synchronize in both direction with the one above.
</span>
</fieldset>
</form>
<FixtureSet title="Selects" description="">
<form className="field-group">
<fieldset>
<legend>Controlled</legend>
<select value={this.state.value} onChange={this.onChange}>
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
</select>
<span className="hint">Value: {this.state.value}</span>
</fieldset>
<fieldset>
<legend>Uncontrolled</legend>
<select defaultValue="">
<option value="">Select a color</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
</select>
</fieldset>
<fieldset>
<legend>Controlled in nested subtree</legend>
<div ref={node => (this._nestedDOMNode = node)} />
<span className="hint">
This should synchronize in both direction with the "Controlled".
</span>
</fieldset>
</form>
<TestCase title="A selected disabled option" relatedIssues="2803">
<TestCase.Steps>
<li>Open the select</li>
<li>Select "1"</li>
<li>Attempt to reselect "Please select an item"</li>
</TestCase.Steps>
<TestCase.ExpectedResult>
The initial picked option should be "Please select an
item", however it should not be a selectable option.
</TestCase.ExpectedResult>
<div className="test-fixture">
<select defaultValue="">
<option value="" disabled>Please select an item</option>
<option>0</option>
<option>1</option>
<option>2</option>
</select>
</div>
</TestCase>
<TestCase title="An unselected disabled option" relatedIssues="2803">
<TestCase.ExpectedResult>
The initial picked option value should "0": the first non-disabled option.
</TestCase.ExpectedResult>
<div className="test-fixture">
<select defaultValue="">
<option disabled>Please select an item</option>
<option>0</option>
<option>1</option>
<option>2</option>
</select>
</div>
</TestCase>
</FixtureSet>
);
}
}
+13 -7
View File
@@ -8,23 +8,20 @@ html {
font-size: 10px;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
font-size: 1.4rem;
margin: 0;
padding: 0;
}
select {
width: 12rem;
}
button {
margin: 10px;
font-size: 18px;
padding: 5px;
}
.header {
background: #222;
box-shadow: inset 0 -1px 3px #000;
@@ -34,6 +31,10 @@ button {
padding: .8rem 1.6rem;
}
.header select {
width: 12rem;
}
.header__inner {
display: table;
margin: 0 auto;
@@ -101,7 +102,8 @@ fieldset {
overflow: hidden;
}
ul, ol {
ul,
ol {
margin: 0 0 2rem 0;
}
@@ -212,3 +214,7 @@ li {
background-color: #f4f4f4;
border-top: 1px solid #d9d9d9;
}
.field-group {
overflow: hidden;
}
@@ -101,14 +101,18 @@ function updateOptions(
// Do not set `select.value` as exact behavior isn't consistent across all
// browsers for all cases.
let selectedValue = '' + (propValue: string);
let defaultSelected = null;
for (let i = 0; i < options.length; i++) {
if (options[i].value === selectedValue) {
options[i].selected = true;
return;
}
if (defaultSelected === null && !options[i].disabled) {
defaultSelected = options[i];
}
}
if (options.length) {
options[0].selected = true;
if (defaultSelected !== null) {
defaultSelected.selected = true;
}
}
}
@@ -128,6 +128,22 @@ describe('ReactDOMSelect', () => {
expect(node.value).toEqual('gorilla');
});
it('should default to the first non-disabled option', () => {
var stub = (
<select defaultValue="">
<option disabled={true}>Disabled</option>
<option disabled={true}>Still Disabled</option>
<option>0</option>
<option disabled={true}>Also Disabled</option>
</select>
);
var container = document.createElement('div');
stub = ReactDOM.render(stub, container);
var node = ReactDOM.findDOMNode(stub);
expect(node.options[0].selected).toBe(false);
expect(node.options[2].selected).toBe(true);
});
it('should allow setting `value` to __proto__', () => {
var stub = (
<select value="__proto__" onChange={noop}>