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

feat(react-jsx-runtime): implements next steps (option D) #27753

Conversation

bsunderhus
Copy link
Contributor

Previous Behavior

New Behavior

Related Issue(s)

  • Fixes #

@fabricteam
Copy link
Collaborator

fabricteam commented May 3, 2023

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
react-accordion
Accordion (including children components)
88.516 kB
26.795 kB
88.501 kB
26.845 kB
-15 B
50 B
react-alert
Alert
93.549 kB
22.537 kB
93.74 kB
22.643 kB
191 B
106 B
react-avatar
Avatar
57.797 kB
15.091 kB
57.755 kB
15.073 kB
-42 B
-18 B
react-avatar
AvatarGroup
15.646 kB
6.298 kB
15.822 kB
6.382 kB
176 B
84 B
react-avatar
AvatarGroupItem
73.973 kB
19.582 kB
73.938 kB
19.578 kB
-35 B
-4 B
react-badge
Badge
23.555 kB
7.256 kB
23.421 kB
7.226 kB
-134 B
-30 B
react-badge
CounterBadge
24.457 kB
7.559 kB
24.329 kB
7.529 kB
-128 B
-30 B
react-badge
PresenceBadge
32.135 kB
8.423 kB
32.02 kB
8.392 kB
-115 B
-31 B
react-button
Button
36.742 kB
9.5 kB
36.788 kB
9.504 kB
46 B
4 B
react-button
CompoundButton
43.896 kB
10.98 kB
43.946 kB
10.975 kB
50 B
-5 B
react-button
MenuButton
41.427 kB
10.836 kB
41.482 kB
10.845 kB
55 B
9 B
react-button
SplitButton
49.649 kB
12.42 kB
49.706 kB
12.463 kB
57 B
43 B
react-button
ToggleButton
55.024 kB
11.436 kB
55.065 kB
11.44 kB
41 B
4 B
react-card
Card - All
88.716 kB
25.114 kB
88.696 kB
25.099 kB
-20 B
-15 B
react-card
Card
83.651 kB
23.658 kB
83.525 kB
23.659 kB
-126 B
1 B
react-card
CardFooter
9.193 kB
3.892 kB
9.06 kB
3.863 kB
-133 B
-29 B
react-card
CardHeader
11.089 kB
4.588 kB
11.003 kB
4.551 kB
-86 B
-37 B
react-card
CardPreview
9.998 kB
4.24 kB
9.873 kB
4.213 kB
-125 B
-27 B
react-checkbox
Checkbox
34.5 kB
10.878 kB
34.405 kB
10.868 kB
-95 B
-10 B
react-combobox
Combobox (including child components)
87.735 kB
28.243 kB
87.65 kB
28.212 kB
-85 B
-31 B
react-combobox
Dropdown (including child components)
86.074 kB
27.848 kB
85.989 kB
27.822 kB
-85 B
-26 B
react-components
react-components: Button, FluentProvider & webLightTheme
64.899 kB
17.91 kB
64.965 kB
17.944 kB
66 B
34 B
react-components
react-components: Accordion, Button, FluentProvider, Image, Menu, Popover
206.425 kB
57.914 kB
206.488 kB
57.953 kB
63 B
39 B
react-components
react-components: FluentProvider & webLightTheme
36.132 kB
11.954 kB
36.302 kB
12.019 kB
170 B
65 B
react-datepicker-compat
DatePicker Compat
222.56 kB
59.204 kB
222.486 kB
59.165 kB
-74 B
-39 B
react-dialog
Dialog (including children components)
92.076 kB
27.492 kB
91.961 kB
27.484 kB
-115 B
-8 B
react-divider
Divider
17.441 kB
6.349 kB
17.303 kB
6.306 kB
-138 B
-43 B
react-field
Field
18.9 kB
7.083 kB
18.844 kB
7.045 kB
-56 B
-38 B
react-image
Image
11.514 kB
4.619 kB
11.691 kB
4.709 kB
177 B
90 B
react-infobutton
InfoButton
130.121 kB
39.785 kB
130.029 kB
39.767 kB
-92 B
-18 B
react-infobutton
InfoLabel
133.586 kB
40.852 kB
133.523 kB
40.868 kB
-63 B
16 B
react-input
Input
24.183 kB
7.772 kB
24.047 kB
7.716 kB
-136 B
-56 B
react-label
Label
10.139 kB
4.231 kB
10.005 kB
4.207 kB
-134 B
-24 B
react-link
Link
12.339 kB
5.105 kB
12.52 kB
5.195 kB
181 B
90 B
react-menu
Menu (including children components)
130.848 kB
39.946 kB
130.774 kB
39.9 kB
-74 B
-46 B
react-menu
Menu (including selectable components)
133.832 kB
40.479 kB
133.666 kB
40.421 kB
-166 B
-58 B
react-persona
Persona
64.718 kB
17.012 kB
64.752 kB
16.989 kB
34 B
-23 B
react-popover
Popover
117.083 kB
36.122 kB
117.278 kB
36.189 kB
195 B
67 B
react-progress
ProgressBar
13.891 kB
5.482 kB
13.757 kB
5.438 kB
-134 B
-44 B
react-provider
FluentProvider
18.079 kB
6.713 kB
18.249 kB
6.779 kB
170 B
66 B
react-radio
Radio
27.404 kB
8.722 kB
27.313 kB
8.713 kB
-91 B
-9 B
react-radio
RadioGroup
11.326 kB
4.743 kB
11.503 kB
4.821 kB
177 B
78 B
react-select
Select
25.373 kB
8.826 kB
25.241 kB
8.785 kB
-132 B
-41 B
react-slider
Slider
34.322 kB
11.099 kB
34.209 kB
11.057 kB
-113 B
-42 B
react-spinbutton
SpinButton
34.121 kB
10.421 kB
33.981 kB
10.376 kB
-140 B
-45 B
react-spinner
Spinner
21.327 kB
7.015 kB
21.247 kB
7.01 kB
-80 B
-5 B
react-switch
Switch
29.924 kB
9.342 kB
29.834 kB
9.349 kB
-90 B
7 B
react-table
DataGrid
150.868 kB
41.518 kB
150.899 kB
41.587 kB
31 B
69 B
react-table
Table (Primitives only)
45.111 kB
12.567 kB
45.191 kB
12.568 kB
80 B
1 B
react-table
Table as DataGrid
133.356 kB
34.002 kB
133.481 kB
34.027 kB
125 B
25 B
react-table
Table (Selection only)
79.125 kB
19.379 kB
79.248 kB
19.448 kB
123 B
69 B
react-table
Table (Sort only)
78.455 kB
19.187 kB
78.578 kB
19.241 kB
123 B
54 B
react-tags
Tag
22.004 kB
7.93 kB
21.923 kB
7.888 kB
-81 B
-42 B
react-text
Text - Default
12.527 kB
4.963 kB
12.705 kB
5.051 kB
178 B
88 B
react-text
Text - Wrappers
15.677 kB
5.284 kB
15.855 kB
5.368 kB
178 B
84 B
react-textarea
Textarea
27.686 kB
9.126 kB
27.53 kB
9.094 kB
-156 B
-32 B
react-tooltip
Tooltip
47.119 kB
16.528 kB
46.956 kB
16.487 kB
-163 B
-41 B
Unchanged fixtures
Package & Exports Size (minified/GZIP)
global-context
createContext
510 B
330 B
global-context
createContextSelector
537 B
342 B
react-overflow
hooks only
11.206 kB
4.266 kB
react-portal
Portal
11.676 kB
4.31 kB
react-portal-compat
PortalCompatProvider
6.473 kB
2.196 kB
react-positioning
usePositioning
24.249 kB
8.856 kB
react-utilities
SSRProvider
180 B
159 B
🤖 This report was generated against 65f0125dd6a85c3c772d63ad3350561af77addbc

