RFC: How to handle native element props for input components#18804
RFC: How to handle native element props for input components#18804sopranopillow wants to merge 5 commits intomasterfrom
Conversation
|
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 66049ae:
|
Asset size changesSize Auditor did not detect a change in bundle size for any component! Baseline commit: 035ba0498cee783b11b31dffa0cbf595b73d6ec8 (build) |
Perf Analysis (
|
| Scenario | Render type | Master Ticks | PR Ticks | Iterations | Status |
|---|---|---|---|---|---|
| Avatar | mount | 814 | 812 | 5000 | |
| BaseButton | mount | 935 | 923 | 5000 | |
| Breadcrumb | mount | 2528 | 2523 | 1000 | |
| ButtonNext | mount | 550 | 541 | 5000 | |
| Checkbox | mount | 1577 | 1621 | 5000 | |
| CheckboxBase | mount | 1342 | 1326 | 5000 | |
| ChoiceGroup | mount | 4865 | 4805 | 5000 | |
| ComboBox | mount | 986 | 971 | 1000 | |
| CommandBar | mount | 10353 | 10212 | 1000 | |
| ContextualMenu | mount | 6344 | 6172 | 1000 | |
| DefaultButton | mount | 1119 | 1200 | 5000 | |
| DetailsRow | mount | 3973 | 3803 | 5000 | |
| DetailsRowFast | mount | 3844 | 3882 | 5000 | |
| DetailsRowNoStyles | mount | 3801 | 3699 | 5000 | |
| Dialog | mount | 2239 | 2300 | 1000 | |
| DocumentCardTitle | mount | 142 | 132 | 1000 | |
| Dropdown | mount | 3429 | 3366 | 5000 | |
| FluentProviderNext | mount | 6831 | 7038 | 5000 | |
| FocusTrapZone | mount | 1825 | 1783 | 5000 | |
| FocusZone | mount | 1826 | 1780 | 5000 | |
| IconButton | mount | 1750 | 1813 | 5000 | |
| Label | mount | 335 | 338 | 5000 | |
| Layer | mount | 1776 | 1807 | 5000 | |
| Link | mount | 448 | 463 | 5000 | |
| MakeStyles | mount | 1720 | 1760 | 50000 | |
| MenuButton | mount | 1496 | 1519 | 5000 | |
| MessageBar | mount | 2001 | 1958 | 5000 | |
| Nav | mount | 3346 | 3339 | 1000 | |
| OverflowSet | mount | 1016 | 1009 | 5000 | |
| Panel | mount | 1374 | 2006 | 1000 | |
| Persona | mount | 815 | 820 | 1000 | |
| Pivot | mount | 1378 | 1396 | 1000 | |
| PrimaryButton | mount | 1332 | 1323 | 5000 | |
| Rating | mount | 7955 | 7859 | 5000 | |
| SearchBox | mount | 1398 | 1387 | 5000 | |
| Shimmer | mount | 2666 | 2609 | 5000 | |
| Slider | mount | 1959 | 1974 | 5000 | |
| SpinButton | mount | 5025 | 5030 | 5000 | |
| Spinner | mount | 413 | 400 | 5000 | |
| SplitButton | mount | 3218 | 3141 | 5000 | |
| Stack | mount | 499 | 515 | 5000 | |
| StackWithIntrinsicChildren | mount | 1569 | 1569 | 5000 | |
| StackWithTextChildren | mount | 4700 | 4702 | 5000 | |
| SwatchColorPicker | mount | 10270 | 10272 | 5000 | |
| Tabs | mount | 1405 | 1420 | 1000 | |
| TagPicker | mount | 2556 | 2462 | 5000 | |
| TeachingBubble | mount | 11611 | 11636 | 5000 | |
| Text | mount | 413 | 450 | 5000 | |
| TextField | mount | 1411 | 1390 | 5000 | |
| ThemeProvider | mount | 1156 | 1154 | 5000 | |
| ThemeProvider | virtual-rerender | 585 | 575 | 5000 | |
| Toggle | mount | 838 | 814 | 5000 | |
| buttonNative | mount | 105 | 109 | 5000 |
Perf Analysis (@fluentui/react-northstar)
Perf tests with no regressions
| Scenario | Current PR Ticks | Baseline Ticks | Ratio |
|---|---|---|---|
| AnimationMinimalPerf.default | 448 | 413 | 1.08:1 |
| AvatarMinimalPerf.default | 212 | 197 | 1.08:1 |
| BoxMinimalPerf.default | 401 | 376 | 1.07:1 |
| ListCommonPerf.default | 709 | 667 | 1.06:1 |
| ReactionMinimalPerf.default | 416 | 391 | 1.06:1 |
| ButtonMinimalPerf.default | 181 | 173 | 1.05:1 |
| SegmentMinimalPerf.default | 388 | 372 | 1.04:1 |
| DividerMinimalPerf.default | 398 | 387 | 1.03:1 |
| GridMinimalPerf.default | 363 | 354 | 1.03:1 |
| LabelMinimalPerf.default | 423 | 409 | 1.03:1 |
| ListMinimalPerf.default | 543 | 529 | 1.03:1 |
| TextAreaMinimalPerf.default | 560 | 542 | 1.03:1 |
| VideoMinimalPerf.default | 654 | 638 | 1.03:1 |
| AlertMinimalPerf.default | 299 | 294 | 1.02:1 |
| AttachmentMinimalPerf.default | 174 | 170 | 1.02:1 |
| FlexMinimalPerf.default | 309 | 304 | 1.02:1 |
| FormMinimalPerf.default | 453 | 443 | 1.02:1 |
| LayoutMinimalPerf.default | 396 | 390 | 1.02:1 |
| MenuMinimalPerf.default | 904 | 886 | 1.02:1 |
| SkeletonMinimalPerf.default | 369 | 362 | 1.02:1 |
| TableManyItemsPerf.default | 2062 | 2012 | 1.02:1 |
| ToolbarMinimalPerf.default | 974 | 959 | 1.02:1 |
| AttachmentSlotsPerf.default | 1111 | 1095 | 1.01:1 |
| CardMinimalPerf.default | 603 | 598 | 1.01:1 |
| CarouselMinimalPerf.default | 499 | 495 | 1.01:1 |
| ChatMinimalPerf.default | 687 | 682 | 1.01:1 |
| DialogMinimalPerf.default | 783 | 775 | 1.01:1 |
| LoaderMinimalPerf.default | 727 | 717 | 1.01:1 |
| MenuButtonMinimalPerf.default | 1767 | 1751 | 1.01:1 |
| TextMinimalPerf.default | 368 | 365 | 1.01:1 |
| ButtonOverridesMissPerf.default | 1760 | 1759 | 1:1 |
| DatepickerMinimalPerf.default | 5513 | 5539 | 1:1 |
| EmbedMinimalPerf.default | 4223 | 4210 | 1:1 |
| HeaderMinimalPerf.default | 401 | 400 | 1:1 |
| HeaderSlotsPerf.default | 818 | 814 | 1:1 |
| InputMinimalPerf.default | 1273 | 1271 | 1:1 |
| ListNestedPerf.default | 607 | 604 | 1:1 |
| SliderMinimalPerf.default | 1581 | 1586 | 1:1 |
| StatusMinimalPerf.default | 714 | 715 | 1:1 |
| TableMinimalPerf.default | 432 | 433 | 1:1 |
| CustomToolbarPrototype.default | 3895 | 3885 | 1:1 |
| AccordionMinimalPerf.default | 170 | 172 | 0.99:1 |
| CheckboxMinimalPerf.default | 2750 | 2790 | 0.99:1 |
| DropdownMinimalPerf.default | 3094 | 3110 | 0.99:1 |
| ItemLayoutMinimalPerf.default | 1292 | 1308 | 0.99:1 |
| PopupMinimalPerf.default | 598 | 604 | 0.99:1 |
| ProviderMergeThemesPerf.default | 1628 | 1644 | 0.99:1 |
| RadioGroupMinimalPerf.default | 472 | 475 | 0.99:1 |
| SplitButtonMinimalPerf.default | 3969 | 4003 | 0.99:1 |
| TooltipMinimalPerf.default | 1043 | 1050 | 0.99:1 |
| TreeMinimalPerf.default | 830 | 840 | 0.99:1 |
| ButtonSlotsPerf.default | 574 | 585 | 0.98:1 |
| RosterPerf.default | 1246 | 1269 | 0.98:1 |
| ProviderMinimalPerf.default | 1018 | 1037 | 0.98:1 |
| ChatWithPopoverPerf.default | 375 | 386 | 0.97:1 |
| DropdownManyItemsPerf.default | 729 | 748 | 0.97:1 |
| ListWith60ListItems.default | 655 | 672 | 0.97:1 |
| PortalMinimalPerf.default | 179 | 184 | 0.97:1 |
| RefMinimalPerf.default | 224 | 230 | 0.97:1 |
| IconMinimalPerf.default | 632 | 654 | 0.97:1 |
| ChatDuplicateMessagesPerf.default | 304 | 316 | 0.96:1 |
| TreeWith60ListItems.default | 172 | 181 | 0.95:1 |
| ImageMinimalPerf.default | 400 | 430 | 0.93:1 |
| </div> | ||
| ``` | ||
|
|
||
| ### Option 1: All native props always applied to root (current behavior) |
There was a problem hiding this comment.
Could you do a survey of other react design systems and what solutions they use ? AFAIK most design systems will follow something along the lines of Option 1 or Option 2
There was a problem hiding this comment.
I'd have to dig into it a bit more for Input (and Esteban would have a better idea for Checkbox) but I don't believe that's true. IIRC from the research I did, they appear to most commonly use either option 2 (combination) or possibly tending to apply the props just to the input not the root. But to confirm that I'd need to make a bunch of codesandboxes and look at their props and DOM.
There was a problem hiding this comment.
So far IMO I'd go for Option 1. seems more consistent with all other converged components.
But yeah, the arguments of counter intuitivity is valid.
Perhaps you could faltten some obvious properties in the root element and manually transfer it to the slot.
There was a problem hiding this comment.
you could faltten some obvious properties in the root element and manually transfer it to the slot.
unless this is consistent and explicitly stated for all vNext controls, I'd avoid any "special case" magic
There was a problem hiding this comment.
Perhaps you could faltten some obvious properties in the root element and manually transfer it to the slot.
@bsunderhus Define "obvious"? I haven't been able to come up with a definition that relies on anything besides intuition, which is subjective and varies between people.
There was a problem hiding this comment.
Yeah, you're right. This is too much subjective.
There was a problem hiding this comment.
I checked a few other systems that I could readily get a working sandbox for their text Input component, and the consensus seemed to be something like Option 2, only slightly modified in that all native props go to the input element, with a small number of exceptions that go to the wrapper (e.g. className).
I tested by adding a few props to the checkbox component and seeing what DOM element they ended up on (using data-test as a proxy for "any arbitrary prop"). The results were similar for most, but I've marked the differences with [*]
| name | id | className | aria-label | data-test | |
|---|---|---|---|---|---|
| Ant | input | input | wrapper | input | input |
| Carbon | input | input | input [*] | input | input |
| Fabric v8 | input | input | wrapper | - | input |
| Northstar v0 | input | input | wrapper | input | wrapper [*] |
- Carbon put
classNameon the input element for text Input component in that table, but for the Checkbox component, it putclassNameon the label element (not the wrapper, nor the input elements).
| - We could use typings to ensure that doing e.g. `<Checkbox checked/>` caused a TS error, but it still seems very unnatural | ||
| - Might not work as easily with 3rd-party form libraries? (needs more research) | ||
|
|
||
| ### Option 2: Most native props applied to root, selected props applied to "actual" element |
There was a problem hiding this comment.
To a certain extent this would happen anyway right ? since the component itself would most likely be exposing some kind of value prop for controlling
|
I am in favor of "Option 2" as it allows the simplest integration with validation libraries, it's one of the most common scenarios. Another thing to mention is that a list of HTML attributes is finite, https://github.com/Semantic-Org/Semantic-UI-React/blob/abaf123cdd0dacaa260ecdd1920a3383baa2ba4e/src/lib/htmlPropsUtils.js#L3-L36. The main question then is how to pass props function App() {
const wrapperRef = React.useRef();
React.useEffect(() => {
console.log(wrapperRef.current.firstChild); // returns a wrapping "div"
}, []);
return (
<div id="wrapper" ref={wrapperRef}>
<Input />
</div>
);
} |
|
I like the idea of having Option 2 for easy integration with form validators like Formik. I would also assume that my Input or Checkbox component's root component be the |
Hotell
left a comment
There was a problem hiding this comment.
q: will this RFC tackle also react exotic handling of change/input events on input components?
Why are you asking this? -> this is important for preact users for example
|
|
||
| You get roughly this HTML, which is not useful: | ||
| ```html | ||
| <div name="foo" checked> |
There was a problem hiding this comment.
are we sure about checked attribute being added ? setAttribute throws error if you don't provide value. is this some buggy react-dom behaviour ?
There was a problem hiding this comment.
Hmm looks like it's not applied. So we should update the resulting HTML, but it doesn't change the point, which is that spreading the attribute there is not useful. https://codepen.io/ecraig12345/pen/mdmOjqL
| </div> | ||
| ``` | ||
|
|
||
| ### Option 1: All native props always applied to root (current behavior) |
There was a problem hiding this comment.
you could faltten some obvious properties in the root element and manually transfer it to the slot.
unless this is consistent and explicitly stated for all vNext controls, I'd avoid any "special case" magic
|
|
||
| ### [react-hook-form](https://github.com/react-hook-form/react-hook-form) | ||
|
|
||
| Note, for this one they'd need to make a wrapper for our component because the library expects to be able to *set* `.value` on the input, which we probably don't intend to support. (Someone brought this up for v8 [here](https://github.com/microsoft/fluentui/issues/18126).) |
There was a problem hiding this comment.
from my experience hook-form is a must support - most lightweight/powerful solution on the market
There was a problem hiding this comment.
How would you integrate this with standard controlled/uncontrolled value behavior?
| </div> | ||
| ``` | ||
|
|
||
| Another unique thing about inputs is that to the degree possible, we'd like them to work nicely with 3rd-party form libraries, which may have APIs that expect to be able to pass native props to the component and have them applied to the actual `<input>`. (needs more research) |
There was a problem hiding this comment.
regarding proper support of various libraries/frameworks - it might be good idea to publish fluent wrappers as separate packages for those particular solutions to make adoption super easy
|
I'd go with option 1 or option 3. while option 1 seems to be counter intuitive from DX perspective:
In general it would be nice to get some solid data set (feedback) from internal customers and open source community on what approach should we adopt/is less "evil". |
AFAIK there's been little/no discussion of Preact support. From what I understand, this would require specific efforts and possibly some changes in approach, and certainly tests to ensure we don't accidentally break it in the future. So we shouldn't start attempting to design/optimize for Preact unless we decide to officially support it. |
@layershifter But how do you decide which props to cherry-pick? How do we make this obvious to users and ensure that it's semi-consistent between relevant components?
What about |
@andrefcdias As far as I'm aware, it's not possible to have the |
|
Just another wild idea: Would it make sense to only ship We can apply our styling and behaviours to the wrapper and simply clone the child and apply any behaviours directly on the input like we do on Users could be happy since they will be using the native input most of the time and they can create their on |
|
|
||
| ### Option 3 | ||
|
|
||
| Let the native input element be the "root" slot, but use a `wrapper` slot as the actual root DOM element. Here's an example: |
There was a problem hiding this comment.
How about a slight modification to Option 3:
- Modify the internal slots API to allow a component to specify the name of a slot that gets the native props that are specified directly on the component's props. It would be the
rootslot by default, but in this case, we'd override it to be theinputslot instead. - The
classNameprop would be special-cased to always go to the root slot. - Make
roota real slot for every component, so you could writeroot={{ id: 'foo' }}to set theidof therootslot.
The result would be that components could choose the most logical slot to receive the native props for that component, but there would always be an escape hatch for a user to specify props on any slot.
E.g.:
<Checkbox id="foo" /> {/* The input element gets id="foo" */}
<Checkbox input={{ id: 'foo' }} /> {/* Same as above, just being more explicit */}
<Checkbox root={{ id: 'bar' }} /> {/* The root element gets id="bar" */}
I have to say I like this idea. robust design systems, like angular material, are using same approach https://material.angular.io/components/input/overview |
AFAIR one of our big consumers is using Preact. I asked this in this RFC, as input events are the one "tricky" area where folks might run into issues with just react event handling behaviors. thanks! |
Props to control componentUsers will need to pass <>
<InputField onChange={/* Should it be there? */}>
<input onChange={/* Or should it be there? */} />
</InputField>
{/* Or even worse when handlers and `value` are on different JSX elements */}
<InputField value={someVariables}>
<input onChange={() => {}} />
</InputField>
</>An obvious answer will be that props should be on // 😔 not really correct, see below
<InputField>
<input onChange={() => {}} value={someVariables} />
</InputField>But even // ✅ this is correct
<InputField onChange={() => {}} value={someVariable}>
<input />
</InputField>For more complex components like In the same time nothing (even typings) will prevent users from doing this: <InputField onChange={() => {}} value={someVariable}>
<input value={otherVariable} />
</InputField>What should win in this case, Usage in other componentsI would expect that API should scale to other components. I am not sure that it will play with well with <SelectField>
{/* it's simple to handle a single child, what about multiple? */}
<select name="cars" id="cars" />
<SelectOption value="volvo">Volvo</option>
<SelectOption value="saab">Saab</option>
</SelectField> |
Could we do this with slots <InputField clearButton={{ onClick: () => setState }} >
<input value={state} onChange={() => setState} />
</InputField>
Similarly you would be controlling the the
No good answer to this, it might depend on how we would need to style a native |
It's an interesting idea but seems like solving one specific problem at the expense of overall API clarity/simplicity, especially since it's completely different from what any other React library does (as far as I've seen). And it doesn't necessarily solve the problem of clarity about where props go, as others pointed out. So I would strongly prefer not to go with this approach. |
The clearable scenario probably isn't going to be built in to the base Input component due to bundle size concerns of adding a direct reference to Button (though we may have a recomposed version or wrapper which handles putting the button it in the slot and setting appropriate props). |
|
After a discussion in the Redmond components crew meeting on Thursday, I made a new RFC #18983 which is a modified version of this RFC with a slightly different approach (based on one of @behowell's comments), and broadening the focus beyond input components in some aspects. @ling1726 @layershifter @Hotell @bsunderhus @andrefcdias (and anyone else interested) please take a look and let me know what you think there. |
|
Closing this one in favor of #18983. That one isn't ready for review yet, but I'll be updating it this week to incorporate comments here and the root as a slot RFC. Thanks @sopranopillow for your work on getting this discussion started! |
Pull request checklist
$ yarn changeDescription of changes
This PR adds an RFC that suggests options on how to move forward with handling native element props for input components. There are pros and cons to each of the options we could follow and so feedback from the team is needed to make a final decision.
Focus areas to test
(optional)