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

[Button][base] Drop component prop #36677

Merged
merged 36 commits into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
c955845
[ButtonUnstyled] Drop `component` prop
mnajdova Mar 29, 2023
8e7ebda
docs:typescript:formatted
mnajdova Mar 29, 2023
615cfaf
Fix lint issues
mnajdova Mar 29, 2023
3b88d2d
Fix tests related to the changes of the conformance test suite
mnajdova Mar 29, 2023
42c3a54
Use React.ButtonHTMLAttributes
mnajdova Mar 29, 2023
abb7d98
remove ButtonUnstyledTypeMap usages
mnajdova Mar 29, 2023
7eecd8a
Merge branch 'master' into base/remove-component-prop
michaldudak Apr 14, 2023
aced6ee
Created a Base UI version of OverridableComponent
michaldudak Apr 17, 2023
b96cc6f
Merge remote-tracking branch 'upstream/master' into base/remove-compo…
michaldudak Apr 17, 2023
1aca46b
revert some test changes, cleanup code
mnajdova Apr 18, 2023
ee4fdb7
Rename the OverridableComponent to PolymorphicComponent
mnajdova Apr 18, 2023
f49956d
Fixed regressions in names
mnajdova Apr 18, 2023
c709d0c
update the spec tests
mnajdova Apr 18, 2023
d61f431
Fix lint issues
mnajdova Apr 19, 2023
d86107a
Update packages/mui-base/src/ButtonUnstyled/ButtonUnstyled.spec.tsx
mnajdova Apr 19, 2023
e0cc64d
Docs updates on the genric usage
mnajdova Apr 19, 2023
10cca14
update button and overriding component structure docs
samuelsycamore Apr 19, 2023
83aaa1e
Merge branch 'master' into base/remove-component-prop
mnajdova Apr 25, 2023
aa50cdc
Merge branch 'base/remove-component-prop' of https://github.com/mnajd…
mnajdova Apr 25, 2023
0c78312
prettier
mnajdova Apr 25, 2023
5c8d684
docs:api
mnajdova Apr 25, 2023
641bd26
fix some issues
mnajdova Apr 25, 2023
27e0da1
fix wrong path separators
mnajdova Apr 25, 2023
cb62291
One more fix
mnajdova Apr 25, 2023
4ebe89f
more changes
mnajdova Apr 25, 2023
41845e6
Remove occurrences of unstyled from button doc
hbjORbj Apr 25, 2023
814df7c
update button api docs
hbjORbj Apr 25, 2023
253450a
remove old files
mnajdova Apr 25, 2023
8c7a61e
Merge branch 'base/remove-component-prop' of https://github.com/mnajd…
mnajdova Apr 25, 2023
920a682
address comments
hbjORbj Apr 25, 2023
508a008
use single quote
hbjORbj Apr 25, 2023
2bb9245
prettier
hbjORbj Apr 25, 2023
fbfbd3c
remove unstyled from overriding component structure doc
hbjORbj Apr 26, 2023
9ce855e
add 1 more ts test
hbjORbj Apr 26, 2023
e089918
resolve merge conflict
hbjORbj Apr 26, 2023
b7d62fb
Merge branch 'master' into base/remove-component-prop
hbjORbj Apr 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/data/base/components/button/UnstyledButtonCustom.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ ButtonRoot.propTypes = {
};

const SvgButton = React.forwardRef(function SvgButton(props, ref) {
return <Button {...props} component={CustomButtonRoot} ref={ref} />;
return <Button {...props} slots={{ root: CustomButtonRoot }} ref={ref} />;
});

