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

RFC: update slots API to allow arrays #19266

Closed
wants to merge 1 commit into from

Conversation

smhigley
Copy link
Contributor

@smhigley smhigley commented Aug 4, 2021

This RFC looks at possibilities for how to handle components like Combobox, List, or Table/Grid that commonly take arrays of data rather than individual slots or manually authored children.

@codesandbox-ci
Copy link

codesandbox-ci bot commented Aug 4, 2021

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 32a068f:

Sandbox Source
Fluent UI React Starter Configuration

@size-auditor
Copy link

size-auditor bot commented Aug 4, 2021

Asset size changes

Size Auditor did not detect a change in bundle size for any component!

Baseline commit: 5369d330c7b0983efba35c6e945340506007c6c8 (build)

@fabricteam
Copy link
Collaborator

📊 Bundle size report

Unchanged fixtures
Package & Exports Size (minified/GZIP)
react-accordion
Accordion (including children components)
78.506 kB
23.21 kB
react-avatar
Avatar
56.558 kB
15.66 kB
react-badge
Badge
24.343 kB
7.165 kB
react-badge
CounterBadge
27.156 kB
7.851 kB
react-badge
PresenseBadge
237 B
177 B
react-button
Button
24.934 kB
8.001 kB
react-button
CompoundButton
30.226 kB
8.878 kB
react-button
MenuButton
26.521 kB
8.509 kB
react-button
ToggleButton
34.531 kB
8.637 kB
react-components
react-components: Accordion, Button, FluentProvider, Image, Menu, Popover
179.799 kB
50.935 kB
react-components
react-components: FluentProvider & webLightTheme
36.237 kB
11.596 kB
react-divider
Divider
15.889 kB
5.747 kB
react-image
Image
10.642 kB
4.264 kB
react-label
Label
9.397 kB
3.839 kB
react-link
Link
14.715 kB
6.012 kB
react-make-styles
makeStaticStyles (runtime)
7.59 kB
3.321 kB
react-make-styles
makeStyles + mergeClasses (runtime)
22.135 kB
8.356 kB
react-make-styles
makeStyles + mergeClasses (build time)
2.557 kB
1.202 kB
react-menu
Menu (including children components)
114.61 kB
34.554 kB
react-menu
Menu (including selectable components)
116.71 kB
34.824 kB
react-popover
Popover
124.181 kB
36.121 kB
react-portal
Portal
7.78 kB
2.672 kB
react-positioning
usePopper
23.157 kB
7.942 kB
react-provider
FluentProvider
16.235 kB
5.972 kB
react-theme
Teams: all themes
32.941 kB
6.674 kB
react-theme
Teams: Light theme
20.247 kB
5.662 kB
react-tooltip
Tooltip
45.281 kB
15.45 kB
react-utilities
SSRProvider
213 B
170 B
🤖 This report was generated against 5369d330c7b0983efba35c6e945340506007c6c8

@fabricteam
Copy link
Collaborator

Perf Analysis (@fluentui/react)

No significant results to display.

All results

Scenario Render type Master Ticks PR Ticks Iterations Status
Avatar mount 851 879 5000
BaseButton mount 852 868 5000
Breadcrumb mount 2610 2604 1000
ButtonNext mount 428 426 5000
Checkbox mount 1510 1469 5000
CheckboxBase mount 1273 1267 5000
ChoiceGroup mount 4637 4647 5000
ComboBox mount 955 965 1000
CommandBar mount 9982 10021 1000
ContextualMenu mount 6043 6141 1000
DefaultButton mount 1118 1077 5000
DetailsRow mount 3624 3632 5000
DetailsRowFast mount 3660 3669 5000
DetailsRowNoStyles mount 3421 3480 5000
Dialog mount 2118 2159 1000
DocumentCardTitle mount 132 144 1000
Dropdown mount 3180 3152 5000
FluentProviderNext mount 7433 7396 5000
FocusTrapZone mount 1756 1745 5000
FocusZone mount 1798 1790 5000
IconButton mount 1704 1707 5000
Label mount 329 330 5000
Layer mount 1758 1752 5000
Link mount 446 450 5000
MakeStyles mount 1812 1783 50000
MenuButton mount 1420 1419 5000
MessageBar mount 1998 2007 5000
Nav mount 3203 3213 1000
OverflowSet mount 1023 1006 5000
Panel mount 2103 2038 1000
Persona mount 811 812 1000
Pivot mount 1408 1372 1000
PrimaryButton mount 1249 1276 5000
Rating mount 7491 7442 5000
SearchBox mount 1329 1275 5000
Shimmer mount 2455 2497 5000
Slider mount 1938 1875 5000
SpinButton mount 4819 4845 5000
Spinner mount 411 417 5000
SplitButton mount 3053 3095 5000
Stack mount 496 484 5000
StackWithIntrinsicChildren mount 1559 1537 5000
StackWithTextChildren mount 4479 4494 5000
SwatchColorPicker mount 10173 9980 5000
Tabs mount 1356 1384 1000
TagPicker mount 2531 2538 5000
TeachingBubble mount 11739 11653 5000
Text mount 402 398 5000
TextField mount 1346 1317 5000
ThemeProvider mount 1159 1183 5000
ThemeProvider virtual-rerender 596 601 5000
Toggle mount 784 801 5000
buttonNative mount 115 120 5000

