mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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:
committed by
Dan Abramov
parent
f6ceacdd44
commit
80411ea9b4
@@ -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>;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user