Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make sure that select has multiple attribute set to appropriate state before appending options #13270

Merged
merged 7 commits into from Aug 3, 2018

Conversation

segoddnja
Copy link

@segoddnja segoddnja commented Jul 25, 2018

Applying multiple options to select node before it's multiple attribute is set to true leads to the first option be selected by default, event in the case there is no value or defaultValue passed in the props.

It leads to another undesired outcome. As setting real selected attributes values happens in the post mount wrapper, browser does not scroll list to selected option if it is outside of the select.

fixes #13222

@pull-bot
Copy link

pull-bot commented Jul 25, 2018

ReactDOM: size: 0.0%, gzip: 0.0%

Details of bundled changes.

Comparing: b565f49...afedb68

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js +0.1% +0.1% 643 KB 643.51 KB 151 KB 151.17 KB UMD_DEV
react-dom.production.min.js 0.0% 0.0% 96.27 KB 96.31 KB 31.21 KB 31.22 KB UMD_PROD
react-dom.development.js +0.1% +0.1% 639.14 KB 639.65 KB 149.83 KB 150 KB NODE_DEV
react-dom.production.min.js 0.0% 0.0% 96.26 KB 96.3 KB 30.74 KB 30.75 KB NODE_PROD
ReactDOM-dev.js +0.1% +0.1% 646.35 KB 646.86 KB 148.13 KB 148.3 KB FB_WWW_DEV
ReactDOM-prod.js 🔺+0.1% 0.0% 278.1 KB 278.29 KB 52.2 KB 52.22 KB FB_WWW_PROD
react-dom.profiling.min.js 0.0% 0.0% 97.39 KB 97.44 KB 31.14 KB 31.15 KB NODE_PROFILING
ReactDOM-profiling.js +0.1% 0.0% 281.39 KB 281.58 KB 52.92 KB 52.95 KB FB_WWW_PROFILING

Generated by 🚫 dangerJS

@nhunzaker
Copy link
Contributor

Awesome. Thank you for getting to the bottom of this!

I'm looking into whether or not this is the best place to ensure multiple is set on a select before options are added. In the meantime, I'd like to figure out how to get test coverage on this.

Could you check to see if JSDOM sets the scroll position on multi-selects? If so, I'd like to add a test for this here:
https://github.com/facebook/react/blob/master/packages/react-dom/src/__tests__/ReactDOMSelect-test.js

Otherwise, I'd like to get manual dom test fixture for this. These live here:
https://github.com/facebook/react/tree/master/fixtures/dom

And we should add a test case to this module:
https://github.com/facebook/react/blob/master/fixtures/dom/src/components/fixtures/selects/index.js

Please let me know if you hit a snag, I'd be happy to provide additional assistance!

@segoddnja
Copy link
Author

@nhunzaker No problem. I think there is really not so much options where to ensure that select has appropriate multiple value. It creates instance by createInstance method call and appends children right after this. So createInstance and createElement only two places where is possible to do this.

Could you check to see if JSDOM sets the scroll position on multi-selects?

I don't think it is possible to do with JSDOM. And even if it had been possible it would have been tested only against JSDOM. Do not see much sense in this.

I've added manual dom fixture.

