From 1c248897d5c1724397eaa41553ca56ecf48817e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Thu, 29 Feb 2024 09:45:47 +0100 Subject: [PATCH 01/32] Component-per-node API --- .../system/index.tsx | 39 ++-- packages/mui-base/src/Switch/Switch.test.tsx | 31 +--- packages/mui-base/src/Switch/Switch.tsx | 175 ++++++------------ packages/mui-base/src/Switch/Switch.types.ts | 126 +++++-------- packages/mui-base/src/Switch/SwitchContext.ts | 6 + packages/mui-base/src/Switch/SwitchThumb.tsx | 38 ++++ packages/mui-base/src/Switch/index.ts | 9 +- packages/mui-base/src/Switch/switchClasses.ts | 40 ---- packages/mui-base/src/useSwitch/useSwitch.ts | 27 ++- .../mui-base/src/useSwitch/useSwitch.types.ts | 16 ++ .../src/utils/combineComponentExports.test.ts | 27 +++ .../src/utils/combineComponentExports.ts | 11 ++ .../mui-base/src/utils/resolveClassName.ts | 13 ++ 13 files changed, 261 insertions(+), 297 deletions(-) create mode 100644 packages/mui-base/src/Switch/SwitchContext.ts create mode 100644 packages/mui-base/src/Switch/SwitchThumb.tsx delete mode 100644 packages/mui-base/src/Switch/switchClasses.ts create mode 100644 packages/mui-base/src/utils/combineComponentExports.test.ts create mode 100644 packages/mui-base/src/utils/combineComponentExports.ts create mode 100644 packages/mui-base/src/utils/resolveClassName.ts diff --git a/docs/data/base/components/switch/UnstyledSwitchIntroduction/system/index.tsx b/docs/data/base/components/switch/UnstyledSwitchIntroduction/system/index.tsx index 8a8fe6ee47..a79256e673 100644 --- a/docs/data/base/components/switch/UnstyledSwitchIntroduction/system/index.tsx +++ b/docs/data/base/components/switch/UnstyledSwitchIntroduction/system/index.tsx @@ -1,40 +1,29 @@ import * as React from 'react'; import { styled } from '@mui/system'; -import { Switch, switchClasses } from '@mui/base/Switch'; +import { Switch } from '@mui/base/Switch'; export default function UnstyledSwitchIntroduction() { const label = { slotProps: { input: { 'aria-label': 'Demo switch' } } }; return (
+ } {...label} defaultChecked> + + + } {...label}> + + } {...label} defaultChecked - /> - - - + > + + + } {...label} disabled> + +
); } diff --git a/packages/mui-base/src/Switch/Switch.test.tsx b/packages/mui-base/src/Switch/Switch.test.tsx index 7ebe7e669e..dafd37c0d7 100644 --- a/packages/mui-base/src/Switch/Switch.test.tsx +++ b/packages/mui-base/src/Switch/Switch.test.tsx @@ -1,38 +1,12 @@ import * as React from 'react'; import { createMount, createRenderer } from '@mui/internal-test-utils'; import { expect } from 'chai'; -import { Switch, SwitchOwnerState, switchClasses } from '@mui/base/Switch'; +import { Switch, SwitchOwnerState } from '@mui/base/Switch'; import { describeConformanceUnstyled } from '../../test/describeConformanceUnstyled'; describe('', () => { - const mount = createMount(); const { render } = createRenderer(); - describeConformanceUnstyled(, () => ({ - inheritComponent: 'span', - render, - mount, - refInstanceof: window.HTMLSpanElement, - testComponentPropWith: 'span', - slots: { - root: { - expectedClassName: switchClasses.root, - }, - thumb: { - expectedClassName: switchClasses.thumb, - }, - input: { - testWithElement: 'input', - expectedClassName: switchClasses.input, - }, - track: { - expectedClassName: switchClasses.track, - isOptional: true, - }, - }, - skip: ['componentProp'], - })); - describe('componentState', () => { it('passes the ownerState prop to all the slots', () => { interface CustomSlotProps { @@ -48,7 +22,6 @@ describe('', () => { data-checked={sp.checked} data-disabled={sp.disabled} data-readonly={sp.readOnly} - data-focusvisible={sp.focusVisible} data-testid="custom" > {children} @@ -63,7 +36,7 @@ describe('', () => { thumb: CustomSlot, }; - const { getAllByTestId } = render(); + const { getAllByTestId } = render(); const renderedComponents = getAllByTestId('custom'); expect(renderedComponents.length).to.equal(3); diff --git a/packages/mui-base/src/Switch/Switch.tsx b/packages/mui-base/src/Switch/Switch.tsx index 45997114e8..d23e9863de 100644 --- a/packages/mui-base/src/Switch/Switch.tsx +++ b/packages/mui-base/src/Switch/Switch.tsx @@ -1,40 +1,15 @@ 'use client'; import * as React from 'react'; import PropTypes from 'prop-types'; -import { PolymorphicComponent } from '../utils/PolymorphicComponent'; -import { unstable_composeClasses as composeClasses } from '../composeClasses'; import { useSwitch } from '../useSwitch'; -import { - SwitchProps, - SwitchOwnerState, - SwitchInputSlotProps, - SwitchRootSlotProps, - SwitchThumbSlotProps, - SwitchTrackSlotProps, - SwitchTypeMap, -} from './Switch.types'; -import { useSlotProps, WithOptionalOwnerState } from '../utils'; -import { useClassNamesOverride } from '../utils/ClassNameConfigurator'; -import { getSwitchUtilityClass } from './switchClasses'; +import { SwitchProps, SwitchOwnerState } from './Switch.types'; +import { resolveClassName } from '../utils/resolveClassName'; +import { SwitchContext } from './SwitchContext'; -const useUtilityClasses = (ownerState: SwitchOwnerState) => { - const { checked, disabled, focusVisible, readOnly } = ownerState; - - const slots = { - root: [ - 'root', - checked && 'checked', - disabled && 'disabled', - focusVisible && 'focusVisible', - readOnly && 'readOnly', - ], - thumb: ['thumb'], - input: ['input'], - track: ['track'], - }; - - return composeClasses(slots, useClassNamesOverride(getSwitchUtilityClass)); -}; +function defaultRender(props: React.ComponentPropsWithRef<'button'>) { + // eslint-disable-next-line react/button-has-type + return + ; + + ); + } + + const { getByRole, getByText } = render(); + const switchElement = getByRole('switch'); + const button = getByText('Toggle'); + + expect(switchElement).to.have.attribute('aria-checked', 'false'); + act(() => { + button.click(); + }); + + expect(switchElement).to.have.attribute('aria-checked', 'true'); + + act(() => { + button.click(); + }); + + expect(switchElement).to.have.attribute('aria-checked', 'false'); + }); + + it('should call onChange when clicked', () => { + const handleChange = spy(); + const { getByRole, container } = render(); + const switchElement = getByRole('switch'); + const internalInput = container.querySelector('input[type="checkbox"]')!; + + act(() => { + switchElement.click(); + }); + + expect(handleChange.callCount).to.equal(1); + expect(handleChange.firstCall.args[0].target).to.equal(internalInput); + }); + describe('prop: disabled', () => { it('should have the `aria-disabled` attribute', () => { const { getByRole } = render(); @@ -165,4 +209,92 @@ describe('', () => { expect(thumb).to.have.attribute('data-readonly', 'true'); expect(thumb).to.have.attribute('data-required', 'true'); }); + + it('should set the name attribute on the input', () => { + const { container } = render(); + const internalInput = container.querySelector('input[type="checkbox"]')! as HTMLInputElement; + + expect(internalInput).to.have.attribute('name', 'switch-name'); + }); + + describe('form handling', () => { + it('should toggle the switch when a parent label is clicked', () => { + const { getByTestId, getByRole } = render( + , + ); + + const switchElement = getByRole('switch'); + const label = getByTestId('label'); + + expect(switchElement).to.have.attribute('aria-checked', 'false'); + + act(() => { + label.click(); + }); + + expect(switchElement).to.have.attribute('aria-checked', 'true'); + }); + + it('should toggle the switch when a linked label is clicked', () => { + const { getByTestId, getByRole } = render( +
+ + +
, + ); + + const switchElement = getByRole('switch'); + const label = getByTestId('label'); + + expect(switchElement).to.have.attribute('aria-checked', 'false'); + + act(() => { + label.click(); + }); + + expect(switchElement).to.have.attribute('aria-checked', 'true'); + }); + }); + + it('should include the switch value in the form submission', function test() { + if (/jsdom/.test(window.navigator.userAgent)) { + // FormData is not available in JSDOM + this.skip(); + } + + let stringifiedFormData = ''; + + const { getByRole } = render( +
{ + event.preventDefault(); + const formData = new FormData(event.currentTarget); + stringifiedFormData = new URLSearchParams(formData as any).toString(); + }} + > + + + , + ); + + const switchElement = getByRole('switch'); + const submitButton = getByRole('button')!; + + submitButton.click(); + + expect(stringifiedFormData).to.equal('test-switch=off'); + + act(() => { + switchElement.click(); + }); + + submitButton.click(); + + expect(stringifiedFormData).to.equal('test-switch=on'); + }); }); diff --git a/packages/mui-base/src/Switch/Switch.tsx b/packages/mui-base/src/Switch/Switch.tsx index 8b34b38040..f798cbdb87 100644 --- a/packages/mui-base/src/Switch/Switch.tsx +++ b/packages/mui-base/src/Switch/Switch.tsx @@ -69,6 +69,7 @@ const Switch = React.forwardRef(function Switch( return ( {render(getButtonProps(buttonProps), ownerState)} + {!checked && } ); diff --git a/packages/mui-base/src/useSwitch/useSwitch.ts b/packages/mui-base/src/useSwitch/useSwitch.ts index dc0b23db86..94316137e5 100644 --- a/packages/mui-base/src/useSwitch/useSwitch.ts +++ b/packages/mui-base/src/useSwitch/useSwitch.ts @@ -2,6 +2,7 @@ import * as React from 'react'; import { useControlled } from '../utils/useControlled'; import { UseSwitchParameters, UseSwitchReturnValue } from './useSwitch.types'; +import { useForkRef } from '../utils/useForkRef'; /** * The basic building block for creating custom switches. @@ -14,8 +15,20 @@ import { UseSwitchParameters, UseSwitchReturnValue } from './useSwitch.types'; * * - [useSwitch API](https://mui.com/base-ui/react-switch/hooks-api/#use-switch) */ -export function useSwitch(props: UseSwitchParameters): UseSwitchReturnValue { - const { checked: checkedProp, defaultChecked, disabled, onChange, readOnly, required } = props; +export function useSwitch(params: UseSwitchParameters): UseSwitchReturnValue { + const { + checked: checkedProp, + defaultChecked, + disabled, + name, + onChange, + readOnly, + required, + inputRef: externalInputRef, + } = params; + + const inputRef = React.useRef(null); + const handleInputRef = useForkRef(inputRef, externalInputRef); const [checked, setCheckedState] = useControlled({ controlled: checkedProp, @@ -41,11 +54,11 @@ export function useSwitch(props: UseSwitchParameters): UseSwitchReturnValue { (otherProps: React.ButtonHTMLAttributes) => (event: React.MouseEvent) => { otherProps.onClick?.(event); - if (disabled || readOnly) { + if (event.defaultPrevented || readOnly) { return; } - setCheckedState((prevChecked) => !prevChecked); + inputRef.current?.click(); }; const getButtonProps: UseSwitchReturnValue['getButtonProps'] = (otherProps = {}) => ({ @@ -61,13 +74,14 @@ export function useSwitch(props: UseSwitchParameters): UseSwitchReturnValue { const getInputProps: UseSwitchReturnValue['getInputProps'] = (otherProps = {}) => ({ checked, disabled, - readOnly, + name, required, + style: { opacity: 0, width: 0, height: 0, margin: 0, padding: 0, overflow: 'hidden' }, + tabIndex: -1, type: 'checkbox', 'aria-hidden': true, - tabIndex: -1, - style: { opacity: 0, width: 0, height: 0, margin: 0, padding: 0, overflow: 'hidden' }, ...otherProps, + ref: handleInputRef, onChange: createHandleInputChange(otherProps), }); diff --git a/packages/mui-base/src/useSwitch/useSwitch.types.ts b/packages/mui-base/src/useSwitch/useSwitch.types.ts index d36cbcec2b..0cf27ae9e3 100644 --- a/packages/mui-base/src/useSwitch/useSwitch.types.ts +++ b/packages/mui-base/src/useSwitch/useSwitch.types.ts @@ -13,6 +13,14 @@ export interface UseSwitchParameters { * If `true`, the component is disabled. */ disabled?: boolean; + /** + * Ref to the underlying input element. + */ + inputRef?: React.Ref; + /** + * Name of the underlying input element. + */ + name?: string; /** * Callback fired when the state is changed. * @@ -32,13 +40,15 @@ export interface UseSwitchParameters { } interface UseSwitchInputSlotOwnProps { - checked?: boolean; - defaultChecked?: boolean; + checked: boolean; disabled?: boolean; - onChange: React.ChangeEventHandler; - readOnly?: boolean; + name?: string; required?: boolean; - type: React.HTMLInputTypeAttribute; + style: React.CSSProperties; + type: 'checkbox'; + 'aria-hidden': React.AriaAttributes['aria-hidden']; + ref: React.RefCallback | null; + onChange: React.ChangeEventHandler; } export type UseSwitchInputSlotProps = Omit & @@ -46,9 +56,11 @@ export type UseSwitchInputSlotProps = Omit; - role: React.AriaRole; + type: 'button'; + role: 'switch'; 'aria-disabled': React.AriaAttributes['aria-disabled']; 'aria-checked': React.AriaAttributes['aria-checked']; + 'aria-readonly': React.AriaAttributes['aria-readonly']; } export type UseSwitchButtonSlotProps = Omit< diff --git a/packages/mui-base/src/utils/useForkRef.ts b/packages/mui-base/src/utils/useForkRef.ts new file mode 100644 index 0000000000..ef39f7f3dd --- /dev/null +++ b/packages/mui-base/src/utils/useForkRef.ts @@ -0,0 +1 @@ +export { default as useForkRef } from '@mui/utils/useForkRef'; From 54805b544e7666418d5df4342c326443cfa72cf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Fri, 1 Mar 2024 09:26:09 +0100 Subject: [PATCH 11/32] Do not render hidden input if name isn't provided --- packages/mui-base/src/Switch/Switch.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mui-base/src/Switch/Switch.tsx b/packages/mui-base/src/Switch/Switch.tsx index f798cbdb87..a2313f8b0b 100644 --- a/packages/mui-base/src/Switch/Switch.tsx +++ b/packages/mui-base/src/Switch/Switch.tsx @@ -69,7 +69,7 @@ const Switch = React.forwardRef(function Switch( return ( {render(getButtonProps(buttonProps), ownerState)} - {!checked && } + {!checked && props.name && } ); From bd8d651e7497b09160ba7066ba5e59371bda83f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Fri, 1 Mar 2024 09:31:48 +0100 Subject: [PATCH 12/32] Update the landing page theme preview --- .../productBaseUI/BaseUIThemesDemo.tsx | 104 +----------------- .../productBaseUI/themed-controls/Switch.tsx | 70 ++++++++++++ 2 files changed, 75 insertions(+), 99 deletions(-) create mode 100644 docs/src/components/productBaseUI/themed-controls/Switch.tsx diff --git a/docs/src/components/productBaseUI/BaseUIThemesDemo.tsx b/docs/src/components/productBaseUI/BaseUIThemesDemo.tsx index 22214da933..2d3a02153d 100644 --- a/docs/src/components/productBaseUI/BaseUIThemesDemo.tsx +++ b/docs/src/components/productBaseUI/BaseUIThemesDemo.tsx @@ -14,7 +14,6 @@ import { Select } from '@mui/base/Select'; import { Slider, sliderClasses } from '@mui/base/Slider'; import { Snackbar } from '@mui/base/Snackbar'; import { SnackbarCloseReason } from '@mui/base/useSnackbar'; -import { Switch, switchClasses } from '@mui/base/Switch'; import { Tab } from '@mui/base/Tab'; import { Tabs } from '@mui/base/Tabs'; import { TabsList } from '@mui/base/TabsList'; @@ -45,6 +44,9 @@ import ROUTES from 'docs/src/route'; import { Link } from '@mui/docs/Link'; import heroVariables from 'docs/src/components/productBaseUI/heroVariables'; +// DS imports +import Switch from './themed-controls/Switch'; + const Panel = styled('div')({ width: 340, backgroundColor: 'var(--muidocs-palette-background-paper)', @@ -429,87 +431,6 @@ const StyledSlider = styled(Slider)(` } `); -const StyledSwitch = styled('span')(` - font-size: 0; - position: relative; - display: inline-block; - width: 34px; - height: 20px; - cursor: pointer; - - - &.${switchClasses.disabled} { - opacity: 0.4; - cursor: not-allowed; - } - - & .${switchClasses.track} { - background: var(--Switch-background, var(--muidocs-palette-grey-300)); - border-radius: max(2px, var(--border-radius) * 4); - display: block; - height: 100%; - width: 100%; - position: absolute; - transition: background-color ease 100ms; - - } - - :hover { - .${switchClasses.track} { - background: var(--Switch-hoverBackground, var(--muidocs-palette-grey-400)); - } - } - - & .${switchClasses.thumb} { - display: block; - width: 14px; - height: 14px; - top: 3px; - left: 3px; - border-radius: max(2px, var(--border-radius)); - background-color: #fff; - position: relative; - transition-property: left; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 120ms; - } - - &.${switchClasses.focusVisible} { - border-radius: max(2px, var(--border-radius) * 4); - outline: 3px solid var(--muidocs-palette-primary-300); - } - - &.${switchClasses.checked} { - .${switchClasses.thumb} { - left: 17px; - top: 3px; - background-color: #fff; - } - - .${switchClasses.track} { - background: var(--muidocs-palette-primary-500); - } - - :hover { - .${switchClasses.track} { - background: var(--muidocs-palette-primary-700); - } - } - } - - & .${switchClasses.input} { - cursor: inherit; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - opacity: 0; - z-index: 1; - margin: 0; - } - `); - const Backdrop = React.forwardRef( (props, ref) => { const { open, className, ...other } = props; @@ -972,26 +893,11 @@ export default function BaseUIThemesDemo() { > Make it your own - + Use every component - + {/* Modal and Snackbar component */} diff --git a/docs/src/components/productBaseUI/themed-controls/Switch.tsx b/docs/src/components/productBaseUI/themed-controls/Switch.tsx new file mode 100644 index 0000000000..47dda86e2f --- /dev/null +++ b/docs/src/components/productBaseUI/themed-controls/Switch.tsx @@ -0,0 +1,70 @@ +import * as React from 'react'; +import { Switch as BaseSwitch, SwitchProps } from '@mui/base/Switch'; +import { css, styled } from '@mui/system'; + +const StyledSwitch = styled(BaseSwitch)(css` + font-size: 0; + position: relative; + display: inline-block; + width: 34px; + height: 20px; + cursor: pointer; + background: var(--Switch-background, var(--muidocs-palette-grey-300)); + border-radius: max(2px, var(--border-radius) * 4); + transition: background-color ease 100ms; + border: none; + padding: 0; + + &[data-disabled] { + opacity: 0.4; + cursor: not-allowed; + } + + $:hover { + background: var(--Switch-hoverBackground, var(--muidocs-palette-grey-400)); + } + + &:focus-visible { + border-radius: max(2px, var(--border-radius) * 4); + outline: 3px solid var(--muidocs-palette-primary-300); + } + + &[data-state='checked'] { + background: var(--muidocs-palette-primary-500); + } +`); + +const StyledSwitchThumb = styled(BaseSwitch.Thumb)(css` + display: block; + width: 14px; + height: 14px; + left: 3px; + border-radius: max(2px, var(--border-radius)); + background-color: #fff; + position: relative; + transition-property: left; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 120ms; + + &[data-state='checked'] { + left: 17px; + background-color: #fff; + + &:hover { + background: var(--muidocs-palette-primary-700); + } + } +`); + +const Switch = React.forwardRef(function Switch( + props: SwitchProps, + ref: React.ForwardedRef, +) { + return ( + + + + ); +}); + +export default Switch; From 27f0b7e0a462b97c78a13488a7af06b3f177bc03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Fri, 1 Mar 2024 09:32:11 +0100 Subject: [PATCH 13/32] Rewrite the Switch on Badge demos --- .../base/components/badge/BadgeVisibility.js | 112 +++++++++--------- .../base/components/badge/BadgeVisibility.tsx | 112 +++++++++--------- 2 files changed, 116 insertions(+), 108 deletions(-) diff --git a/docs/data/base/components/badge/BadgeVisibility.js b/docs/data/base/components/badge/BadgeVisibility.js index 1acc876e0b..880ff49e4b 100644 --- a/docs/data/base/components/badge/BadgeVisibility.js +++ b/docs/data/base/components/badge/BadgeVisibility.js @@ -3,7 +3,7 @@ import { Badge as BaseBadge, badgeClasses } from '@mui/base/Badge'; // Auxiliary demo components import { styled, Stack } from '@mui/system'; import { Button, buttonClasses } from '@mui/base/Button'; -import { Switch, switchClasses } from '@mui/base/Switch'; +import { Switch as BaseSwitch } from '@mui/base/Switch'; import Divider from '@mui/material/Divider'; // Icons import AddIcon from '@mui/icons-material/Add'; @@ -13,6 +13,7 @@ import MailIcon from '@mui/icons-material/Mail'; const blue = { 200: '#99CCF3', 500: '#007FFF', + 700: '#0059B2', }; const grey = { @@ -95,66 +96,73 @@ const StyledButton = styled(Button)( `, ); -const Root = styled('span')( +const Switch = styled(BaseSwitch)( ({ theme }) => ` - position: relative; + width: 38px; + height: 24px; + margin: 10px; + padding: 0; + box-sizing: border-box; + background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[800] : grey[200]}; + border-radius: 24px; display: inline-block; - width: 32px; - height: 20px; - cursor: pointer; + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 120ms; + box-shadow: inset 0px 1px 1px ${ + theme.palette.mode === 'dark' ? 'rgba(0, 0, 0, 0.5)' : 'rgba(0, 0, 0, 0.05)' + }; + &[data-disabled] { + opacity: 0.4; + cursor: not-allowed; + } - & .${switchClasses.track} { - background: ${theme.palette.mode === 'dark' ? grey[600] : grey[400]}; - border-radius: 16px; - display: block; - height: 100%; - width: 100%; - position: absolute; + &:hover:not([data-disabled]) { + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; + border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]}; } - & .${switchClasses.thumb} { - position: relative; + &:focus-visible { + box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[700] : blue[200]}; + } + + &[data-state="checked"] { + border: none; + background: ${blue[500]}; + } + + &[data-state="checked"]:not([data-disabled]):hover { + background: ${blue[700]}; + } + `, +); + +const Thumb = styled(BaseSwitch.Thumb)( + ({ theme }) => ` + box-sizing: border-box; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[800] : grey[200]}; display: block; - width: 14px; - height: 14px; - top: 3px; - left: 3px; + width: 16px; + height: 16px; + left: 4px; border-radius: 16px; - background-color: #fff; + background-color: #FFF; + position: relative; transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 120ms; - } - - &.${switchClasses.focusVisible} .${switchClasses.track} { - box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? grey[700] : blue[200]}; - } + box-shadow: 0px 1px 2px ${ + theme.palette.mode === 'dark' ? 'rgba(0, 0, 0, 0.25)' : 'rgba(0, 0, 0, 0.1)' + }; - &.${switchClasses.checked} { - .${switchClasses.thumb} { - left: 15px; - top: 3px; + &[data-state="checked"] { + left: 18px; background-color: #fff; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3); } - - .${switchClasses.track} { - background: ${blue[500]}; - } - } - - & .${switchClasses.input} { - cursor: inherit; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - opacity: 0; - z-index: 1; - margin: 0; - } - `, +`, ); const StyledLabel = styled('label')( @@ -205,13 +213,9 @@ export default function BadgeVisibility() { Show badge - + + + diff --git a/docs/data/base/components/badge/BadgeVisibility.tsx b/docs/data/base/components/badge/BadgeVisibility.tsx index 1acc876e0b..880ff49e4b 100644 --- a/docs/data/base/components/badge/BadgeVisibility.tsx +++ b/docs/data/base/components/badge/BadgeVisibility.tsx @@ -3,7 +3,7 @@ import { Badge as BaseBadge, badgeClasses } from '@mui/base/Badge'; // Auxiliary demo components import { styled, Stack } from '@mui/system'; import { Button, buttonClasses } from '@mui/base/Button'; -import { Switch, switchClasses } from '@mui/base/Switch'; +import { Switch as BaseSwitch } from '@mui/base/Switch'; import Divider from '@mui/material/Divider'; // Icons import AddIcon from '@mui/icons-material/Add'; @@ -13,6 +13,7 @@ import MailIcon from '@mui/icons-material/Mail'; const blue = { 200: '#99CCF3', 500: '#007FFF', + 700: '#0059B2', }; const grey = { @@ -95,66 +96,73 @@ const StyledButton = styled(Button)( `, ); -const Root = styled('span')( +const Switch = styled(BaseSwitch)( ({ theme }) => ` - position: relative; + width: 38px; + height: 24px; + margin: 10px; + padding: 0; + box-sizing: border-box; + background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]}; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[800] : grey[200]}; + border-radius: 24px; display: inline-block; - width: 32px; - height: 20px; - cursor: pointer; + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 120ms; + box-shadow: inset 0px 1px 1px ${ + theme.palette.mode === 'dark' ? 'rgba(0, 0, 0, 0.5)' : 'rgba(0, 0, 0, 0.05)' + }; + &[data-disabled] { + opacity: 0.4; + cursor: not-allowed; + } - & .${switchClasses.track} { - background: ${theme.palette.mode === 'dark' ? grey[600] : grey[400]}; - border-radius: 16px; - display: block; - height: 100%; - width: 100%; - position: absolute; + &:hover:not([data-disabled]) { + background: ${theme.palette.mode === 'dark' ? grey[800] : grey[100]}; + border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]}; } - & .${switchClasses.thumb} { - position: relative; + &:focus-visible { + box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[700] : blue[200]}; + } + + &[data-state="checked"] { + border: none; + background: ${blue[500]}; + } + + &[data-state="checked"]:not([data-disabled]):hover { + background: ${blue[700]}; + } + `, +); + +const Thumb = styled(BaseSwitch.Thumb)( + ({ theme }) => ` + box-sizing: border-box; + border: 1px solid ${theme.palette.mode === 'dark' ? grey[800] : grey[200]}; display: block; - width: 14px; - height: 14px; - top: 3px; - left: 3px; + width: 16px; + height: 16px; + left: 4px; border-radius: 16px; - background-color: #fff; + background-color: #FFF; + position: relative; transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 120ms; - } - - &.${switchClasses.focusVisible} .${switchClasses.track} { - box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? grey[700] : blue[200]}; - } + box-shadow: 0px 1px 2px ${ + theme.palette.mode === 'dark' ? 'rgba(0, 0, 0, 0.25)' : 'rgba(0, 0, 0, 0.1)' + }; - &.${switchClasses.checked} { - .${switchClasses.thumb} { - left: 15px; - top: 3px; + &[data-state="checked"] { + left: 18px; background-color: #fff; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3); } - - .${switchClasses.track} { - background: ${blue[500]}; - } - } - - & .${switchClasses.input} { - cursor: inherit; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - opacity: 0; - z-index: 1; - margin: 0; - } - `, +`, ); const StyledLabel = styled('label')( @@ -205,13 +213,9 @@ export default function BadgeVisibility() { Show badge - + + + From ea047852b9f1dc5e1ed6f4c0739a598af18d4d07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Fri, 1 Mar 2024 09:32:29 +0100 Subject: [PATCH 14/32] Clear the customization page --- .../customization/CreateSlot.tsx.preview | 3 - .../customization/DisabledDefaultClasses.js | 91 ------- .../customization/DisabledDefaultClasses.tsx | 91 ------- .../DisabledDefaultClasses.tsx.preview | 14 -- .../customization/PrepareForSlot.js | 14 -- .../customization/PrepareForSlot.tsx | 14 -- .../customization/PrepareForSlot.tsx.preview | 3 - .../customization/SlotPropsCallback.js | 71 ------ .../customization/SlotPropsCallback.tsx | 71 ------ .../SlotPropsCallback.tsx.preview | 2 - .../customization/StylingCustomCss.js | 58 ----- .../customization/StylingCustomCss.tsx | 58 ----- .../StylingCustomCss.tsx.preview | 2 - .../customization/StylingHooks.js | 81 ------ .../customization/StylingHooks.tsx | 81 ------ .../customization/StylingHooks.tsx.preview | 1 - .../customization/StylingSlots.js | 65 ----- .../customization/StylingSlots.tsx | 65 ----- .../customization/StylingSlots.tsx.preview | 1 - .../StylingSlotsSingleComponent.js | 63 ----- .../StylingSlotsSingleComponent.tsx | 63 ----- .../StylingSlotsSingleComponent.tsx.preview | 1 - .../customization/customization.md | 231 +----------------- 23 files changed, 3 insertions(+), 1141 deletions(-) delete mode 100644 docs/data/base/getting-started/customization/CreateSlot.tsx.preview delete mode 100644 docs/data/base/getting-started/customization/DisabledDefaultClasses.js delete mode 100644 docs/data/base/getting-started/customization/DisabledDefaultClasses.tsx delete mode 100644 docs/data/base/getting-started/customization/DisabledDefaultClasses.tsx.preview delete mode 100644 docs/data/base/getting-started/customization/PrepareForSlot.js delete mode 100644 docs/data/base/getting-started/customization/PrepareForSlot.tsx delete mode 100644 docs/data/base/getting-started/customization/PrepareForSlot.tsx.preview delete mode 100644 docs/data/base/getting-started/customization/SlotPropsCallback.js delete mode 100644 docs/data/base/getting-started/customization/SlotPropsCallback.tsx delete mode 100644 docs/data/base/getting-started/customization/SlotPropsCallback.tsx.preview delete mode 100644 docs/data/base/getting-started/customization/StylingCustomCss.js delete mode 100644 docs/data/base/getting-started/customization/StylingCustomCss.tsx delete mode 100644 docs/data/base/getting-started/customization/StylingCustomCss.tsx.preview delete mode 100644 docs/data/base/getting-started/customization/StylingHooks.js delete mode 100644 docs/data/base/getting-started/customization/StylingHooks.tsx delete mode 100644 docs/data/base/getting-started/customization/StylingHooks.tsx.preview delete mode 100644 docs/data/base/getting-started/customization/StylingSlots.js delete mode 100644 docs/data/base/getting-started/customization/StylingSlots.tsx delete mode 100644 docs/data/base/getting-started/customization/StylingSlots.tsx.preview delete mode 100644 docs/data/base/getting-started/customization/StylingSlotsSingleComponent.js delete mode 100644 docs/data/base/getting-started/customization/StylingSlotsSingleComponent.tsx delete mode 100644 docs/data/base/getting-started/customization/StylingSlotsSingleComponent.tsx.preview diff --git a/docs/data/base/getting-started/customization/CreateSlot.tsx.preview b/docs/data/base/getting-started/customization/CreateSlot.tsx.preview deleted file mode 100644 index 07d5df8721..0000000000 --- a/docs/data/base/getting-started/customization/CreateSlot.tsx.preview +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/docs/data/base/getting-started/customization/DisabledDefaultClasses.js b/docs/data/base/getting-started/customization/DisabledDefaultClasses.js deleted file mode 100644 index bd0144e628..0000000000 --- a/docs/data/base/getting-started/customization/DisabledDefaultClasses.js +++ /dev/null @@ -1,91 +0,0 @@ -import * as React from 'react'; -import { styled } from '@mui/system'; -import { Switch } from '@mui/base/Switch'; -import { ClassNameConfigurator } from '@mui/base'; - -const Root = styled('span')( - ({ ownerState }) => ` - font-size: 0; - position: relative; - display: inline-block; - width: 32px; - height: 20px; - background: #b3c3d3; - border-radius: 10px; - margin: 10px; - cursor: pointer; - - ${ - ownerState.disabled - ? `opacity: 0.4; - cursor: not-allowed;` - : '' - } - - ${ownerState.checked ? 'background: #007fff;' : ''} -`, -); - -const Thumb = styled('span')( - ({ ownerState }) => ` - display: block; - width: 14px; - height: 14px; - top: 3px; - left: 3px; - border-radius: 16px; - background-color: #fff; - position: relative; - transition: all 200ms ease; - - ${ - ownerState.focusVisible - ? `background-color: rgba(255, 255, 255, 1); - box-shadow: 0 0 1px 8px rgba(0, 0, 0, 0.25);` - : '' - } - - ${ - ownerState.checked - ? `left: 14px; - top: 3px; - background-color: #fff;` - : '' - } -`, -); - -const Input = styled('input')` - cursor: inherit; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - opacity: 0; - z-index: 1; - margin: 0; -`; - -const slots = { root: Root, thumb: Thumb, input: Input }; - -export default function DisabledDefaultClasses() { - return ( -
- {/* The built-in classes (base-Switch-root, base--checked, etc.) are enabled by default, - even though they are not used */} - - - {/* ClassNameConfigurator removes the built-in classes, - leaving only the one generated by Emotion */} - - -
- ); -} diff --git a/docs/data/base/getting-started/customization/DisabledDefaultClasses.tsx b/docs/data/base/getting-started/customization/DisabledDefaultClasses.tsx deleted file mode 100644 index 05232cf9dc..0000000000 --- a/docs/data/base/getting-started/customization/DisabledDefaultClasses.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import * as React from 'react'; -import { styled } from '@mui/system'; -import { Switch, SwitchRootSlotProps, SwitchThumbSlotProps } from '@mui/base/Switch'; -import { ClassNameConfigurator } from '@mui/base'; - -const Root = styled('span')( - ({ ownerState }: SwitchRootSlotProps) => ` - font-size: 0; - position: relative; - display: inline-block; - width: 32px; - height: 20px; - background: #b3c3d3; - border-radius: 10px; - margin: 10px; - cursor: pointer; - - ${ - ownerState.disabled - ? `opacity: 0.4; - cursor: not-allowed;` - : '' - } - - ${ownerState.checked ? 'background: #007fff;' : ''} -`, -); - -const Thumb = styled('span')( - ({ ownerState }: SwitchThumbSlotProps) => ` - display: block; - width: 14px; - height: 14px; - top: 3px; - left: 3px; - border-radius: 16px; - background-color: #fff; - position: relative; - transition: all 200ms ease; - - ${ - ownerState.focusVisible - ? `background-color: rgba(255, 255, 255, 1); - box-shadow: 0 0 1px 8px rgba(0, 0, 0, 0.25);` - : '' - } - - ${ - ownerState.checked - ? `left: 14px; - top: 3px; - background-color: #fff;` - : '' - } -`, -); - -const Input = styled('input')` - cursor: inherit; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - opacity: 0; - z-index: 1; - margin: 0; -`; - -const slots = { root: Root, thumb: Thumb, input: Input }; - -export default function DisabledDefaultClasses() { - return ( -
- {/* The built-in classes (base-Switch-root, base--checked, etc.) are enabled by default, - even though they are not used */} - - - {/* ClassNameConfigurator removes the built-in classes, - leaving only the one generated by Emotion */} - - -
- ); -} diff --git a/docs/data/base/getting-started/customization/DisabledDefaultClasses.tsx.preview b/docs/data/base/getting-started/customization/DisabledDefaultClasses.tsx.preview deleted file mode 100644 index cf0e581aa9..0000000000 --- a/docs/data/base/getting-started/customization/DisabledDefaultClasses.tsx.preview +++ /dev/null @@ -1,14 +0,0 @@ -{/* The built-in classes (base-Switch-root, base--checked, etc.) are enabled by default, - even though they are not used */} - - - {/* ClassNameConfigurator removes the built-in classes, - leaving only the one generated by Emotion */} - - \ No newline at end of file diff --git a/docs/data/base/getting-started/customization/PrepareForSlot.js b/docs/data/base/getting-started/customization/PrepareForSlot.js deleted file mode 100644 index 8eb41a69ea..0000000000 --- a/docs/data/base/getting-started/customization/PrepareForSlot.js +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; -import { prepareForSlot } from '@mui/base/utils'; -import { Button } from '@mui/base/Button'; -import Link from 'next/link'; - -const LinkSlot = prepareForSlot(Link); - -export default function PrepareForSlot() { - return ( - - ); -} diff --git a/docs/data/base/getting-started/customization/PrepareForSlot.tsx b/docs/data/base/getting-started/customization/PrepareForSlot.tsx deleted file mode 100644 index f3bbad2cd5..0000000000 --- a/docs/data/base/getting-started/customization/PrepareForSlot.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; -import { prepareForSlot } from '@mui/base/utils'; -import { Button } from '@mui/base/Button'; -import Link from 'next/link'; - -const LinkSlot = prepareForSlot(Link); - -export default function PrepareForSlot() { - return ( - href="/" slots={{ root: LinkSlot }} prefetch={false}> - Home - - ); -} diff --git a/docs/data/base/getting-started/customization/PrepareForSlot.tsx.preview b/docs/data/base/getting-started/customization/PrepareForSlot.tsx.preview deleted file mode 100644 index a6393e1d7e..0000000000 --- a/docs/data/base/getting-started/customization/PrepareForSlot.tsx.preview +++ /dev/null @@ -1,3 +0,0 @@ - href="/" slots={{ root: LinkSlot }} prefetch={false}> - Home - \ No newline at end of file diff --git a/docs/data/base/getting-started/customization/SlotPropsCallback.js b/docs/data/base/getting-started/customization/SlotPropsCallback.js deleted file mode 100644 index 7bc38a0e5b..0000000000 --- a/docs/data/base/getting-started/customization/SlotPropsCallback.js +++ /dev/null @@ -1,71 +0,0 @@ -import * as React from 'react'; -import { Switch } from '@mui/base/Switch'; - -const css = ` - .my-switch { - font-size: 0; - position: relative; - display: inline-block; - width: 32px; - height: 20px; - background: #B3C3D3; - border-radius: 10px; - margin: 10px; - cursor: pointer; - } - - .my-switch.on { - background: #007FFF; - } - - .my-switch.focused .base-Switch-thumb { - background-color: rgba(255, 255, 255, 1); - box-shadow: 0 0 1px 8px rgba(0, 0, 0, 0.25); - } - - .my-switch .base-Switch-thumb { - display: block; - width: 14px; - height: 14px; - top: 3px; - left: 3px; - border-radius: 16px; - background-color: #FFF; - position: relative; - transition: all 200ms ease; - } - - .my-switch.on .base-Switch-thumb { - left: 14px; - top: 3px; - background-color: #FFF; - } - - .my-switch .base-Switch-input { - cursor: inherit; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - opacity: 0; - z-index: 1; - margin: 0; - }`; - -export default function SlotPropsCallback() { - const slotProps = { - root: (ownerState) => ({ - className: `my-switch ${ownerState.checked ? 'on' : 'off'} ${ - ownerState.focusVisible ? 'focused' : '' - }`, - }), - }; - - return ( -
- - -
- ); -} diff --git a/docs/data/base/getting-started/customization/SlotPropsCallback.tsx b/docs/data/base/getting-started/customization/SlotPropsCallback.tsx deleted file mode 100644 index 7e7ef2b2d1..0000000000 --- a/docs/data/base/getting-started/customization/SlotPropsCallback.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import * as React from 'react'; -import { Switch, SwitchOwnerState } from '@mui/base/Switch'; - -const css = ` - .my-switch { - font-size: 0; - position: relative; - display: inline-block; - width: 32px; - height: 20px; - background: #B3C3D3; - border-radius: 10px; - margin: 10px; - cursor: pointer; - } - - .my-switch.on { - background: #007FFF; - } - - .my-switch.focused .base-Switch-thumb { - background-color: rgba(255, 255, 255, 1); - box-shadow: 0 0 1px 8px rgba(0, 0, 0, 0.25); - } - - .my-switch .base-Switch-thumb { - display: block; - width: 14px; - height: 14px; - top: 3px; - left: 3px; - border-radius: 16px; - background-color: #FFF; - position: relative; - transition: all 200ms ease; - } - - .my-switch.on .base-Switch-thumb { - left: 14px; - top: 3px; - background-color: #FFF; - } - - .my-switch .base-Switch-input { - cursor: inherit; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - opacity: 0; - z-index: 1; - margin: 0; - }`; - -export default function SlotPropsCallback() { - const slotProps = { - root: (ownerState: SwitchOwnerState) => ({ - className: `my-switch ${ownerState.checked ? 'on' : 'off'} ${ - ownerState.focusVisible ? 'focused' : '' - }`, - }), - }; - - return ( -
- - -
- ); -} diff --git a/docs/data/base/getting-started/customization/SlotPropsCallback.tsx.preview b/docs/data/base/getting-started/customization/SlotPropsCallback.tsx.preview deleted file mode 100644 index 024276c23c..0000000000 --- a/docs/data/base/getting-started/customization/SlotPropsCallback.tsx.preview +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/docs/data/base/getting-started/customization/StylingCustomCss.js b/docs/data/base/getting-started/customization/StylingCustomCss.js deleted file mode 100644 index 457de824ae..0000000000 --- a/docs/data/base/getting-started/customization/StylingCustomCss.js +++ /dev/null @@ -1,58 +0,0 @@ -import * as React from 'react'; -import { Switch as SwitchUnstyled, switchClasses } from '@mui/base/Switch'; - -const css = ` - .my-switch { - font-size: 0; - position: relative; - display: inline-block; - width: 32px; - height: 20px; - background: #B3C3D3; - border-radius: 10px; - margin: 10px; - cursor: pointer; - } - - .my-switch.${switchClasses.checked} { - background: #007FFF; - } - - .my-switch .${switchClasses.thumb} { - display: block; - width: 14px; - height: 14px; - top: 3px; - left: 3px; - border-radius: 16px; - background-color: #FFF; - position: relative; - transition: all 200ms ease; - } - - .my-switch.${switchClasses.checked} .${switchClasses.thumb} { - left: 14px; - top: 3px; - background-color: #FFF; - } - - .my-switch .${switchClasses.input} { - cursor: inherit; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - opacity: 0; - z-index: 1; - margin: 0; - }`; - -export default function StylingCustomCss() { - return ( -
- - -
- ); -} diff --git a/docs/data/base/getting-started/customization/StylingCustomCss.tsx b/docs/data/base/getting-started/customization/StylingCustomCss.tsx deleted file mode 100644 index 457de824ae..0000000000 --- a/docs/data/base/getting-started/customization/StylingCustomCss.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import * as React from 'react'; -import { Switch as SwitchUnstyled, switchClasses } from '@mui/base/Switch'; - -const css = ` - .my-switch { - font-size: 0; - position: relative; - display: inline-block; - width: 32px; - height: 20px; - background: #B3C3D3; - border-radius: 10px; - margin: 10px; - cursor: pointer; - } - - .my-switch.${switchClasses.checked} { - background: #007FFF; - } - - .my-switch .${switchClasses.thumb} { - display: block; - width: 14px; - height: 14px; - top: 3px; - left: 3px; - border-radius: 16px; - background-color: #FFF; - position: relative; - transition: all 200ms ease; - } - - .my-switch.${switchClasses.checked} .${switchClasses.thumb} { - left: 14px; - top: 3px; - background-color: #FFF; - } - - .my-switch .${switchClasses.input} { - cursor: inherit; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - opacity: 0; - z-index: 1; - margin: 0; - }`; - -export default function StylingCustomCss() { - return ( -
- - -
- ); -} diff --git a/docs/data/base/getting-started/customization/StylingCustomCss.tsx.preview b/docs/data/base/getting-started/customization/StylingCustomCss.tsx.preview deleted file mode 100644 index 52366e9f8c..0000000000 --- a/docs/data/base/getting-started/customization/StylingCustomCss.tsx.preview +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/docs/data/base/getting-started/customization/StylingHooks.js b/docs/data/base/getting-started/customization/StylingHooks.js deleted file mode 100644 index f21e8817d2..0000000000 --- a/docs/data/base/getting-started/customization/StylingHooks.js +++ /dev/null @@ -1,81 +0,0 @@ -import * as React from 'react'; -import clsx from 'clsx'; -import { useSwitch } from '@mui/base/useSwitch'; -import { styled } from '@mui/system'; - -const SwitchRoot = styled('span')` - font-size: 0; - position: relative; - display: inline-block; - width: 32px; - height: 20px; - background: #b3c3d3; - border-radius: 10px; - margin: 10px; - cursor: pointer; - - &.Switch-disabled { - opacity: 0.4; - cursor: not-allowed; - } - - &.Switch-checked { - background: #007fff; - } -`; - -const SwitchInput = styled('input')` - cursor: inherit; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - opacity: 0; - z-index: 1; - margin: 0; -`; - -const SwitchThumb = styled('span')` - display: block; - width: 14px; - height: 14px; - top: 3px; - left: 3px; - border-radius: 16px; - background-color: #fff; - position: relative; - transition: all 200ms ease; - - &.Switch-focusVisible { - background-color: rgb(255 255 255 / 1); - box-shadow: 0 0 1px 8px rgb(0 0 0 / 0.25); - } - - &.Switch-checked { - left: 14px; - top: 3px; - background-color: #fff; - } -`; - -function Switch(props) { - const { getInputProps, checked, disabled, focusVisible } = useSwitch(props); - - const stateClasses = { - 'Switch-checked': checked, - 'Switch-disabled': disabled, - 'Switch-focusVisible': focusVisible, - }; - - return ( - - - - - ); -} - -export default function StylingHooks() { - return ; -} diff --git a/docs/data/base/getting-started/customization/StylingHooks.tsx b/docs/data/base/getting-started/customization/StylingHooks.tsx deleted file mode 100644 index d055d4dc0b..0000000000 --- a/docs/data/base/getting-started/customization/StylingHooks.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import * as React from 'react'; -import clsx from 'clsx'; -import { useSwitch, UseSwitchParameters } from '@mui/base/useSwitch'; -import { styled } from '@mui/system'; - -const SwitchRoot = styled('span')` - font-size: 0; - position: relative; - display: inline-block; - width: 32px; - height: 20px; - background: #b3c3d3; - border-radius: 10px; - margin: 10px; - cursor: pointer; - - &.Switch-disabled { - opacity: 0.4; - cursor: not-allowed; - } - - &.Switch-checked { - background: #007fff; - } -`; - -const SwitchInput = styled('input')` - cursor: inherit; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - opacity: 0; - z-index: 1; - margin: 0; -`; - -const SwitchThumb = styled('span')` - display: block; - width: 14px; - height: 14px; - top: 3px; - left: 3px; - border-radius: 16px; - background-color: #fff; - position: relative; - transition: all 200ms ease; - - &.Switch-focusVisible { - background-color: rgb(255 255 255 / 1); - box-shadow: 0 0 1px 8px rgb(0 0 0 / 0.25); - } - - &.Switch-checked { - left: 14px; - top: 3px; - background-color: #fff; - } -`; - -function Switch(props: UseSwitchParameters & { 'aria-label'?: string }) { - const { getInputProps, checked, disabled, focusVisible } = useSwitch(props); - - const stateClasses = { - 'Switch-checked': checked, - 'Switch-disabled': disabled, - 'Switch-focusVisible': focusVisible, - }; - - return ( - - - - - ); -} - -export default function StylingHooks() { - return ; -} diff --git a/docs/data/base/getting-started/customization/StylingHooks.tsx.preview b/docs/data/base/getting-started/customization/StylingHooks.tsx.preview deleted file mode 100644 index 3854607bb0..0000000000 --- a/docs/data/base/getting-started/customization/StylingHooks.tsx.preview +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/data/base/getting-started/customization/StylingSlots.js b/docs/data/base/getting-started/customization/StylingSlots.js deleted file mode 100644 index c29afb6be3..0000000000 --- a/docs/data/base/getting-started/customization/StylingSlots.js +++ /dev/null @@ -1,65 +0,0 @@ -import * as React from 'react'; -import { styled } from '@mui/system'; -import { Switch, switchClasses } from '@mui/base/Switch'; - -const SwitchRoot = styled('span')` - font-size: 0; - position: relative; - display: inline-block; - width: 32px; - height: 20px; - background: #b3c3d3; - border-radius: 10px; - margin: 10px; - cursor: pointer; - - &.${switchClasses.disabled} { - opacity: 0.4; - cursor: not-allowed; - } - - &.${switchClasses.checked} { - background: #007fff; - } -`; - -const SwitchThumb = styled('span')` - display: block; - width: 14px; - height: 14px; - top: 3px; - left: 3px; - border-radius: 16px; - background-color: #fff; - position: relative; - transition: all 200ms ease; - - .${switchClasses.focusVisible} & { - background-color: rgb(255 255 255 / 1); - box-shadow: 0 0 1px 8px rgb(0 0 0 / 0.25); - } - - .${switchClasses.checked} > & { - left: 14px; - top: 3px; - background-color: #fff; - } -`; - -const SwitchInput = styled('input')` - cursor: inherit; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - opacity: 0; - z-index: 1; - margin: 0; -`; - -export default function StylingSlots() { - return ( - - ); -} diff --git a/docs/data/base/getting-started/customization/StylingSlots.tsx b/docs/data/base/getting-started/customization/StylingSlots.tsx deleted file mode 100644 index c29afb6be3..0000000000 --- a/docs/data/base/getting-started/customization/StylingSlots.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import * as React from 'react'; -import { styled } from '@mui/system'; -import { Switch, switchClasses } from '@mui/base/Switch'; - -const SwitchRoot = styled('span')` - font-size: 0; - position: relative; - display: inline-block; - width: 32px; - height: 20px; - background: #b3c3d3; - border-radius: 10px; - margin: 10px; - cursor: pointer; - - &.${switchClasses.disabled} { - opacity: 0.4; - cursor: not-allowed; - } - - &.${switchClasses.checked} { - background: #007fff; - } -`; - -const SwitchThumb = styled('span')` - display: block; - width: 14px; - height: 14px; - top: 3px; - left: 3px; - border-radius: 16px; - background-color: #fff; - position: relative; - transition: all 200ms ease; - - .${switchClasses.focusVisible} & { - background-color: rgb(255 255 255 / 1); - box-shadow: 0 0 1px 8px rgb(0 0 0 / 0.25); - } - - .${switchClasses.checked} > & { - left: 14px; - top: 3px; - background-color: #fff; - } -`; - -const SwitchInput = styled('input')` - cursor: inherit; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - opacity: 0; - z-index: 1; - margin: 0; -`; - -export default function StylingSlots() { - return ( - - ); -} diff --git a/docs/data/base/getting-started/customization/StylingSlots.tsx.preview b/docs/data/base/getting-started/customization/StylingSlots.tsx.preview deleted file mode 100644 index 4e3d5ce333..0000000000 --- a/docs/data/base/getting-started/customization/StylingSlots.tsx.preview +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/data/base/getting-started/customization/StylingSlotsSingleComponent.js b/docs/data/base/getting-started/customization/StylingSlotsSingleComponent.js deleted file mode 100644 index b131b8b29e..0000000000 --- a/docs/data/base/getting-started/customization/StylingSlotsSingleComponent.js +++ /dev/null @@ -1,63 +0,0 @@ -import * as React from 'react'; -import { styled } from '@mui/system'; -import { Switch as BaseSwitch, switchClasses } from '@mui/base/Switch'; - -const Switch = styled(BaseSwitch)` - font-size: 0; - position: relative; - display: inline-block; - width: 32px; - height: 20px; - background: #b3c3d3; - border-radius: 10px; - margin: 10px; - cursor: pointer; - - & .${switchClasses.thumb} { - display: block; - width: 14px; - height: 14px; - top: 3px; - left: 3px; - border-radius: 16px; - background-color: #fff; - position: relative; - transition: all 200ms ease; - } - - & .${switchClasses.input} { - cursor: inherit; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - opacity: 0; - z-index: 1; - margin: 0; - } - - &.${switchClasses.disabled} { - opacity: 0.4; - cursor: not-allowed; - } - - &.${switchClasses.checked} { - background: #007fff; - - & .${switchClasses.thumb} { - left: 14px; - top: 3px; - background-color: #fff; - } - } - - &.${switchClasses.focusVisible} .${switchClasses.thumb} { - background-color: rgb(255 255 255 / 1); - box-shadow: 0 0 1px 8px rgb(0 0 0 / 0.25); - } -`; - -export default function StylingSlotsSingleComponent() { - return ; -} diff --git a/docs/data/base/getting-started/customization/StylingSlotsSingleComponent.tsx b/docs/data/base/getting-started/customization/StylingSlotsSingleComponent.tsx deleted file mode 100644 index b131b8b29e..0000000000 --- a/docs/data/base/getting-started/customization/StylingSlotsSingleComponent.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import * as React from 'react'; -import { styled } from '@mui/system'; -import { Switch as BaseSwitch, switchClasses } from '@mui/base/Switch'; - -const Switch = styled(BaseSwitch)` - font-size: 0; - position: relative; - display: inline-block; - width: 32px; - height: 20px; - background: #b3c3d3; - border-radius: 10px; - margin: 10px; - cursor: pointer; - - & .${switchClasses.thumb} { - display: block; - width: 14px; - height: 14px; - top: 3px; - left: 3px; - border-radius: 16px; - background-color: #fff; - position: relative; - transition: all 200ms ease; - } - - & .${switchClasses.input} { - cursor: inherit; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - opacity: 0; - z-index: 1; - margin: 0; - } - - &.${switchClasses.disabled} { - opacity: 0.4; - cursor: not-allowed; - } - - &.${switchClasses.checked} { - background: #007fff; - - & .${switchClasses.thumb} { - left: 14px; - top: 3px; - background-color: #fff; - } - } - - &.${switchClasses.focusVisible} .${switchClasses.thumb} { - background-color: rgb(255 255 255 / 1); - box-shadow: 0 0 1px 8px rgb(0 0 0 / 0.25); - } -`; - -export default function StylingSlotsSingleComponent() { - return ; -} diff --git a/docs/data/base/getting-started/customization/StylingSlotsSingleComponent.tsx.preview b/docs/data/base/getting-started/customization/StylingSlotsSingleComponent.tsx.preview deleted file mode 100644 index 0ff96d2010..0000000000 --- a/docs/data/base/getting-started/customization/StylingSlotsSingleComponent.tsx.preview +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/data/base/getting-started/customization/customization.md b/docs/data/base/getting-started/customization/customization.md index 6ebdd37540..ff99298b49 100644 --- a/docs/data/base/getting-started/customization/customization.md +++ b/docs/data/base/getting-started/customization/customization.md @@ -4,232 +4,7 @@ With Base UI, you have the freedom to decide how much you want to customize a component's structure and style. -## Styling the components - -This section reviews several methods of customization that are available: applying custom CSS rules, overriding default subcomponent slots, customizing the slot props, and using hooks to build fully custom components. - -### Which option to choose? - -The multitude of options can be overwhelming, especially if you're new to Base UI. -How to decide which one to use, then? - -The first decision to make is whether to use unstyled components or hooks. -Hooks are better suited for making component libraries that can be further customized. -For example, our own Joy UI is implemented using hooks from Base UI. -Hooks also serve as the basis for several Material UI components, and future versions of the library will use them even more extensively. - -If you don't need to make your component library customizable (for instance, by exposing `slotProps`), then the unstyled components may be a better option thanks to their simplicity. - -After choosing unstyled components, there is one more decision to make: how to style them. -The answer depends on the styling solution used in the project: - -#### Plain CSS, Sass, Less - -...or anything else that compiles to CSS: - -You can either [style the components using the built-in classes](#applying-custom-css-rules) or [specify your own classes](#customizing-slot-props) and reference them in your stylesheets. - -#### CSS Modules - -When working with [CSS Modules](https://github.com/css-modules/css-modules), the simplest approach is to [specify custom classes using `slotProps`](#customizing-slot-props), as shown below: - -```tsx -import clsx from 'clsx'; -import { Switch as BaseSwitch, SwitchOwnerState } from '@mui/base/Switch'; -import classes from './styles.module.css'; - -export default function Switch(props) { - const slotProps = { - root: (ownerState: SwitchOwnerState) => ({ - className: clsx(classes.root, { - [classes.checked]: ownerState.checked, - [classes.disabled]: ownerState.disabled, - }), - }), - thumb: { className: classes.thumb }, - track: { className: classes.track }, - input: { className: classes.input }, - }; - - return ; -} -``` - -In this example we're using the [clsx](https://www.npmjs.com/package/clsx) utility to reduce the effort needed to apply class names conditionally. - -#### Tailwind CSS - -Use [`slotProps`](#customizing-slot-props) to apply custom styles using [Tailwind CSS](https://tailwindcss.com/), as shown below: - -```tsx -import { Switch as BaseSwitch, SwitchOwnerState } from '@mui/base/Switch'; - -export default function Switch(props) { - const slotProps = { - root: (ownerState: SwitchOwnerState) => ({ - className: `inline-block w-8 h-5 rounded-full cursor-pointer relative ${ - ownerState.checked ? 'bg-cyan-500' : 'bg-zinc-400' - }`, - }), - thumb: (ownerState: SwitchOwnerState) => ({ - className: `bg-white block w-3.5 h-3.5 rounded-full relative top-[3px] ${ - ownerState.checked ? 'left-[3px]' : 'left-[14px]' - }`, - }), - input: { className: 'absolute w-full h-full inset-0 opacity-0 z-10 m-0' }, - }; - - return ; -} -``` - -See our [Working with Tailwind CSS guide](/base-ui/guides/working-with-tailwind-css/) for more information about integrating Base UI and Tailwind CSS. - -#### Styled components - -If you use a CSS-in-JS solution with a styled-components-like API (such as [MUI System](https://mui.com/system/getting-started/) or [Emotion](https://emotion.sh/docs/introduction)), the best method is to provide the styled subcomponents using the [`slots` prop](#overriding-subcomponent-slots), as shown in the [demo below](#overriding-subcomponent-slots). - -Alternatively, you can wrap the whole unstyled component in a `styled` utility and target the individual subcomponents using CSS classes: - -{{"demo": "StylingSlotsSingleComponent.js", "defaultCodeOpen": true}} - ---- - -### Applying custom CSS rules - -If you're happy with the default structure of a component's rendered HTML, you can apply custom styles to the component's classes. - -Each component has its own set of classes. -Some classes are **static**, which is to say that they are always present on the component. -Others are applied **conditionally**—like `base--disabled`, for example, which is only present when a component is disabled. - -Each component's API documentation lists all classes that the component uses. -Additionally, you can import a `[componentName]Classes` object that describes all the classes a given component uses, as the following demo illustrates: - -{{"demo": "StylingCustomCss.js", "defaultCodeOpen": true}} - -If you don't use these classes, you can clean up the DOM by disabling them. -See [Disabling default CSS classes](#disabling-default-css-classes) for instructions. - -### Overriding subcomponent slots - -If you want to make changes to a component's rendered HTML structure, you can override the default subcomponents ("slots") using the `slots` and/or `component` prop—see ["Shared props" on the Base Usage page](/base-ui/getting-started/usage/#shared-props) for more details. - -The following demo uses [Switch](/base-ui/react-switch/) to show how to create a styled component by applying styles to three of its subcomponent slots: `root`, `thumb`, and `input`. - -Note that although this demo uses [MUI System](https://mui.com/system/styled/) as a styling solution, you are free to choose any alternative. - -{{"demo": "StylingSlots.js"}} - -The components you pass in the `slots` prop receive the `ownerState` prop from the top-level component (the "owner"). -By convention, it contains all props passed to the owner, merged with its rendering state. - -For example: - -```jsx - -``` - -In this case, `MyCustomThumb` component receives the `ownerState` object with the following data: - -```ts -{ - checked: boolean; - disabled: boolean; - focusVisible: boolean; - readOnly: boolean; - 'data-foo': string; -} -``` - -You can use this object to style your component. - -:::warning -When inserting a component from a third-party library into a slot, you may encounter this warning: `"React does not recognize the ownerState prop on a DOM element."` -This is because the custom component isn't prepared to receive the `ownerState` like a built-in library component would be. +:::error +**TODO** The previous content was deleted as it was not up to date. +This page requires a rewrite. ::: - -If you need to use the `ownerState` to propagate some props to a third-party component, you must create a custom wrapper for this purpose. -But if you don't need the `ownerState` and just want to resolve the error, you can use the `prepareForSlot` utility: - -{{"demo": "PrepareForSlot.js", "defaultCodeOpen": true}} - -### Customizing slot props - -Use the `slotProps` prop to customize the inner component props. -The most common use case is setting a class name, but you can set any prop, including event handlers. - -The following example shows how to add a custom class to two of the Switch's slots: - -```tsx -function Switch(props: SwitchProps) { - const slotProps: SwitchProps['slotProps'] = { - thumb: { - className: 'my-thumb', - }, - track: { - className: 'my-track', - }, - }; - - return ; -} -``` - -The `switch:thumb` and `switch:class` are added unconditionally—they will always be present on the Switch component. - -You may need to apply a class only when a component is in a particular state. -A good example is adding `on` and `off` classes to a Switch based on its checked state, as shown in the demo below: - -{{"demo": "SlotPropsCallback.js", "defaultCodeOpen": true}} - -Here, instead of an object with props, the root slot receives a callback function. -Its only parameter is `ownerState`, which is an object that describes the state of the "owner component"—the Switch in this case. -The `ownerState` holds all the props of the owner component (with defaults applied where applicable) and is augmented with the internal state of the component. -In the case of the Select, the additional information includes the `checked`, `disabled`, `focusVisible`, and `readOnly` boolean fields. - -### Creating custom components using hooks - -If you need complete control over a component's rendered HTML structure, you can build it with hooks. - -Hooks give you access to the _logic_ that a component uses, but without any default structure. -See ["Components vs. hooks" on the Base Usage page](/base-ui/getting-started/usage/#components-vs-hooks) for more details. - -Hooks return the current state of the component (for example `checked`, `disabled`, `open`, etc.) and provide functions that return props you can apply to your fully custom components. - -In the case of [Switch](/base-ui/react-switch/), the component is accompanied by the `useSwitch` hook which gives you all of the functionality without any structure. - -It returns the following object: - -```ts -{ - checked: Readonly; - disabled: Readonly; - readOnly: Readonly; - focusVisible: Readonly; - getInputProps: (otherProps?: object) => SwitchInputProps; -} -``` - -The `checked`, `disabled`, `readOnly`, and `focusVisible` fields represent the state of the switch. -Use them to apply styling to your HTML elements. - -The `getInputProps` function can be used to get the props to place on an HTML `` to make the switch accessible. - -{{"demo": "StylingHooks.js", "defaultCodeOpen": true}} - -## Disabling default CSS classes - -If you don't need the built-in classes on components, you may disable them. -This will clean up the DOM and can be useful especially if you apply your own classes or style components using a CSS-in-JS solution. -To do this, wrap your components in a ClassNameConfigurator component (imported from `@mui/base/utils`): - -```tsx - - - -``` - -Inspect the elements in the following demo to see the difference: - -{{"demo": "DisabledDefaultClasses.js"}} From 6ab2ae44efd92106cff28619669152f7446d2e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Fri, 1 Mar 2024 09:35:40 +0100 Subject: [PATCH 15/32] Prettier --- docs/data/base/components/switch/switch.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data/base/components/switch/switch.md b/docs/data/base/components/switch/switch.md index 60ba5ddc0a..459c09fac1 100644 --- a/docs/data/base/components/switch/switch.md +++ b/docs/data/base/components/switch/switch.md @@ -42,8 +42,8 @@ The Switch component is composed of a root that houses one interior slot—a thu Use the `render` prop to override the root or thumb component: ```jsx - ()}> - ()} /> + }> + } /> ``` From f29123ce4782466aac36364cff4ae2818e3dcf6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Fri, 1 Mar 2024 10:46:52 +0100 Subject: [PATCH 16/32] Update the customization demo on the landing page --- .../productBaseUI/BaseUICustomization.tsx | 162 ++++++++---------- 1 file changed, 67 insertions(+), 95 deletions(-) diff --git a/docs/src/components/productBaseUI/BaseUICustomization.tsx b/docs/src/components/productBaseUI/BaseUICustomization.tsx index 797744435b..5307c1ea33 100644 --- a/docs/src/components/productBaseUI/BaseUICustomization.tsx +++ b/docs/src/components/productBaseUI/BaseUICustomization.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { styled } from '@mui/system'; -import clsx from 'clsx'; -import { Switch as SwitchUnstyled } from '@mui/base/Switch'; +import { Switch as BaseSwitch } from '@mui/base/Switch'; import { useSwitch, UseSwitchParameters } from '@mui/base/useSwitch'; import Box from '@mui/material/Box'; import Grid from '@mui/material/Grid'; @@ -18,51 +17,52 @@ import HighlightedCode from 'docs/src/modules/components/HighlightedCode'; import MarkdownElement from 'docs/src/components/markdown/MarkdownElement'; const code = ` -import clsx from 'clsx'; -import { styled } from '@mui/system'; -import { SwitchUnstyled } from '@mui/base/Switch'; +import { Switch as BaseSwitch } from '@mui/base/Switch'; import { useSwitch } from '@mui/base/useSwitch'; +import { styled } from '@mui/system'; -const StyledSwitchRoot = styled('span')(\` +const StyledSwitchRoot = styled('button')(\` font-size: 0; position: relative; - display: inline-block; + display: inline-flex; width: 40px; height: 24px; margin: 10px; + padding: 0; + border: none; cursor: pointer; border-radius: 16px; - background: #A0AAB4; + background: #B0B8C4; + transition: all ease 120ms; + + :hover { + background: #9DA8B7; + } - &.Mui-disabled { + &[data-disabled] { opacity: 0.4; cursor: not-allowed; } - &.Mui-checked { + &[data-state="checked"] { background: #007FFF; - & .MuiSwitch-thumb { - left: 20px; + :hover { + background: #0072E5; } } - &.Mui-focusVisible { - outline: 2px solid #007FFF; - outline-offset: 2px; + &:focus-visible { + outline: 4px solid rgba(0, 127, 255, 0.4); } -\`); -const StyledSwitchInput = styled('input')\` - cursor: inherit; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - opacity: 0; - z-index: 1; - margin: 0; -\`; + :where([data-mui-color-scheme='dark']) & { + background: #6B7A90; + + :hover { + background: #434D5B; + } + } +\`); const StyledSwitchThumb = styled('span')\` display: block; @@ -71,67 +71,59 @@ const StyledSwitchThumb = styled('span')\` top: 4px; left: 4px; border-radius: 16px; - background-color: #FFF; + background-color: #fff; position: relative; transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 120ms; + transition-duration: 150ms; - &.Mui-checked { + &[data-state='checked'] { left: 20px; } \`; function SwitchFromHook(props) { - const { - getInputProps, - checked, - disabled, - focusVisible, - } = useSwitch(props); + const { getInputProps, getButtonProps, checked, disabled } = useSwitch(props); - const stateClasses = { - 'Mui-checked': checked, - 'Mui-disabled': disabled, - 'Mui-focusVisible': focusVisible, + const stateAttributes = { + 'data-state': checked ? 'checked' : 'unchecked', + 'data-disabled': disabled || undefined, }; return ( - - - + + + ); } function App() { return ( - + } + > + } /> + ) } `; -const startLine = [6, 89, 64]; -const endLine = [26, 93, 84]; +const startLine = [4, 85, 65]; +const endLine = [45, 87, 79]; const scrollTo = [0, 1400, 1140]; -const StyledSwitchRoot = styled('span')(` +const StyledSwitchRoot = styled('button')(` font-size: 0; position: relative; - display: inline-block; + display: inline-flex; width: 40px; height: 24px; margin: 10px; + padding: 0; + border: none; cursor: pointer; border-radius: 16px; background: #B0B8C4; @@ -141,22 +133,19 @@ const StyledSwitchRoot = styled('span')(` background: #9DA8B7; } - &.Mui-disabled { + &[data-disabled] { opacity: 0.4; cursor: not-allowed; } - &.Mui-checked { + &[data-state="checked"] { background: #007FFF; :hover { background: #0072E5; } - & .MuiSwitch-thumb { - left: 20px; - } } - &.Mui-focusVisible { + &:focus-visible { outline: 4px solid rgba(0, 127, 255, 0.4); } @@ -169,18 +158,6 @@ const StyledSwitchRoot = styled('span')(` } `); -const StyledSwitchInput = styled('input')` - cursor: inherit; - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; - opacity: 0; - z-index: 1; - margin: 0; -`; - const StyledSwitchThumb = styled('span')` display: block; width: 16px; @@ -194,24 +171,23 @@ const StyledSwitchThumb = styled('span')` transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; - &.Mui-checked { + &[data-state='checked'] { left: 20px; } `; function SwitchFromHook(props: UseSwitchParameters) { - const { getInputProps, checked, disabled, focusVisible } = useSwitch(props); + const { getInputProps, getButtonProps, checked, disabled } = useSwitch(props); - const stateClasses = { - 'Mui-checked': checked, - 'Mui-disabled': disabled, - 'Mui-focusVisible': focusVisible, + const stateAttributes = { + 'data-state': checked ? 'checked' : 'unchecked', + 'data-disabled': disabled || undefined, }; return ( - - - + + + ); } @@ -256,7 +232,7 @@ export default function BaseUICustomization() { } title="Overriding subcomponent slots" - description="Default DOM structure doesn't suit your needs? Replace any node with the element you prefer using the `slots` prop." + description="Default DOM structure doesn't suit your needs? Replace any node with the element you prefer using the `render` prop." /> setIndex(2)}> @@ -286,16 +262,12 @@ export default function BaseUICustomization() { }), })} > - + } + > + } /> + Date: Fri, 1 Mar 2024 11:25:13 +0100 Subject: [PATCH 17/32] Remove failing useButton test --- .../mui-base/src/useButton/useButton.test.tsx | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/packages/mui-base/src/useButton/useButton.test.tsx b/packages/mui-base/src/useButton/useButton.test.tsx index 17951927d1..628f7cbed6 100644 --- a/packages/mui-base/src/useButton/useButton.test.tsx +++ b/packages/mui-base/src/useButton/useButton.test.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { act, createRenderer, fireEvent } from '@mui/internal-test-utils'; +import { createRenderer, fireEvent } from '@mui/internal-test-utils'; import { expect } from 'chai'; import { spy } from 'sinon'; import { useButton } from '@mui/base/useButton'; @@ -169,32 +169,6 @@ describe('useButton', () => { expect(handleClickInternal.callCount).to.equal(1); expect(handleClickExternal.callCount).to.equal(0); }); - - it('handles onFocusVisible and does not include it in the root props', () => { - interface WithFocusVisibleHandler { - onFocusVisible: React.FocusEventHandler; - } - - function TestComponent(props: WithFocusVisibleHandler) { - const ref = React.useRef(null); - const { getRootProps } = useButton({ ...props, rootRef: ref }); - - // @ts-expect-error onFocusVisible is removed from props - expect(getRootProps().onFocusVisible).to.equal(undefined); - - return + ; + + ); + } + + const { getByRole, getByText } = render(); + const switchElement = getByRole('switch'); + const button = getByText('Toggle'); - it('should update its state when changed from outside', () => { - function Test() { - const [checked, setChecked] = React.useState(false); - return ( -
- - ; -
- ); - } + expect(switchElement).to.have.attribute('aria-checked', 'false'); + act(() => { + button.click(); + }); - const { getByRole, getByText } = render(); - const switchElement = getByRole('switch'); - const button = getByText('Toggle'); + expect(switchElement).to.have.attribute('aria-checked', 'true'); - expect(switchElement).to.have.attribute('aria-checked', 'false'); - act(() => { - button.click(); + act(() => { + button.click(); + }); + + expect(switchElement).to.have.attribute('aria-checked', 'false'); }); - expect(switchElement).to.have.attribute('aria-checked', 'true'); + it('should update its state if the underlying input is toggled', () => { + const { getByRole, container } = render(); + const switchElement = getByRole('switch'); + const internalInput = container.querySelector('input[type="checkbox"]')! as HTMLInputElement; + + act(() => { + internalInput.click(); + }); - act(() => { - button.click(); + expect(switchElement).to.have.attribute('aria-checked', 'true'); }); + }); - expect(switchElement).to.have.attribute('aria-checked', 'false'); + describe('extra props', () => { + it('should override the built-in attributes', () => { + const { container } = render(); + expect(container.firstElementChild as HTMLElement).to.have.attribute('role', 'checkbox'); + expect(container.firstElementChild as HTMLElement).to.have.attribute('data-state', 'checked'); + }); }); - it('should call onChange when clicked', () => { - const handleChange = spy(); - const { getByRole, container } = render(); - const switchElement = getByRole('switch'); - const internalInput = container.querySelector('input[type="checkbox"]')!; + describe('prop: onChange', () => { + it('should call onChange when clicked', () => { + const handleChange = spy(); + const { getByRole, container } = render(); + const switchElement = getByRole('switch'); + const internalInput = container.querySelector('input[type="checkbox"]')!; + + act(() => { + switchElement.click(); + }); - act(() => { - switchElement.click(); + expect(handleChange.callCount).to.equal(1); + expect(handleChange.firstCall.args[0].target).to.equal(internalInput); }); + }); + + describe('prop: onClick', () => { + it('should call onClick when clicked', () => { + const handleClick = spy(); + const { getByRole } = render(); + const switchElement = getByRole('switch'); + + act(() => { + switchElement.click(); + }); - expect(handleChange.callCount).to.equal(1); - expect(handleChange.firstCall.args[0].target).to.equal(internalInput); + expect(handleClick.callCount).to.equal(1); + }); }); describe('prop: disabled', () => { @@ -138,46 +168,6 @@ describe('', () => { }); }); - it('should update its state if the underlying input is toggled', () => { - const { getByRole, container } = render(); - const switchElement = getByRole('switch'); - const internalInput = container.querySelector('input[type="checkbox"]')! as HTMLInputElement; - - act(() => { - internalInput.click(); - }); - - expect(switchElement).to.have.attribute('aria-checked', 'true'); - }); - - it('should place the style hooks on the root and the thumb', () => { - const { getByRole } = render( - - - , - ); - - const switchElement = getByRole('switch'); - const thumb = switchElement.querySelector('span'); - - expect(switchElement).to.have.attribute('data-state', 'checked'); - expect(switchElement).to.have.attribute('data-disabled', 'true'); - expect(switchElement).to.have.attribute('data-readonly', 'true'); - expect(switchElement).to.have.attribute('data-required', 'true'); - - expect(thumb).to.have.attribute('data-state', 'checked'); - expect(thumb).to.have.attribute('data-disabled', 'true'); - expect(thumb).to.have.attribute('data-readonly', 'true'); - expect(thumb).to.have.attribute('data-required', 'true'); - }); - - it('should set the name attribute on the input', () => { - const { container } = render(); - const internalInput = container.querySelector('input[type="checkbox"]')! as HTMLInputElement; - - expect(internalInput).to.have.attribute('name', 'switch-name'); - }); - describe('form handling', () => { it('should toggle the switch when a parent label is clicked', () => { const { getByTestId, getByRole } = render( @@ -220,42 +210,70 @@ describe('', () => { expect(switchElement).to.have.attribute('aria-checked', 'true'); }); - }); - it('should include the switch value in the form submission', function test() { - if (/jsdom/.test(window.navigator.userAgent)) { - // FormData is not available in JSDOM - this.skip(); - } + it('should include the switch value in the form submission', function test() { + if (/jsdom/.test(window.navigator.userAgent)) { + // FormData is not available in JSDOM + this.skip(); + } + + let stringifiedFormData = ''; + + const { getByRole } = render( +
{ + event.preventDefault(); + const formData = new FormData(event.currentTarget); + stringifiedFormData = new URLSearchParams(formData as any).toString(); + }} + > + + + , + ); - let stringifiedFormData = ''; + const switchElement = getByRole('switch'); + const submitButton = getByRole('button')!; + submitButton.click(); + + expect(stringifiedFormData).to.equal('test-switch=off'); + + act(() => { + switchElement.click(); + }); + + submitButton.click(); + + expect(stringifiedFormData).to.equal('test-switch=on'); + }); + }); + + it('should place the style hooks on the root and the thumb', () => { const { getByRole } = render( -
{ - event.preventDefault(); - const formData = new FormData(event.currentTarget); - stringifiedFormData = new URLSearchParams(formData as any).toString(); - }} - > - - - , + + + , ); const switchElement = getByRole('switch'); - const submitButton = getByRole('button')!; - - submitButton.click(); + const thumb = switchElement.querySelector('span'); - expect(stringifiedFormData).to.equal('test-switch=off'); + expect(switchElement).to.have.attribute('data-state', 'checked'); + expect(switchElement).to.have.attribute('data-disabled', 'true'); + expect(switchElement).to.have.attribute('data-readonly', 'true'); + expect(switchElement).to.have.attribute('data-required', 'true'); - act(() => { - switchElement.click(); - }); + expect(thumb).to.have.attribute('data-state', 'checked'); + expect(thumb).to.have.attribute('data-disabled', 'true'); + expect(thumb).to.have.attribute('data-readonly', 'true'); + expect(thumb).to.have.attribute('data-required', 'true'); + }); - submitButton.click(); + it('should set the name attribute on the input', () => { + const { container } = render(); + const internalInput = container.querySelector('input[type="checkbox"]')! as HTMLInputElement; - expect(stringifiedFormData).to.equal('test-switch=on'); + expect(internalInput).to.have.attribute('name', 'switch-name'); }); }); From 07798cdba549e3e227b9122de0febf0176c55831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Thu, 21 Mar 2024 19:26:31 +0100 Subject: [PATCH 32/32] API docs --- docs/pages/base-ui/api/switch.json | 1 + .../base-ui/api/use-switch-style-hooks.json | 11 ----- docs/pages/base-ui/api/use-switch.json | 6 +-- .../api-docs-base/switch/switch.json | 13 ++++-- .../api-docs/use-switch/use-switch.json | 12 +++-- packages/mui-base/src/Switch/Switch.tsx | 11 ++--- packages/mui-base/src/Switch/Switch.types.ts | 44 ++----------------- .../src/Switch/useSwitchStyleHooks.ts | 6 +-- .../mui-base/src/useSwitch/useSwitch.types.ts | 21 +++++++-- 9 files changed, 49 insertions(+), 76 deletions(-) delete mode 100644 docs/pages/base-ui/api/use-switch-style-hooks.json diff --git a/docs/pages/base-ui/api/switch.json b/docs/pages/base-ui/api/switch.json index 77e7ab75a1..dc6f6d24d1 100644 --- a/docs/pages/base-ui/api/switch.json +++ b/docs/pages/base-ui/api/switch.json @@ -5,6 +5,7 @@ "defaultChecked": { "type": { "name": "bool" } }, "disabled": { "type": { "name": "bool" }, "default": "false" }, "inputRef": { "type": { "name": "custom", "description": "ref" } }, + "name": { "type": { "name": "string" } }, "onChange": { "type": { "name": "func" }, "signature": { diff --git a/docs/pages/base-ui/api/use-switch-style-hooks.json b/docs/pages/base-ui/api/use-switch-style-hooks.json deleted file mode 100644 index 3a79771d7d..0000000000 --- a/docs/pages/base-ui/api/use-switch-style-hooks.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "parameters": {}, - "returnValue": {}, - "name": "useSwitchStyleHooks", - "filename": "/packages/mui-base/src/Switch/useSwitchStyleHooks.ts", - "imports": [ - "import { useSwitchStyleHooks } from '@mui/base/Switch';", - "import { useSwitchStyleHooks } from '@mui/base';" - ], - "demos": "
    " -} diff --git a/docs/pages/base-ui/api/use-switch.json b/docs/pages/base-ui/api/use-switch.json index a73fefc2fe..3eb6fe0a23 100644 --- a/docs/pages/base-ui/api/use-switch.json +++ b/docs/pages/base-ui/api/use-switch.json @@ -2,7 +2,7 @@ "parameters": { "checked": { "type": { "name": "boolean", "description": "boolean" } }, "defaultChecked": { "type": { "name": "boolean", "description": "boolean" } }, - "disabled": { "type": { "name": "boolean", "description": "boolean" } }, + "disabled": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" }, "inputRef": { "type": { "name": "React.Ref<HTMLInputElement>", @@ -16,8 +16,8 @@ "description": "React.ChangeEventHandler<HTMLInputElement>" } }, - "readOnly": { "type": { "name": "boolean", "description": "boolean" } }, - "required": { "type": { "name": "boolean", "description": "boolean" } } + "readOnly": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" }, + "required": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" } }, "returnValue": { "checked": { "type": { "name": "boolean", "description": "boolean" }, "required": true }, diff --git a/docs/translations/api-docs-base/switch/switch.json b/docs/translations/api-docs-base/switch/switch.json index ee6712fc3c..88ca92182a 100644 --- a/docs/translations/api-docs-base/switch/switch.json +++ b/docs/translations/api-docs-base/switch/switch.json @@ -1,25 +1,30 @@ { "componentDescription": "The foundation for building custom-styled switches.", "propDescriptions": { - "checked": { "description": "If true, the component is checked." }, + "checked": { "description": "If true, the switch is checked." }, "className": { "description": "Class names applied to the element or a function that returns them based on the component's state." }, "defaultChecked": { "description": "The default checked state. Use when the component is not controlled." }, - "disabled": { "description": "If true, the component is disabled." }, + "disabled": { + "description": "If true, the component is disabled and can't be interacted with." + }, "inputRef": { "description": "Ref to the underlying input element." }, + "name": { "description": "Name of the underlying input element." }, "onChange": { "description": "Callback fired when the state is changed.", "typeDescriptions": { "event": "The event source of the callback. You can pull out the new value by accessing event.target.value (string). You can pull out the new checked state by accessing event.target.checked (boolean)." } }, - "readOnly": { "description": "If true, the component is read only." }, + "readOnly": { + "description": "If true, the component is read-only. Functionally, this is equivalent to being disabled, but the assistive technologies will announce this differently." + }, "render": { "description": "A function to customize rendering of the component." }, "required": { - "description": "If true, the input element is required." + "description": "If true, the switch must be checked for the browser validation to pass." } }, "classDescriptions": {} diff --git a/docs/translations/api-docs/use-switch/use-switch.json b/docs/translations/api-docs/use-switch/use-switch.json index 00d8acb022..a7c78dfc11 100644 --- a/docs/translations/api-docs/use-switch/use-switch.json +++ b/docs/translations/api-docs/use-switch/use-switch.json @@ -1,17 +1,21 @@ { "hookDescription": "The basic building block for creating custom switches.", "parametersDescriptions": { - "checked": { "description": "If true, the component is checked." }, + "checked": { "description": "If true, the switch is checked." }, "defaultChecked": { "description": "The default checked state. Use when the component is not controlled." }, - "disabled": { "description": "If true, the component is disabled." }, + "disabled": { + "description": "If true, the component is disabled and can't be interacted with." + }, "inputRef": { "description": "Ref to the underlying input element." }, "name": { "description": "Name of the underlying input element." }, "onChange": { "description": "Callback fired when the state is changed." }, - "readOnly": { "description": "If true, the component is read only." }, + "readOnly": { + "description": "If true, the component is read-only. Functionally, this is equivalent to being disabled, but the assistive technologies will announce this differently." + }, "required": { - "description": "If true, the input element is required." + "description": "If true, the switch must be checked for the browser validation to pass." } }, "returnValueDescriptions": { diff --git a/packages/mui-base/src/Switch/Switch.tsx b/packages/mui-base/src/Switch/Switch.tsx index 37692ba209..05ecd0fcad 100644 --- a/packages/mui-base/src/Switch/Switch.tsx +++ b/packages/mui-base/src/Switch/Switch.tsx @@ -78,7 +78,7 @@ Switch.propTypes /* remove-proptypes */ = { // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ // └─────────────────────────────────────────────────────────────────────┘ /** - * If `true`, the component is checked. + * If `true`, the switch is checked. */ checked: PropTypes.bool, /** @@ -94,7 +94,7 @@ Switch.propTypes /* remove-proptypes */ = { */ defaultChecked: PropTypes.bool, /** - * If `true`, the component is disabled. + * If `true`, the component is disabled and can't be interacted with. * * @default false */ @@ -104,7 +104,7 @@ Switch.propTypes /* remove-proptypes */ = { */ inputRef: refType, /** - * @ignore + * Name of the underlying input element. */ name: PropTypes.string, /** @@ -116,7 +116,8 @@ Switch.propTypes /* remove-proptypes */ = { */ onChange: PropTypes.func, /** - * If `true`, the component is read only. + * If `true`, the component is read-only. + * Functionally, this is equivalent to being disabled, but the assistive technologies will announce this differently. * * @default false */ @@ -126,7 +127,7 @@ Switch.propTypes /* remove-proptypes */ = { */ render: PropTypes.func, /** - * If `true`, the `input` element is required. + * If `true`, the switch must be checked for the browser validation to pass. * * @default false */ diff --git a/packages/mui-base/src/Switch/Switch.types.ts b/packages/mui-base/src/Switch/Switch.types.ts index 9389022b19..680bef0aad 100644 --- a/packages/mui-base/src/Switch/Switch.types.ts +++ b/packages/mui-base/src/Switch/Switch.types.ts @@ -1,5 +1,5 @@ -import * as React from 'react'; import { BaseUiComponentCommonProps } from '../utils/BaseUiComponentCommonProps'; +import { UseSwitchParameters } from '../useSwitch'; export type SwitchOwnerState = { checked: boolean; @@ -9,45 +9,7 @@ export type SwitchOwnerState = { }; export interface SwitchProps - extends Omit, 'onChange'> { - /** - * If `true`, the component is checked. - */ - checked?: boolean; - /** - * The default checked state. Use when the component is not controlled. - */ - defaultChecked?: boolean; - /** - * If `true`, the component is disabled. - * - * @default false - */ - disabled?: boolean; - /** - * Ref to the underlying input element. - */ - inputRef?: React.Ref; - /** - * Callback fired when the state is changed. - * - * @param {React.ChangeEvent} event The event source of the callback. - * You can pull out the new value by accessing `event.target.value` (string). - * You can pull out the new checked state by accessing `event.target.checked` (boolean). - */ - onChange?: React.ChangeEventHandler; - /** - * If `true`, the component is read only. - * - * @default false - */ - readOnly?: boolean; - /** - * If `true`, the `input` element is required. - * - * @default false - */ - required?: boolean; -} + extends UseSwitchParameters, + Omit, 'onChange'> {} export interface SwitchThumbProps extends BaseUiComponentCommonProps<'span', SwitchOwnerState> {} diff --git a/packages/mui-base/src/Switch/useSwitchStyleHooks.ts b/packages/mui-base/src/Switch/useSwitchStyleHooks.ts index 601de69ff9..a27606e1a8 100644 --- a/packages/mui-base/src/Switch/useSwitchStyleHooks.ts +++ b/packages/mui-base/src/Switch/useSwitchStyleHooks.ts @@ -1,11 +1,9 @@ import * as React from 'react'; import { SwitchOwnerState } from './Switch.types'; import { getStyleHookProps } from '../utils/getStyleHookProps'; + /** - * - * API: - * - * - [useSwitchStyleHooks API](https://mui.com/base-ui/api/use-switch-style-hooks/) + * @ignore - internal hook. */ export function useSwitchStyleHooks(ownerState: SwitchOwnerState) { return React.useMemo(() => { diff --git a/packages/mui-base/src/useSwitch/useSwitch.types.ts b/packages/mui-base/src/useSwitch/useSwitch.types.ts index 2be42439d8..6ab74ea6fd 100644 --- a/packages/mui-base/src/useSwitch/useSwitch.types.ts +++ b/packages/mui-base/src/useSwitch/useSwitch.types.ts @@ -2,7 +2,7 @@ import * as React from 'react'; export interface UseSwitchParameters { /** - * If `true`, the component is checked. + * If `true`, the switch is checked. */ checked?: boolean; /** @@ -10,7 +10,9 @@ export interface UseSwitchParameters { */ defaultChecked?: boolean; /** - * If `true`, the component is disabled. + * If `true`, the component is disabled and can't be interacted with. + * + * @default false */ disabled?: boolean; /** @@ -30,11 +32,16 @@ export interface UseSwitchParameters { */ onChange?: React.ChangeEventHandler; /** - * If `true`, the component is read only. + * If `true`, the component is read-only. + * Functionally, this is equivalent to being disabled, but the assistive technologies will announce this differently. + * + * @default false */ readOnly?: boolean; /** - * If `true`, the `input` element is required. + * If `true`, the switch must be checked for the browser validation to pass. + * + * @default false */ required?: boolean; } @@ -51,6 +58,9 @@ interface UseSwitchInputElementOwnProps { onChange: React.ChangeEventHandler; } +/** + * Props that are received by the input element of the Switch. + */ export type UseSwitchInputElementProps = Omit< TOther, keyof UseSwitchInputElementOwnProps @@ -66,6 +76,9 @@ interface UseSwitchButtonElementOwnProps { 'aria-readonly': React.AriaAttributes['aria-readonly']; } +/** + * Props that are received by the button element of the Switch. + */ export type UseSwitchButtonElementProps = Omit< TOther, keyof UseSwitchButtonElementOwnProps