@size-auditor
Copy link

size-auditor bot commented May 3, 2023

Asset size changes

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

Baseline commit: 65f0125dd6a85c3c772d63ad3350561af77addbc (build)

@fabricteam
Copy link
Collaborator

fabricteam commented May 3, 2023

Perf Analysis (@fluentui/react-components)

Scenario Render type Master Ticks PR Ticks Iterations Status
Avatar mount 578 599 5000 Possible regression
Button mount 294 302 5000 Possible regression
InfoButton mount 17 17 5000 Possible regression
SpinButton mount 1279 1334 5000 Possible regression
All results

Scenario Render type Master Ticks PR Ticks Iterations Status
Avatar mount 578 599 5000 Possible regression
Button mount 294 302 5000 Possible regression
Field mount 1009 1051 5000
FluentProvider mount 659 669 5000
FluentProviderWithTheme mount 78 85 10
FluentProviderWithTheme virtual-rerender 72 72 10
FluentProviderWithTheme virtual-rerender-with-unmount 76 76 10
InfoButton mount 17 17 5000 Possible regression
MakeStyles mount 904 875 50000
Persona mount 1601 1654 5000
SpinButton mount 1279 1334 5000 Possible regression

@layershifter
Copy link
Member

It would be good to summarize changes required from consumers/developers to upgrade to a new approach