Perf Analysis (@fluentui/react-northstar)

Perf tests with no regressions
Scenario Current PR Ticks Baseline Ticks Ratio
FormMinimalPerf.default 407 365 1.12:1
TreeWith60ListItems.default 175 159 1.1:1
ButtonMinimalPerf.default 168 156 1.08:1
AnimationMinimalPerf.default 422 394 1.07:1
CardMinimalPerf.default 566 527 1.07:1
CarouselMinimalPerf.default 458 433 1.06:1
ButtonSlotsPerf.default 558 531 1.05:1
ChatWithPopoverPerf.default 367 349 1.05:1
LabelMinimalPerf.default 386 367 1.05:1
ListCommonPerf.default 620 592 1.05:1
SegmentMinimalPerf.default 354 337 1.05:1
StatusMinimalPerf.default 668 636 1.05:1
AccordionMinimalPerf.default 155 149 1.04:1
DropdownManyItemsPerf.default 658 630 1.04:1
PopupMinimalPerf.default 594 571 1.04:1
PortalMinimalPerf.default 179 172 1.04:1
TextMinimalPerf.default 350 337 1.04:1
AvatarMinimalPerf.default 189 183 1.03:1
DropdownMinimalPerf.default 3089 3012 1.03:1
ProviderMergeThemesPerf.default 1692 1639 1.03:1
DatepickerMinimalPerf.default 5312 5232 1.02:1
FlexMinimalPerf.default 283 277 1.02:1
ListMinimalPerf.default 501 489 1.02:1
ReactionMinimalPerf.default 372 364 1.02:1
TableManyItemsPerf.default 1844 1812 1.02:1
TextAreaMinimalPerf.default 478 468 1.02:1
AlertMinimalPerf.default 271 267 1.01:1
DialogMinimalPerf.default 733 723 1.01:1
EmbedMinimalPerf.default 4058 4021 1.01:1
HeaderSlotsPerf.default 743 733 1.01:1
ItemLayoutMinimalPerf.default 1196 1186 1.01:1
RefMinimalPerf.default 237 235 1.01:1
SkeletonMinimalPerf.default 339 334 1.01:1
BoxMinimalPerf.default 333 333 1:1
ButtonOverridesMissPerf.default 1665 1662 1:1
ChatMinimalPerf.default 643 644 1:1
GridMinimalPerf.default 311 310 1:1
ImageMinimalPerf.default 354 354 1:1
InputMinimalPerf.default 1229 1228 1:1
LayoutMinimalPerf.default 356 356 1:1
ListWith60ListItems.default 625 627 1:1
LoaderMinimalPerf.default 683 683 1:1
SliderMinimalPerf.default 1565 1567 1:1
SplitButtonMinimalPerf.default 3716 3708 1:1
IconMinimalPerf.default 608 608 1:1
CustomToolbarPrototype.default 3809 3800 1:1
TooltipMinimalPerf.default 989 989 1:1
TreeMinimalPerf.default 774 777 1:1
CheckboxMinimalPerf.default 2669 2687 0.99:1
ListNestedPerf.default 537 545 0.99:1
ProviderMinimalPerf.default 992 1001 0.99:1
RadioGroupMinimalPerf.default 442 445 0.99:1
TableMinimalPerf.default 401 406 0.99:1
ToolbarMinimalPerf.default 907 920 0.99:1
AttachmentMinimalPerf.default 148 151 0.98:1
MenuMinimalPerf.default 814 827 0.98:1
MenuButtonMinimalPerf.default 1591 1623 0.98:1
AttachmentSlotsPerf.default 1023 1052 0.97:1
DividerMinimalPerf.default 336 346 0.97:1
HeaderMinimalPerf.default 350 359 0.97:1
ChatDuplicateMessagesPerf.default 289 300 0.96:1
RosterPerf.default 1152 1201 0.96:1
VideoMinimalPerf.default 616 642 0.96:1

