Skip to content

Commit

Permalink
[Button][base] Drop component prop (mui#36677)
Browse files Browse the repository at this point in the history
  • Loading branch information
mnajdova authored and binh1298 committed May 17, 2023
1 parent 160442e commit 2b07a0d
Show file tree
Hide file tree
Showing 23 changed files with 131 additions and 138 deletions.
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} />
{/* @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

0 comments on commit 2b07a0d

Please sign in to comment.