ref,
...props,
}),
{ required: true, componentType: 'div' },
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
{ required: true, componentType: 'div' },
{ componentType: 'div' },

Is there sense to use required? It will be always defined anyway, correct?

Copy link
Contributor Author

@bsunderhus bsunderhus May 4, 2023

Choose a reason for hiding this comment

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

undefined is still a possible value if required is not provided and NonNullable is not assigned in the type. This is important to be clear also, required by itself doesn't do much, you gotta put it side by side together with the NonNullable type on the slot definition.


export type DialogContentSlots = {
root: Slot<'div'>;
root: NonNullable<Slot<'div'>>;
Copy link
Member

Choose a reason for hiding this comment

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

Will it explode if we will keep previous definition? I.e. root: Slot<'div'>;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. the previous definition is simply wrong. root slot should be non nullable and declared as required. since from this PR and forward we're finally treating root as proper slot if we don't properly declare it as non nullable and required we'll start to see it as optional in the render method.


import { createElement } from '@fluentui/react-jsx-runtime';
import { createElementNext } from '@fluentui/react-jsx-runtime';
Copy link
Member

Choose a reason for hiding this comment

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

It's draft, so it's okay to use createElementNext, but for a final change let's consider better naming

onKeyDown: handleKeyDown,
ref: useMergedRefs(ref, dialogRef),
}),
backdrop: backdropSlot,
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason why you created backdropSlot variable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe it was for the case of not having the overrides option. Because in that case you'd need a reference to the slot component definition to then modify it's properties.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But honestly, I see no harm in having overrides property there, it makes things easier

Copy link
Member

Choose a reason for hiding this comment

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

But honestly, I see no harm in having overrides property there, it makes things easier

How overrides makes it easier? :)

const slot = slot(props.slot, {
  overrides: {
    onClick: useEventCallback(() => {
      // Hm.. What should I call? 
      if (typeof props.slot === 'object' && props.slot !== null) {
        props.slot.onClick()
      }
    })
  }
})

Do we really want to do that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

typeof props.slot === 'object' && props.slot !== null this is the only problem I see, and it's unnecessary in this scenario, we have isResolvedShorthand to avoid that.

Copy link
Member

Choose a reason for hiding this comment

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

Correct, but following will be simpler and does not require checks (and we are doing this anyway):

const foo = slot()

foo.onClick = mergeCallbacks(foo.onClick, () => {})

id: useDialogContext_unstable(ctx => ctx.dialogTitleId),
...props,
}),
{ componentType: defaultComponentType, required: true },
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
{ componentType: defaultComponentType, required: true },
{ componentType: as, required: true },

Why not as?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It actually should be as in that case! otherwise there's no way to alter root base type, since getNativeElementProps will do us the favor of removing the as property 🥲

Comment on lines 27 to 32
getNativeElementProps(as, {
ref,
id: useDialogContext_unstable(ctx => ctx.dialogTitleId),
...props,
}),
{ componentType: defaultComponentType, required: true },
Copy link
Member

Choose a reason for hiding this comment

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

      getNativeElementProps(as /* element type */, {}),
      { componentType: defaultComponentType /* element type */, required: true },

This smells a bit, looks we can consider to create a special function for root slot or embed props filtering to slot()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or we could just stop prop filtering?! 👀 as we have concise types to avoid unnecessary properties?!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It makes absolutely no sense to embed props filtering to slot, it has proven to be unnecessary for every single slot and that is precisely why we don't have it in all implementations, I do believe we can move forward to a way that we stop treating root as something different than just another slot, let's just stop prop filtering root.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What are the impediments stopping us from opting out of not filtering props for the root slot?

Copy link
Member

Choose a reason for hiding this comment

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

What are the impediments stopping us from opting out of not filtering props for the root slot?

We will need to filter out component's props from props manually, which might not be so bad.