@layershifter layershifter self-requested a review August 5, 2021 15:17
...etc,
components: {
root: 'div',
options: 'div',
Copy link
Contributor

Choose a reason for hiding this comment

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

One drawback here is that you can't specify a different component for each entry in the array. In this case, every option is a <div>, and there's no way to make certain ones be a <GroupHeader> component or something.

This comment is mostly an FYI because I think I have a decent solution that modifies how this components prop works. I'm going to write an RFC with this proposal, but a quick summary:

// Currently requires a components={...} object separate from the slot value
<MyButton components={{ label: MyLabel }} label="foo" />
<MyButton components={{ label: 'span' }} label={{ className: 'baz', children: 'bar' }} />

// Proposal: slots could accept a tuple of [ type, shorthandValue ]
<MyButton label={[ MyLabel, 'foo' ]} />
<MyButton label={[ 'span', { className: 'baz', children: 'bar' } ]} />

That would naturally allow a different component to be specified per slot in this RFC:

<Combobox options={[ 
  [GroupHeader, 'Fruits'],
  'apple',
  'banana',
  [GroupHeader, 'Vegetables'],
  'carrot',
  'daikon',
]} />


```js
export function resolveShorthandArray<Props extends Record<string, any>>(
value: ShorthandProps<Props>[],
Copy link
Contributor

Choose a reason for hiding this comment

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

You'd also want to accept undefined | null here:

Suggested change
value: ShorthandProps<Props>[],
value: ShorthandProps<Props>[] | undefined | null,

value: ShorthandProps<Props>[],
defaultProps?: Props,
): ObjectShorthandProps<Props>[] {
if (value === null || !Array.isArray(value)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

If you take my suggestion above, you'd also need to check for undefined (or just falsy). Also, (nit) remove the isArray check; that should be handled by typescript.

Suggested change
if (value === null || !Array.isArray(value)) {
if (!value) {

}
```

One downside to this solution is it that in order for an author to define a set of props that appear on every option, they'll need to duplicate that prop object for each option in `props.options`. We could potentially consider adding another prop like `defaultOption: React.HTMLAttributes<HTMLElement>` to make that easier.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's ok to start by requiring authors to manually apply a prop to each entry for now (e.g. using map). If it becomes a problem, we could add another way of doing it in the future.

@bsunderhus bsunderhus mentioned this pull request Aug 6, 2021
Than this:

```js
<Combobox>
Copy link
Member

Choose a reason for hiding this comment

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

Can we update this example to showcase usage? IMO this comparison will be more accurate:

const options = ["apple", "banana", "cucumber"];

function App() {
  return (
    <>
      <Combobox options={options} />
      {/* VS. */}
      <Combobox>
        {options.map(item => (
          <Option key={item}>{item}</Option>
        ))}
      </Combobox>
    </>
  );
}

It's very similar to what React docs are showing: https://reactjs.org/docs/lists-and-keys.html#basic-list-component

Copy link
Member

@ling1726 ling1726 Aug 9, 2021

Choose a reason for hiding this comment

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

The map does not need to be rendered directly as children, this will now resemble the shorthand proposal quite closely, and allows easy memoization

const options = ["apple", "banana", "cucumber"];

function App() {
  const comboboxItems = options.map(item => (<Option key={item.id}>{item.name}</Option>))
  return (
    <>
      <Combobox>
        {comboboxItems}
      </Combobox>
    </>
  );
}

Copy link
Member

Choose a reason for hiding this comment

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

I prefer this approach to having two props of items and onRenderItems which is what we did in v8. It's effectively the same thing, but the mapping is done by the user instead of the component.

return (
<slots.root {...slotProps.root}>
{slots.options.map((Option, i) => {
return <Option key={`option-${i}`} {...slotProps.options[i]} />;
Copy link
Member

Choose a reason for hiding this comment

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

Using Index as a key is an anti-pattern in React:

We don’t recommend using indexes for keys if the order of items may change. This can negatively impact performance and may cause issues with component state. Check out Robin Pokorny’s article for an in-depth explanation on the negative impacts of using an index as a key. If you choose not to assign an explicit key to list items then React will default to using indexes as keys.

https://reactjs.org/docs/lists-and-keys.html#keys
https://robinpokorny.medium.com/index-as-a-key-is-an-anti-pattern-e0349aece318


## Summary

This RFC proposes modifying the Slots API to accept arrays of slot props to support components like Combobox or List, where the most ergonomic authoring experience is passing in an array of data rather than unique JSX children.
Copy link
Member

Choose a reason for hiding this comment

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

This is how we started in Semantic UI React and we decided to use the same API in Northstar, we call it shorthand collections. When we started there were no other way to pass props from parent to children components:

From our POV this API worked well as it's really simple:

// 🦁 wow, so easy!
<Dropdown items={["Robert Tolbert", "Wanda Howard", "Tim Deboer"]} />

However, usage in complex applications that are our main audience is different: data comes from different sources, lists are huge and there strict performance requirements. Below is a summary of feedback that we received from Northstar customers.

Keys are hard

First of all, customers should use keys (which is not usually obvious in examples):

<>
  // 🟥 if we will use these strings a keys, we will get a React warning about items with duplicate keys
  <Dropdown items={["Robert", "Robert"]} />
  // ✅ will not produce warnings
  <Dropdown
    items={[
      { key: "robert-k", content: "Robert" },
      { key: "robert-t", content: "Robert" }
    ]}
  />
</>

There is no easy option how generate keys for customers in automated way, they should pass it. Check a comment from @levithomason in Semantic-Org/Semantic-UI-React#2418 (comment), it's a good summary from library POV.

Performance issues (memoization)

"My items are really expensive to render, how I memoize them?"

Items can really expensive to re-render especially in components like Dropdown, Menu, List & etc. A simple Avatar from Teams is below, it's even without status 😱

image

One of the frequent asks that we had was about memoization, we have an entry in our FAQ. Developers want to implement their features and expect that library will work intuitively for them. But it does not seem to be true from an example below:

const MemoizedItem = React.memo(Menu.Item, (prevProps, nextProps) => {
  return prevProps.active === nextProps.active;
});

const renderBlocker = (Component, props) => <MemoizedItem {...props} />;

function App() {
  return (
    <>
      {/* 👎 Even with `renderItem()` it will not be better */}
      <Menu
        items={[
          { content: "One", key: "one", children: renderBlocker },
          { content: "Two", key: "two", children: renderBlocker }
        ]}
      />

      {/* 👍 This looks like in React docs */}
      <Menu>
        <MemoizedItem>One</MemoizedItem>
        <MemoizedItem>Two</MemoizedItem>
      </Menu>
    </>
  );
}

Performance issues (redundant rendering)

Another feedback that we received is related to a fact that customers should compute their items in advance for menus (or dropdowns, etc.) even before they are visible.

It could be cheap to get data for the items itself, but each item can contain a lot of additional info required to render: user's state, description, etc. For example:

<>
  <Dropdown
    items={[
      {
        // 👇 "key" & "content" are required for rendering
        key: "robert-t",
        content: "Robert Tolbert",
        // 👇 "image" & "description" could be fetched later, but we don't allow this with "items" 😕
        image: "/avatar/RobertTolbert.jpg",
        description: "Software Engineer"
      }
    ]}
  />

  <Dropdown>
    {/* 👇 only minimal data to render "Dropdown" */}
    {[{ key: "robert-t", content: "Robert Tolbert" }].map(item => (
      // 👍 Data can be fetched lazily inside custom component
      <DropdownItemThatFetchesDetails {...item} />
    ))}
  </Dropdown>
</>

A short demo of this behavior is available on this CodeSandbox:

  • for v0 we should call getUserStatus() before a menu is opened
  • for v9 we call getUserStatus() only when a menu is opened

Performance issues (root of update is moved to the parent)

Another issue that we met is related to a fact that a parent component is a root of rendering. It's highly coupled to a previous section. See an example below:

<>
  <Dropdown
    // 👇 "online" is a property of an item, if we will change it, Dropdown should be re-rendered *anyway*
    // 💥 Other items will be also re-rendered by default, to avoid this we will need to use memoization
    items={[{ key: "robert-t", content: "Robert Tolbert", status: "online" }]}
  />

  <Dropdown>
    {[{ key: "robert-t", content: "Robert Tolbert" }].map(item => (
      // 😅 As "status" is computed *inside* an item, only it will be updated
      <DropdownItemWithStatus {...item} />
    ))}
  </Dropdown>
</>

Demo of this behavior on CodeSandbox, check output in console:

// with "items"
render:DropdownA
render:DropdownItemA Jane
render:DropdownItemA John
// with JSX
render:DropdownItemBJane

Bundle size penalty

If I don't use DropdownItem from Fluent, it will be bundled anyway with Dropdown as it calls it internally:

<>
  <Dropdown
    // 👇 somewhere internally we need to render "DropdownItem", it's a direct dependency
    //    and will be bundled anyway
    items={[{ key: "robert-t", content: "Robert Tolbert", status: "online" }]}
  />

  <Dropdown>
    {/* 👇 Can be absolutely custom and used custom styles without penalties */}
    <CustomItem />
  </Dropdown>
</>

Virtualization

Components like Dropdown (Combobox) usually have filtering (client search) on client-side this means that input items are different from output items:

image
image

This means that virtualization component should be put inside Dropdown to know about its state, so we will need to add more props only for it:

<>
  <Dropdown
    items={items}
    renderItem={item => <MemoizedItem {...item} />}
    renderItems={(
      jsxItems /* items from "renderItem()" */,
      rawItems /* complex lists can require item objects */
    ) => (
      <List height={150} itemCount={items.length} itemSize={35} width={300}>{items}</List>
    )}
  >{/* 💡 this should be inside */}
  </Dropdown>
  {/* VS. */}
  <Dropdown><List height={150} itemCount={items.length} itemSize={35} width={300}>{MemoizedItem}</List>
  </Dropdown>
</>

It's one of reasons why we refactored Tree component in Northstar to hooks, it was to complex and not enough customisable.

BTW, a I don't think that we should include virtualization into components by default:

  • our customers has own solutions
  • it will bundle size penalty for components

Libraries that were designed after React hooks appeared also use JSX children in the most cases:

This happened mainly because of React Context & hooks, these APIs simplified sharing data & handlers between parent and children components. We also use use-context-selector to avoid redundant updates in child components, there is a good showcase for it in Northstar docs (only items with updated state are re-rendered).


For components like Combobox I suggest to keep going with JSX items. We will meet edge cases like Tree component where items are nested and it will be impossible to use JSX items, but I am considering this as an exception rather than a rule.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the detailed comment! The issues with performance and memoization are great to bring up. I'm curious about the best way to approach some of the issues I ran into when implementing Combobox with children, especially now that the Descendants API has been removed.

Essentially, the biggest issue is that the parent control (or a parent context) needs to be aware of the number of child options present, as well as their key/id values so that the active item can be controlled. The parent is the one that receives the keyboard events, and the one that needs to set the current item's id in an attribute, so it needs the ability to get the previous/next/first/last/etc child's id in response to user interaction.

Theoretically this could work with React.children, as long as it's a flat list. The issue there is something like this:

<Combobox>
  <Group name="fruits">
    <Option>apple</Option>
    <Option>banana</Option>
  </Group>
  <Group name="vegetables">
    <Option>onion</Option>
    <Option>zucchini</Option>
  </Group>
</Combobox>

Potentially we could do something like create a context that gives each option the ability to register/deregister itself by id, though it would be difficult to ensure we have the ids in the correct order. There's maybe also something we can do with React.Children in both Combobox and Group, so Combobox's context ends up with the correct set of current options. No matter what we do, the parent is going to need to update any time the descendants array updates, just based on how activedescendant works. There's likely a clever solution to this somewhere that I haven't been able to think of yet 😊.

For memoization, since we're dealing with slot arrays, I also wonder if authoring like this might work:

const MemoOption = React.memo(Option);

<Combobox options=[
  <MemoOption props={...etc} />
  <MemoOption props={...etc} />
  <MemoOption props={...etc} />
] />

I need to test it more, but it seems like that might still allow delayed updates and allowing the author to update one child without affecting the others. I'm definitely not confident on that approach yet, I'd definitely need to test it more (or you might know offhand if there's a reason it wouldn't fly). It definitely makes managing Combobox's state easier though 😅.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Virtualization is also a really interesting case, since I think even with options as children, the virtualization provider would likely need to live inside Combobox as a slot, since Combobox itself also needs to know what the total number of options is vs. the rendered number.

Copy link
Member

Choose a reason for hiding this comment

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

Chatted @smhigley and @behowell about this RFC. After pondering a bit, I think having a slot that takes an array of literals, props objects, or Components is what I would expect as consistent with singular slots in other components. A common scenario will be to pass in the array of props and be able to have a Component that is given each of the those props as a template. What if there was an itemAs property that behaved like as (literal tag name, component, or render function) that would be used for each item? This extends the as metaphor from single slots to a slot with an array.

Copy link
Member

Choose a reason for hiding this comment

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

the virtualization provider would likely need to live inside Combobox as a slot, since Combobox itself also needs to know what the total number of options is vs. the rendered number.

With the v9 hooks/render func composition architecture, I would have expected users to simply create their own component variants and wrap children with the virtualization provider

The authoring experience would look like this:

```js
<Combobox option={{ 'some-default-prop-on-all-options': 'test' }} data={['apple', 'banana', 'cauliflower']}>
Copy link
Member

Choose a reason for hiding this comment

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

Having been a web-dev using OUIF, this kind of shorthand API was extremely unfriendly to deal with especially when you need to support features like:

  • Async loading
  • Special actions rerendering a specific item
  • Slightly altering specific items but not all

OUIF was very well known to have a lot of onRenderX callbacks or specific onXLifecycle methods to support this kind of shorthand API, which not only resulted in more code, but actually required mappings from our own application state to specific component lifecycle methods

In contrast, Combobox and Listbox patterns do commonly take an array of data and use it to render mostly uniform items. The same is likely going to be true of components like List (if we have one) and DetailsList/Grid. For those controls, good authoring ergonomics lean more towards this:

```js
<Combobox options={['apple', 'banana', 'cucumber', ...etc]} />
Copy link
Member

Choose a reason for hiding this comment

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

Many combobox/dropdown examples that I have seen in Teams, were not this library friendly. Most items could not just be mapped to simple strings, but also required icons, specific layout changes, styles overrides etc..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ling1726 thanks for the feedback! Does it help at all that options is a slot array and not a string array? It means items could be authored as strings, slot objects, or JSX elements without needing an onRenderX function.

Copy link
Member

Choose a reason for hiding this comment

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

For the RFC I would propose that it also provides some code samples of these more complex options content

@layershifter
Copy link
Member

Sometime ago we had an offline conversation with @smhigley & @behowell, I promised to prototype a solution that will allow to avoid using arrays aka shorthand collections.

I took as an example: https://www.w3.org/TR/wai-aria-practices-1.2/examples/combobox/combobox-autocomplete-list.html
And here is where I left after two hours: https://codesandbox.io/s/practical-framework-ng2l3?file=/src/App.tsx

Implementation is super-dirty, a lot of things could be improved, but key things:

  • arrow navigation works: I am using registration via refs, elements are stored in DOM order, getNextId() & getPrevId() returning IDs for focusing
  • navigation value (i.e. active id) & input value are passed via context, context selectors are used to reduce re-renders
  • items filtering is done in options itself (that could be wrong and implemented in a different way in Reach UI, their implementation has more sense to me)

@smhigley
Copy link
Contributor Author

smhigley commented Sep 15, 2021

Adding this as a comment so it doesn't get lost:

One thing we talked about in the Combobox/Dropdown sync is creating a POC for a useOptionGroup (or more generally named) hook that uses React.children to register the keys/id pairs of its Option children (or child groups that also implement useOptionGroup). That should get us an ordered list of all child Option nodes within a parent context, at the expense of not allowing stray wrapping <div>s or other elements/components that don't implement useOptionGroup.

@behowell can correct me if I missed anything important 😄

@ling1726
Copy link
Member

Adding this as a comment so it doesn't get lost:

One thing we talked about in the Combobox/Dropdown sync is creating a POC for a useOptionGroup

Please link a POC PR when there is one, thanks !

@GeoffCoxMSFT
Copy link
Member

Combobox ended up using children for items so we don't have a pressing need to solve this problem now. We can resurrect this if we encounter a need later.

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.

7 participants