export default function UnstyledButtonCustom() {
Expand Down
2 changes: 1 addition & 1 deletion docs/data/base/components/button/UnstyledButtonCustom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const SvgButton = React.forwardRef(function SvgButton(
props: ButtonProps,
ref: React.ForwardedRef<any>,
) {
return <Button {...props} component={CustomButtonRoot} ref={ref} />;
return <Button {...props} slots={{ root: CustomButtonRoot }} ref={ref} />;
});

export default function UnstyledButtonCustom() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import Stack from '@mui/material/Stack';
export default function UnstyledButtonsDisabledFocusCustom() {
return (
<Stack spacing={2}>
<CustomButton component="span" disabled>
<CustomButton slots={{ root: 'span' }} disabled>
focusableWhenDisabled = false
</CustomButton>
<CustomButton component="span" disabled focusableWhenDisabled>
<CustomButton slots={{ root: 'span' }} disabled focusableWhenDisabled>
focusableWhenDisabled = true
</CustomButton>
</Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import * as React from 'react';
import Button, { buttonClasses, ButtonTypeMap } from '@mui/base/Button';
import { styled } from '@mui/system';
import Stack from '@mui/material/Stack';
import { OverridableComponent } from '@mui/types';
import { PolymorphicComponent } from '@mui/base/utils';

export default function UnstyledButtonsDisabledFocusCustom() {
return (
<Stack spacing={2}>
<CustomButton component="span" disabled>
<CustomButton slots={{ root: 'span' }} disabled>
focusableWhenDisabled = false
</CustomButton>
<CustomButton component="span" disabled focusableWhenDisabled>
<CustomButton slots={{ root: 'span' }} disabled focusableWhenDisabled>
focusableWhenDisabled = true
</CustomButton>
</Stack>
Expand Down Expand Up @@ -52,4 +52,4 @@ const CustomButton = styled(Button)`
opacity: 0.5;
cursor: not-allowed;
}
` as OverridableComponent<ButtonTypeMap>;
` as PolymorphicComponent<ButtonTypeMap>;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<CustomButton component="span" disabled>
<CustomButton slots={{ root: 'span' }} disabled>
focusableWhenDisabled = false
</CustomButton>
<CustomButton component="span" disabled focusableWhenDisabled>
<CustomButton slots={{ root: 'span' }} disabled focusableWhenDisabled>
focusableWhenDisabled = true
</CustomButton>
4 changes: 2 additions & 2 deletions docs/data/base/components/button/UnstyledButtonsSpan.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import Stack from '@mui/material/Stack';
export default function UnstyledButtonsSpan() {
return (
<Stack spacing={2} direction="row">
<CustomButton component="span">Button</CustomButton>
<CustomButton component="span" disabled>
<CustomButton slots={{ root: 'span' }}>Button</CustomButton>
<CustomButton slots={{ root: 'span' }} disabled>
Disabled
</CustomButton>
</Stack>
Expand Down
8 changes: 4 additions & 4 deletions docs/data/base/components/button/UnstyledButtonsSpan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import * as React from 'react';
import Button, { buttonClasses, ButtonTypeMap } from '@mui/base/Button';
import { styled } from '@mui/system';
import Stack from '@mui/material/Stack';
import { OverridableComponent } from '@mui/types';
import { PolymorphicComponent } from '@mui/base/utils';

export default function UnstyledButtonsSpan() {
return (
<Stack spacing={2} direction="row">
<CustomButton component="span">Button</CustomButton>
<CustomButton component="span" disabled>
<CustomButton slots={{ root: 'span' }}>Button</CustomButton>
<CustomButton slots={{ root: 'span' }} disabled>
Disabled
</CustomButton>
</Stack>
Expand Down Expand Up @@ -50,4 +50,4 @@ const CustomButton = styled(Button)`
opacity: 0.5;
cursor: not-allowed;
}
` as OverridableComponent<ButtonTypeMap>;
` as PolymorphicComponent<ButtonTypeMap>;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<CustomButton component="span">Button</CustomButton>
<CustomButton component="span" disabled>
<CustomButton slots={{ root: 'span' }}>Button</CustomButton>
<CustomButton slots={{ root: 'span' }} disabled>
Disabled
</CustomButton>
34 changes: 24 additions & 10 deletions docs/data/base/components/button/button.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,30 +54,44 @@ The Button component is composed of a root `<button>` slot with no interior slot
</button>
```

### Slot props
### Custom structure

:::info
The following props are available on all non-utility Base components.
See [Usage](/base/getting-started/usage/) for full details.
:::

Use the `component` prop to override the root slot with a custom element:
Use the `slots.root` prop to override the root slot with a custom element:

```jsx
<Button component="div" />
<Button slots={{ root: 'div' }} />
```

:::info
The `slots` prop is available on all non-utility Base components.
See [Overriding component structure](/base/guides/overriding-component-structure/) for full details.
:::

If you provide a non-interactive element such as a `<span>`, the Button component will automatically add the necessary accessibility attributes.

Compare the attributes on the `<span>` in this demo with the Button from the previous demo—try inspecting them both with your browser's dev tools:

{{"demo": "UnstyledButtonsSpan.js"}}

:::warning
If a Button is customized with a non-button element (i.e. `<Button component="span" />`), it will not submit the form it's in when clicked.
Similarly, `<Button component="span" type="reset">` will not reset its parent form.
If a Button is customized with a non-button element (for instance, `<Button slots={{ root: "span" }} />`), it will not submit the form it's in when clicked.
Similarly, `<Button slots={{ root: "span" }} type="reset">` will not reset its parent form.
:::

#### Usage with TypeScript

In TypeScript, you can specify the custom component type used in the `slots.root` as a generic to the unstyled component. This way, you can safely provide the custom compoenent's props directly on the compnent:

```tsx
<Button<typeof CustomComponent> slots={{ root: CustomComponent }} customProp />
```

The same applies for props specific to custom primitive elements:

```tsx
<Button<'img'> slots={{ root: 'img' }} src="button.png" />
```

## Hook

```js
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import * as React from 'react';
import Button from '@mui/base/Button';

export default function DivButton() {
return <Button component="div">Button</Button>;
return <Button slots={{ root: 'div' }}>Button</Button>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import * as React from 'react';
import Button from '@mui/base/Button';

export default function DivButton() {
return <Button component="div">Button</Button>;
return <Button slots={{ root: 'div' }}>Button</Button>;
}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<Button component="div">Button</Button>
<Button slots={{ root: 'div' }}>Button</Button>
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,17 @@ Slots are most commonly filled by HTML tags, but may also be filled by React com

All components contain a root slot that defines their primary node in the DOM tree; more complex components also contain additional interior slots named after the elements they represent.

All _non-utility_ Base UI components accept two props for overriding their rendered HTML structure:
All _non-utility_ Base UI components accept [the `slots` prop](#the-slots-prop) for overriding their rendered HTML structure.

- `component`—to override the root slot
- `slots`—to override any interior slots (when present) as well as the root

Additionally, you can pass custom props to interior slots using `slotProps`.
Additionally, you can pass custom props to [interior slots](#interior-slots) using `slotProps`.

## The root slot

The root slot represents the component's outermost element.
For simpler components, the root slot is often filled by the native HTML element that the component is intended to replace.

For example, the [Button's](/base/react-button/) root slot is a `<button>` element.
This component _only_ has a root slot; more complex components may have additional [interior slots](#interior-slots).

### The component prop

Use the `component` prop to override a component's root slot.
The demo below shows how to replace the Button's `<button>` tag with a `<div>`:

{{"demo": "OverridingRootSlot.js"}}

:::success
If you provide a non-interactive element like a `<div>` or a `<span>`, the Button will automatically add the necessary accessibility attributes.
Try inspecting the demo Button above in your browser's dev tools to see this feature in action.
:::
This component _only_ has a root slot; more complex components may have additional interior slots.

## Interior slots

Expand All @@ -49,37 +34,17 @@ For example, the [Slider](/base/react-slider/) is composed of a root `<span>` th

### The slots prop

Use the `slots` prop to replace a component's interior slots.
Use the `slots` prop to replace the elements in a component's slots, including the root.
The example below shows how to override the listbox slot in the [Select](/base/react-select/) component—a `<ul>` by default—with an `<ol>`:

{{"demo": "OverridingInternalSlot.js"}}

Note that you can also use the `slots` prop to override the root slot:

```jsx
// This:
<Select slots={{ root: 'span' }} />

// ...is the same as this:
<Select component="span">
```

But if you try to override the root slot with both `component` and `slots`, then `component` will take precedence:

```jsx
// This:
<Select component="div" slots={{ root: 'span' }} />

// ...renders as this:
<div class="MuiSelect-root" />
```

### The slotProps prop

The `slotProps` prop is an object that contains the props for all slots within a component.
You can use it to define additional custom props to pass to a component's interior slots.

For example, the code snippet below shows how to add a custom CSS class to the badge slot of the [Base UI Badge](/base/react-badge/) component:
For example, the code snippet below shows how to add a custom CSS class to the badge slot of the [Badge](/base/react-badge/) component:

```jsx
<Badge slotProps={{ badge: { className: 'my-badge' } }} />
Expand Down
1 change: 0 additions & 1 deletion docs/pages/base/api/button.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"description": "func<br>&#124;&nbsp;{ current?: { focusVisible: func } }"
}
},
"component": { "type": { "name": "elementType" } },
"disabled": { "type": { "name": "bool" }, "default": "false" },
"focusableWhenDisabled": { "type": { "name": "bool" }, "default": "false" },
"slotProps": {
Expand Down
1 change: 0 additions & 1 deletion docs/translations/api-docs-base/button/button.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"componentDescription": "The foundation for building custom-styled buttons.",
"propDescriptions": {
"action": "A ref for imperative actions. It currently only supports <code>focusVisible()</code> action.",
"component": "The component used for the root node. Either a string to use a HTML element or a component.",
"disabled": "If <code>true</code>, the component is disabled.",
"focusableWhenDisabled": "If <code>true</code>, allows a disabled button to receive focus.",
"slotProps": "The props used for each slot inside the Button.",
Expand Down
20 changes: 13 additions & 7 deletions packages/api-docs-builder/ApiBuilders/ComponentApiBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ const generateApiPage = (
reactApi: ReactApi,
onlyJsonFile: boolean = false,
) => {
const normalizedApiPathname = reactApi.apiPathname.replace(/\\/g, '/');
/**
* Gather the metadata needed for the component's API page.
*/
Expand Down Expand Up @@ -351,7 +352,7 @@ const generateApiPage = (
}),
spread: reactApi.spread,
themeDefaultProps: reactApi.themeDefaultProps,
muiName: reactApi.apiPathname.startsWith('/joy-ui')
muiName: normalizedApiPathname.startsWith('/joy-ui')
? reactApi.muiName.replace('Mui', 'Joy')
: reactApi.muiName,
forwardsRefTo: reactApi.forwardsRefTo,
Expand Down Expand Up @@ -421,14 +422,16 @@ const attachTranslations = (reactApi: ReactApi) => {
let description = generatePropDescription(prop, propName);
description = renderMarkdownInline(description);

const normalizedApiPathname = reactApi.apiPathname.replace(/\\/g, '/');

if (propName === 'classes') {
description += ' See <a href="#css">CSS API</a> below for more details.';
} else if (propName === 'sx') {
description +=
' See the <a href="/system/getting-started/the-sx-prop/">`sx` page</a> for more details.';
} else if (propName === 'slots' && !reactApi.apiPathname.startsWith('/material-ui')) {
} else if (propName === 'slots' && !normalizedApiPathname.startsWith('/material-ui')) {
description += ' See <a href="#slots">Slots API</a> below for more details.';
} else if (reactApi.apiPathname.startsWith('/joy-ui')) {
} else if (normalizedApiPathname.startsWith('/joy-ui')) {
switch (propName) {
case 'size':
description +=
Expand Down Expand Up @@ -682,22 +685,25 @@ export default async function generateComponentApi(
// eslint-disable-next-line no-console
console.log('Built API docs for', reactApi.name);

const normalizedApiPathname = reactApi.apiPathname.replace(/\\/g, '/');
const normalizedFilename = reactApi.filename.replace(/\\/g, '/');

if (!skipApiGeneration) {
// Generate pages, json and translations
let translationPagesDirectory = 'docs/translations/api-docs';
if (reactApi.apiPathname.startsWith('/joy-ui') && reactApi.filename.includes('mui-joy/src')) {
if (normalizedApiPathname.startsWith('/joy-ui') && normalizedFilename.includes('mui-joy/src')) {
translationPagesDirectory = 'docs/translations/api-docs-joy';
} else if (
reactApi.apiPathname.startsWith('/base') &&
reactApi.filename.includes('mui-base/src')
normalizedApiPathname.startsWith('/base') &&
normalizedFilename.includes('mui-base/src')
) {
translationPagesDirectory = 'docs/translations/api-docs-base';
}

generateApiTranslations(path.join(process.cwd(), translationPagesDirectory), reactApi);

// Once we have the tabs API in all projects, we can make this default
const generateOnlyJsonFile = reactApi.apiPathname.startsWith('/base');
const generateOnlyJsonFile = normalizedApiPathname.startsWith('/base');
generateApiPage(apiPagesDirectory, translationPagesDirectory, reactApi, generateOnlyJsonFile);

// Add comment about demo & api links (including inherited component) to the component file
Expand Down
25 changes: 19 additions & 6 deletions packages/mui-base/src/Button/Button.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,37 @@ const polymorphicComponentTest = () => {
return <div />;
};

const Root = function Root() {
return <div />;
};

return (
<div>
{/* @ts-expect-error */}
<Button invalidProp={0} />

<Button component="a" href="#" />
<Button slots={{ root: 'a' }} href="#" />

<Button component={CustomComponent} stringProp="test" numberProp={0} />
hbjORbj marked this conversation as resolved.
Show resolved Hide resolved
{/* @ts-expect-error */}
<Button component={CustomComponent} />
<Button<typeof CustomComponent>
slots={{ root: CustomComponent }}
stringProp="test"
numberProp={0}
/>

{/* @ts-expect-error onClick must be specified in the custom root component */}
<Button<typeof Root> slots={{ root: Root }} onClick={() => {}} />

{/* @ts-expect-error required props not specified */}
<Button<typeof CustomComponent> slots={{ root: CustomComponent }} />

<Button<'svg'> viewBox="" />

<Button
component="button"
onClick={(e: React.MouseEvent<HTMLButtonElement>) => e.currentTarget.checkValidity()}
/>

<Button<'div'>
component="div"
slotProps={{ root: 'div' }}
ref={(elem) => {
expectType<HTMLDivElement | null, typeof elem>(elem);
}}
Expand Down
Loading