...props,
}),
action: resolveShorthand(action, {
root: slot<DialogTitleProps>(
Copy link
Member

Choose a reason for hiding this comment

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

Why do you need to specify DialogTitleProps explicitly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You don't actually. if you remove it'll work. I guess it's there for some implementation reminiscent


const slotComponent = {
...defaultProps,
$$typeof: SLOT_COMPONENT_TYPEOF_SYMBOL,
Copy link
Member

Choose a reason for hiding this comment

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

What does $$typeof? I removed it together with SLOT_COMPONENT_TYPEOF_SYMBOL and everything still works 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is a type that is defined by React.ExoticComponent, every single exotic component has that property to help react to identify what component type is that. We can remove it for sure, I just kept it there to maintain implementation similar to what react does.

Copy link
Member

Choose a reason for hiding this comment

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

Let's remove it as it does nothing for this implementation.

@@ -0,0 +1,34 @@
import * as React from 'react';
import { isSlot, UnknownSlotProps, SLOT_COMPONENT_METADATA_SYMBOL } from '@fluentui/react-utilities';
import { SlotComponent } from '@fluentui/react-utilities/src/compose/types';
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
import { SlotComponent } from '@fluentui/react-utilities/src/compose/types';
import { SlotComponent } from '@fluentui/react-utilities';

}
}

return Object.assign(slotComponent, overrides);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return Object.assign(slotComponent, overrides);
return slotComponent;

Let's remove overrides as a concept from this implementation. It had sense in Northstar as factories returned there a React element, but here it returns props. With overrides there will be two ways of doing things:

const a = slot(props.a)
slot.foo = 'foo'

// vs

const a = slot(props.a, { overrides: { foo: 'foo' })

What is a correct way? And would it should be done via overrides? 🐱

Copy link
Contributor Author

@bsunderhus bsunderhus May 4, 2023

Choose a reason for hiding this comment

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

Well, yeah. we can remove it for sure. #27753 (comment), this becomes necessary though.

And in the backdrop it'll require some conditional as it's an optional slot:

const backdropSlot = slot(backdrop, {
  componentType: 'div',
  required: open && modalType !== 'non-modal',
  defaultProps: { 'aria-hidden': 'true' },
});
if (backdropSlot) {
  backdropSlot.onClick = handledBackdropClick;
}

return {
    backdrop: backdropSlot,

@layershifter
Copy link
Member

I am in favor of Option D (this PR)

  • it's less changes ➡️ faster upgrade path
  • smaller bundle size as .props can't be minified

Anyway, before we will do any changes we need to ensure that a new release with these changes will not break consumers

ref: useMergedRefs(ref, dialogRef),
}),
backdrop: backdropSlot,
root: slot<DialogSurfaceProps>(
Copy link
Member

Choose a reason for hiding this comment

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

As componentType is a required param, is there a reason to keep it options? Did you consider following?

// Option 1
// slot(COMPONENT_TYPE, PROPS, OPTIONS)
slot('div', props.slot, { required: true })

// Option 2
// slot(OPTIONS)
slot({ elementType: 'div', props: props.slot, required: true })

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Option 1 I'm not a big fan, 3 arguments on a function brings a lot of options.

I'd be ok with Option 2, looks even simpler for me, one less argument

Copy link
Contributor Author

@bsunderhus bsunderhus May 4, 2023

Choose a reason for hiding this comment

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

here's one thing though.... on Option 2 you create the property props, that indicates we're going to be passing the props for the slot there, but we don't know if we have props, we only have props.slot which is a shorthand (which might be props of the slot, or might be only children, or null, or undefined). soooo, should we call it shorthand?

Copy link
Member

Choose a reason for hiding this comment

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

should we call it shorthand?

Or may be just value...

if (shorthand === null || (shorthand === undefined && !required)) {
return undefined;
}
const metadata: SlotComponentMetadata<Props> = { componentType };
Copy link
Member

Choose a reason for hiding this comment

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

Out of curiosity, why componentType instead of elementType?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just because 🤷🏼‍♂️. I guess it's because we have components property on the state and that would be something similar?! but yeah, elementType is more concise

@bsunderhus bsunderhus force-pushed the react-jsx-runtime/feat--implements-next-steps-(option-D) branch 2 times, most recently from 7c79e9f to cfd0e7a Compare May 10, 2023 11:51
@bsunderhus bsunderhus force-pushed the react-jsx-runtime/feat--implements-next-steps-(option-D) branch from ce22db7 to 91fa887 Compare May 12, 2023 12:18
'aria-hidden': 'true',
onClick: handledBackdropClick,
backdrop: backdropSlot,
root: rootSlot<DialogSurfaceSlots>({
Copy link
Member

Choose a reason for hiding this comment

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

Looking on it now, I am thinking may be we can be more explicit. Here is an example:

const state = {
  root: slot.root(),
  backdrop: slot.optional(),
  foo: slot.required()
}

The downside that I see: it will be harder to have conditions for required/optinal as you have in this component.

@bsunderhus WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

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

I like making this more explicit and not having a default of "required: false". This part of the slots API always seems to confuse devs working on components.

There are actually three different cases:

  • optional: only rendered if a user value is provided for the slot (unless they set the slot to null).
  • nullable: rendered by default, but can be set to null by the user to prevent rendering.
    • In the current API, this is the case if you pass required: true unless you also add NonNullable to the slot type.
  • required: always rendered; can't be set to null.

Splitting out the three cases into different slot functions would make it harder to mess up, especially mixing up nullable vs. required (since we could enforce that the slot is NonNullable if passed to the required slot function).

If we had that, I don't think we'd need to special-case the root slot; it'd just be a "required" slot.

Copy link
Contributor

Choose a reason for hiding this comment

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

It would be really great to come up with better names for the three cases as well... As always, naming is the hardest part of software engineering.

@@ -244,6 +281,10 @@ export function useScrollbarWidth(options: UseScrollbarWidthOptions): number | u
// @internal
export function useTimeout(): readonly [(fn: () => void, delay: number) => void, () => void];

// Warnings were encountered during analysis:
//
// /workspaces/fluentui/dist/out-tsc/types/packages/react-components/react-utilities/src/compose/types.d.ts:169:5 - (ae-incompatible-release-tags) The symbol "[SLOT_COMPONENT_METADATA_SYMBOL]" is marked as @public, but its signature references "SLOT_COMPONENT_METADATA_SYMBOL" which is marked as @internal
Copy link
Member

Choose a reason for hiding this comment

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

💥 ?

@bsunderhus bsunderhus force-pushed the react-jsx-runtime/feat--implements-next-steps-(option-D) branch from 91fa887 to 371489a Compare May 16, 2023 08:51
Comment on lines 69 to 176
export function rootSlot<Slots extends SlotPropsRecord, Primary extends keyof Slots = 'root'>(
options: SlotOptions<ExtractSlotProps<Slots[Primary]>> & {
props: PropsWithoutRef<ExtractSlotProps<Slots[Primary]>>;
ref: 'ref' extends keyof ExtractSlotProps<Slots[Primary]>
? ExtractSlotProps<Slots[Primary]>['ref']
: React.Ref<HTMLElement>;
},
): SlotComponent<ExtractSlotProps<Slots[Primary]>> {
const { defaultProps, elementType } = options;
const props: ExtractSlotProps<Slots[Primary]> = getNativeElementProps(elementType as string, {
ref: options.ref,
...options.props,
});
return slot({ shorthand: props, elementType, defaultProps, required: true });
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This function is called rootSlot but it appears to be getting the primary slot's props (not root), and doesn't seem to do proper prop splitting when the primary slot is not root.

Also, building in getNativeElementProps here won't work in general: Some components need to pass value to excludedPropNames. Or others use getPartitionedNativeProps (if the primary slot is not root). And some components don't have a native element as the root slot, so do manual prop splitting. Can we have callers do the prop splitting before calling rootSlot?

Copy link
Contributor

Choose a reason for hiding this comment

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

If we go with the suggestion from this comment: https://github.com/microsoft/fluentui/pull/27753/files#r1194833022, then we don't need a rootSlot() function. It should work to use slot.required().

'aria-hidden': 'true',
onClick: handledBackdropClick,
backdrop: backdropSlot,
root: rootSlot<DialogSurfaceSlots>({
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be really great to come up with better names for the three cases as well... As always, naming is the hardest part of software engineering.

Comment on lines 30 to 69
export function slot<Props extends UnknownSlotProps = UnknownSlotProps>(
options: { shorthand: Props | SlotShorthandValue | undefined | null; required?: boolean } & SlotOptions<Props>,
): SlotComponent<Props> | undefined {
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like this slot function is saving the render function in a different way than resolveShorthand (on a metadata object vs. on SLOT_RENDER_FUNCTION_SYMBOL).

That's fine in general, but it would be nice to keep interoperability between calls to resolveShorthand and slot. The main reason is that resolveShorthand is the only way for a component to modify slot props of a different component. E.g. if a Foo component has a slot of type Bar, and wants to modify the props sent to its bar slot, it would need to call resolveShorthand on the bar slot first. Here's an example where a component needs to modify the slot of another component: #27834

Here's a contrived stress test case that should work. Multiple repeated calls to resolveShorthand followed by a call to slot:

const component = <TestComponent mySlot={{ children: () => { /* render function */ }} />;

// Inside TestComponent:

// The first call moves the render function to `SLOT_RENDER_FUNCTION_SYMBOL`:
const mySlotResolved1 = resolveShorthand(props.mySlot);
// The second call should maintain the render function.
const mySlotResolved2 = resolveShorthand(mySlotResolved1);
// The call to slot should also maintain the render function.
const mySlot = slot({ shorthand: mySlotResolved2 });

// When rendering the component with mySlot, it should use the render function.

I think in order for that to work, slot would need to check for the existence of SLOT_RENDER_FUNCTION_SYMBOL and make sure it ends up in the right place on the metadata.

};

export function slot<Props extends UnknownSlotProps = UnknownSlotProps>(
options: { shorthand: Props | SlotShorthandValue | undefined; required: true } & SlotOptions<Props>,
Copy link
Contributor

Choose a reason for hiding this comment

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

The "shorthand" argument is not necessarily a shorthand value. Not sure what the best name would be. Perhaps value, which is the name that resolveShorthand uses.

Also, super-nit, the object should be called "params" or something (and the type "SlotParams"), rather than "options"/"SlotOptions" since they are not optional 🙂.

Suggested change
options: { shorthand: Props | SlotShorthandValue | undefined; required: true } & SlotOptions<Props>,
params: { value: Props | SlotShorthandValue | undefined; required: true } & SlotParams<Props>,

Comment on lines 12 to 23
function createElementFromSlotComponent<Props extends UnknownSlotProps>(
type: SlotComponent<Props>,
overrideChildren: React.ReactNode[],
): React.ReactElement<Props> | null {
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like createElementFromSlotComponent is ignoring any props set in JSX. Setting slot props in JSX is definitely not recommended, but since I don't think we can prevent it via TypeScript, then we should probably handle it here. Otherwise it will silently fail to work, and could be very confusing to a dev.

I.e. this should work, and result in the foo prop being overridden to be "bar":

<state.mySlot foo="bar" />

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Setting slot props in JSX is definitely not recommended, but since I don't think we can prevent it via TypeScript, then we should probably handle it here. Otherwise it will silently fail to work, and could be very confusing to a dev.

We can prevent it via Typescript! 💪🏻 The SlotComponent extending React.ExoticComponent<React.PropsWithChildren<{}>> makes sure that we're enforcing JSX props to be React.PropsWithChildren<{}> in other words, we'll be enforcing through Typescript that slots only support children as a valid property on override time, ensuring the only possible way to add properties to a slot is through SlotComponent creation (by slot methods)

@bsunderhus bsunderhus force-pushed the react-jsx-runtime/feat--implements-next-steps-(option-D) branch from 45f8cfd to 90c3459 Compare May 29, 2023 19:39
@bsunderhus bsunderhus force-pushed the react-jsx-runtime/feat--implements-next-steps-(option-D) branch from 90c3459 to 5a5688b Compare May 29, 2023 20:00
@bsunderhus bsunderhus force-pushed the react-jsx-runtime/feat--implements-next-steps-(option-D) branch from 5a5688b to dc6ca6c Compare May 29, 2023 20:12
@bsunderhus bsunderhus force-pushed the react-jsx-runtime/feat--implements-next-steps-(option-D) branch 2 times, most recently from aea42bb to f01e04f Compare May 29, 2023 20:47
@fabricteam
Copy link
Collaborator

fabricteam commented May 29, 2023

🕵 fluentuiv9 No visual regressions between this PR and main

@bsunderhus bsunderhus force-pushed the react-jsx-runtime/feat--implements-next-steps-(option-D) branch 14 times, most recently from 1f50ecd to 401adf5 Compare May 30, 2023 22:48
@bsunderhus bsunderhus force-pushed the react-jsx-runtime/feat--implements-next-steps-(option-D) branch from 401adf5 to b1398fa Compare May 31, 2023 10:28
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.

None yet

4 participants