@@ -158,6 +158,25 @@ class SelectFixture extends React.Component {
</form>
</div>
</TestCase>

<TestCase title="A multiple selec being scrolled to first selected option">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: selec

@gaearon
Copy link
Collaborator

gaearon commented Aug 1, 2018

Can this be colocated with other ReactDOMFiberSelect logic?

@segoddnja segoddnja force-pushed the master branch 2 times, most recently from 473b52f to d7c57e5 Compare August 1, 2018 21:50
@segoddnja
Copy link
Author

Can this be colocated with other ReactDOMFiberSelect logic?

I don't think so. At least I don't see a way how to do this.

@gaearon
Copy link
Collaborator

gaearon commented Aug 1, 2018

Can you explain why? I’m curious why we usually adjust properties later but here we have to do it right after creation. What’s special?

@nhunzaker
Copy link
Contributor

nhunzaker commented Aug 1, 2018

Just to recap, the issue is that the multiple attribute needs to be set before the option children elements are appended. Otherwise the first item is considered selected and the scroll position of the multiselect isn't updated when it transitions to multi-mode.

It looks like we were actually doing this in the postMountHook:
https://github.com/facebook/react/blob/master/packages/react-dom/src/client/ReactDOMFiberSelect.js#L174-L183

But, by this point, the option items are already appended to the select. This is a change in the order of operations from React 15 to React 16 (children are now appended before attributes are added):

image

What if we made a createHook, and switched on the tag, similar to postMountHook?

That could look like:

export function createElement(
  type: string,
  props: Object,
  rootContainerElement: Element | Document,
  parentNamespace: string,) {
  // ...
  switch (type) {
    case 'select'
      ReactDOMFiberSelect.createHook(props);
      break;
  }
}

Though for this case, since it's the only element so far that has an issue, maybe we could just use an if

@gaearon
Copy link
Collaborator

gaearon commented Aug 1, 2018

the multiple attribute needs to be set before the option children elements are appended.

The comment should say this.

Copy link
Collaborator

@gaearon gaearon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks all right to me. Posted some nits.

@nhunzaker Please feel free to merge once they’re addressed.

// Make sure that `select` has `multiple` attribute set to appropriate state before appending options
// To prevent first option be initialy made selected
// see more details in https://github.com/facebook/react/issues/13222
if (type === 'select' && !!props.multiple) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this context !! is redundant. You can drop it.

@@ -378,6 +378,12 @@ export function createElement(
// See discussion in https://github.com/facebook/react/pull/6896
// and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
domElement = ownerDocument.createElement(type);
// Make sure that `select` has `multiple` attribute set to appropriate state before appending options
// To prevent first option be initialy made selected
// see more details in https://github.com/facebook/react/issues/13222
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please change the comment to the suggestion in the discussion thread

@@ -378,6 +378,12 @@ export function createElement(
// See discussion in https://github.com/facebook/react/pull/6896
// and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
domElement = ownerDocument.createElement(type);
// Make sure that `select` has `multiple` attribute set to appropriate state before appending options
// To prevent first option be initialy made selected
// see more details in https://github.com/facebook/react/issues/13222
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't perfect, but could you update this comment to read like:

// Normally attributes are assigned in `setInitialDOMProperties`, however the `multiple`
// attribute on `select`s needs to be added before `option`s are inserted. This prevents 
// a bug where the `select` does not scroll to the correct option because singular 
// `select` elements automatically pick the first item.
// See https://github.com/facebook/react/issues/13222

@gaearon Could this be clearer?

// To prevent first option be initialy made selected
// see more details in https://github.com/facebook/react/issues/13222
if (type === 'select' && !!props.multiple) {
domElement.setAttribute('multiple', 'true');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use property assignment in the postMountWrapper, could you do the same here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

postMountWrapper attempts to update options, but as at this moment select doesn't have any child appended, it makes no sense, it will iterate over empty options collection. However it should not cause any problems so if you think it is better approach I can do this change.

@gaearon
Copy link
Collaborator

gaearon commented Aug 1, 2018

What if props.multiple changes during an update, but we insert a child into it before the change? Is that also going to cause the same issue?

remove redundant conversion to bool type
@aweary
Copy link
Contributor

aweary commented Aug 2, 2018

Could we consider changing the order back to match React 15? I brought this up a while back but there was no follow up on it. Was reordering these operations intentional?

@segoddnja
Copy link
Author

@gaearon Google chrome scrolls to selected option only once.

const s = document.createElement('select');
s.multiple = true;
for(let i = 0; i < 10; i++) {
  const o = document.createElement('option');
  o.text = i;
  o.value = i;
  s.appendChild(o);
}
//Scroll down to selected option
s.options[9].selected = true;
//Do not scroll up to selected option
s.options[9].selected = false;
s.options[0].selected = true;

So any further changes won't change scroll position.

@gaearon
Copy link
Collaborator

gaearon commented Aug 2, 2018

Could we consider changing the order back to match React 15? I brought this up a while back but there was no follow up on it. Was reordering these operations intentional?

Reordering was done in ea34204 as far as I can see. Ironically it was partially done because of <select>:

Finally, we need the ability to update an instance after all the children
have been insertion. Such as <select value={...} />. I called this
finalizeInitialChildren.

That said we can probably split setting initial properties in two phases as well (before and after appending the children). Do we have a proposal for which ones should go where?

@segoddnja
Copy link
Author

I do not know about other cases when it is necessary to assign properties before appending the children. As it turned out this issue (as well as solution) was known long time ago. So, probably there are some other related issues in the list.

@gaearon
Copy link
Collaborator

gaearon commented Aug 2, 2018

What about the suggested fix in #12242 (comment)? Would that also solve the issue?

@segoddnja
Copy link
Author

Well, it solves only issue described in the pull request title. But, this change doesn't fix the scrolling issue.

@gaearon
Copy link
Collaborator

gaearon commented Aug 2, 2018

Okay. That sounds good then. Can you please fix conflicts/CI?

@nhunzaker nhunzaker dismissed their stale review August 2, 2018 22:34

Requested changes look good, though I think we still need to answer whether or not we're okay placing this fix where it is at.

@nhunzaker
Copy link
Contributor

nhunzaker commented Aug 2, 2018

What if props.multiple changes during an update, but we insert a child into it before the change? Is that also going to cause the same issue?

I'm not sure, but:

That said we can probably split setting initial properties in two phases as well (before and after appending the children). Do we have a proposal for which ones should go where?

What if we just placed a hook here:

const domElement: Instance = createElement(
type,
props,
rootContainerInstance,
parentNamespace,
);
precacheFiberNode(internalInstanceHandle, domElement);
updateFiberProps(domElement, props);
return domElement;
}

Maybe something like:

// ... createInstance()
  const domElement: Instance = createElement(
    type,
    props,
    rootContainerInstance,
    parentNamespace,
  );
  precacheFiberNode(internalInstanceHandle, domElement);
  updateFiberProps(domElement, props);
  
  // Open to a better name
  setPropsBeforeChildren(type, props, domElement)

  return domElement;
}

Then in ReactDOMFiberComponent:

function setPropsBeforeChildren(type, props, domElement) {
  if (type === 'select' && props.multiselect) {
    select.multiselect = true
  }
}

@gaearon
Copy link
Collaborator

gaearon commented Aug 3, 2018

@nhunzaker Is your suggestion technically different except code organization? I'm fine with either tbh.

@gaearon
Copy link
Collaborator

gaearon commented Aug 3, 2018

I guess a separate function makes it a bit harder to miss why we're doing it. And GCC will inline it anyway.

@nhunzaker
Copy link
Contributor

@gaearon I don't think it introduces anything new. Purely code organization.

Honestly I'm okay with this how it is, though I do think it's worth starting catalog why these sorts of fixes need to happen at specific points in the process for a later refactor. There isn't really great place for a fix like this and adding an extra layer of indirection doesn't feel right.

// `select` elements automatically pick the first item.
// See https://github.com/facebook/react/issues/13222
if (type === 'select' && props.multiple) {
domElement.setAttribute('multiple', 'true');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for consistence with ReactDOMFiberSelect, can you assign this as a property, like:

domElement.multiple = true

@gaearon
Copy link
Collaborator

gaearon commented Aug 3, 2018

Might want to start a document with a list of hacks for any input type and reasons for them.

@nhunzaker
Copy link
Contributor

Sounds good. I'll start a markdown document and add it to the fixtures. Maybe it could be hosted along side a public version of the fixtures using Netlify (need to circle back to that too).

@nhunzaker
Copy link
Contributor

@gaearon This looks good on my end and I have verified it locally.

@nhunzaker
Copy link
Contributor

@segoddnja I pushed a small commit to remove an unused ref just now, but I believe this is good to merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Select multiple - does not scroll to selected item(items)
6 participants