From 0230df94ca877fdd63a42c3e276f75c3a4316eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Wed, 7 Jul 2021 12:05:32 +0200 Subject: [PATCH 1/9] [Switch] Create SwitchUnstyled and useSwitch (#26688) Co-authored-by: Marija Najdova Co-authored-by: Olivier Tassinari --- docs/pages/api-docs/switch-unstyled.js | 23 + docs/pages/api-docs/switch-unstyled.json | 29 ++ docs/src/modules/utils/parseTest.ts | 5 +- .../components/switches/UnstyledSwitches.js | 73 +++ .../components/switches/UnstyledSwitches.tsx | 73 +++ .../switches/UnstyledSwitchesMaterial.js | 435 ++++++++++++++++++ .../components/switches/UseSwitchesBasic.js | 88 ++++ .../components/switches/UseSwitchesBasic.tsx | 88 ++++ .../components/switches/UseSwitchesCustom.js | 95 ++++ .../components/switches/UseSwitchesCustom.tsx | 95 ++++ .../switches/UseSwitchesMaterial.js | 399 ++++++++++++++++ .../src/pages/components/switches/switches.md | 43 +- docs/src/pagesApi.js | 1 + .../switch-unstyled/switch-unstyled-de.json | 1 + .../switch-unstyled/switch-unstyled-es.json | 1 + .../switch-unstyled/switch-unstyled-fr.json | 1 + .../switch-unstyled/switch-unstyled-ja.json | 1 + .../switch-unstyled/switch-unstyled-pt.json | 1 + .../switch-unstyled/switch-unstyled-ru.json | 1 + .../switch-unstyled/switch-unstyled-zh.json | 1 + .../switch-unstyled/switch-unstyled.json | 16 + .../material-ui-system/src/createStyled.js | 5 +- packages/material-ui-unstyled/package.json | 1 + .../SwitchUnstyled/SwitchUnstyled.test.tsx | 78 ++++ .../src/SwitchUnstyled/SwitchUnstyled.tsx | 211 +++++++++ .../src/SwitchUnstyled/index.ts | 6 + .../SwitchUnstyled/switchUnstyledClasses.ts | 37 ++ .../src/SwitchUnstyled/useSwitch.test.tsx | 86 ++++ .../src/SwitchUnstyled/useSwitch.ts | 185 ++++++++ packages/material-ui-unstyled/src/index.d.ts | 3 + packages/material-ui-unstyled/src/index.js | 3 + .../material-ui-unstyled/src/utils/index.d.ts | 3 - .../src/utils/{index.js => index.ts} | 0 .../src/utils/isHostComponent.js | 5 - .../src/utils/isHostComponent.ts | 10 + .../material-ui-unstyled/tsconfig.build.json | 4 +- .../src/ButtonBase/TouchRipple.d.ts | 17 +- .../material-ui/src/Switch/Switch.test.js | 14 + .../material-ui/src/useTouchRipple/index.ts | 1 + .../src/useTouchRipple/useTouchRipple.ts | 161 +++++++ test/utils/describeConformance.js | 3 +- test/utils/describeConformanceUnstyled.tsx | 297 ++++++++++++ test/utils/index.js | 1 + 43 files changed, 2582 insertions(+), 19 deletions(-) create mode 100644 docs/pages/api-docs/switch-unstyled.js create mode 100644 docs/pages/api-docs/switch-unstyled.json create mode 100644 docs/src/pages/components/switches/UnstyledSwitches.js create mode 100644 docs/src/pages/components/switches/UnstyledSwitches.tsx create mode 100644 docs/src/pages/components/switches/UnstyledSwitchesMaterial.js create mode 100644 docs/src/pages/components/switches/UseSwitchesBasic.js create mode 100644 docs/src/pages/components/switches/UseSwitchesBasic.tsx create mode 100644 docs/src/pages/components/switches/UseSwitchesCustom.js create mode 100644 docs/src/pages/components/switches/UseSwitchesCustom.tsx create mode 100644 docs/src/pages/components/switches/UseSwitchesMaterial.js create mode 100644 docs/translations/api-docs/switch-unstyled/switch-unstyled-de.json create mode 100644 docs/translations/api-docs/switch-unstyled/switch-unstyled-es.json create mode 100644 docs/translations/api-docs/switch-unstyled/switch-unstyled-fr.json create mode 100644 docs/translations/api-docs/switch-unstyled/switch-unstyled-ja.json create mode 100644 docs/translations/api-docs/switch-unstyled/switch-unstyled-pt.json create mode 100644 docs/translations/api-docs/switch-unstyled/switch-unstyled-ru.json create mode 100644 docs/translations/api-docs/switch-unstyled/switch-unstyled-zh.json create mode 100644 docs/translations/api-docs/switch-unstyled/switch-unstyled.json create mode 100644 packages/material-ui-unstyled/src/SwitchUnstyled/SwitchUnstyled.test.tsx create mode 100644 packages/material-ui-unstyled/src/SwitchUnstyled/SwitchUnstyled.tsx create mode 100644 packages/material-ui-unstyled/src/SwitchUnstyled/index.ts create mode 100644 packages/material-ui-unstyled/src/SwitchUnstyled/switchUnstyledClasses.ts create mode 100644 packages/material-ui-unstyled/src/SwitchUnstyled/useSwitch.test.tsx create mode 100644 packages/material-ui-unstyled/src/SwitchUnstyled/useSwitch.ts delete mode 100644 packages/material-ui-unstyled/src/utils/index.d.ts rename packages/material-ui-unstyled/src/utils/{index.js => index.ts} (100%) delete mode 100644 packages/material-ui-unstyled/src/utils/isHostComponent.js create mode 100644 packages/material-ui-unstyled/src/utils/isHostComponent.ts create mode 100644 packages/material-ui/src/useTouchRipple/index.ts create mode 100644 packages/material-ui/src/useTouchRipple/useTouchRipple.ts create mode 100644 test/utils/describeConformanceUnstyled.tsx diff --git a/docs/pages/api-docs/switch-unstyled.js b/docs/pages/api-docs/switch-unstyled.js new file mode 100644 index 00000000000000..cfe5e8e70f3db4 --- /dev/null +++ b/docs/pages/api-docs/switch-unstyled.js @@ -0,0 +1,23 @@ +import * as React from 'react'; +import ApiPage from 'docs/src/modules/components/ApiPage'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import jsonPageContent from './switch-unstyled.json'; + +export default function Page(props) { + const { descriptions, pageContent } = props; + return ; +} + +Page.getInitialProps = () => { + const req = require.context( + 'docs/translations/api-docs/switch-unstyled', + false, + /switch-unstyled.*.json$/, + ); + const descriptions = mapApiPageTranslations(req); + + return { + descriptions, + pageContent: jsonPageContent, + }; +}; diff --git a/docs/pages/api-docs/switch-unstyled.json b/docs/pages/api-docs/switch-unstyled.json new file mode 100644 index 00000000000000..f8a148346b7227 --- /dev/null +++ b/docs/pages/api-docs/switch-unstyled.json @@ -0,0 +1,29 @@ +{ + "props": { + "checked": { "type": { "name": "bool" } }, + "className": { "type": { "name": "string" } }, + "component": { "type": { "name": "elementType" } }, + "components": { + "type": { + "name": "shape", + "description": "{ Input?: elementType, Root?: elementType, Thumb?: elementType }" + }, + "default": "{}" + }, + "componentsProps": { "type": { "name": "object" }, "default": "{}" }, + "defaultChecked": { "type": { "name": "bool" } }, + "disabled": { "type": { "name": "bool" } }, + "onChange": { "type": { "name": "func" } }, + "readOnly": { "type": { "name": "bool" } }, + "required": { "type": { "name": "bool" } } + }, + "name": "SwitchUnstyled", + "styles": { "classes": [], "globalClasses": {}, "name": null }, + "spread": true, + "forwardsRefTo": "HTMLSpanElement", + "filename": "/packages/material-ui-unstyled/src/SwitchUnstyled/SwitchUnstyled.tsx", + "inheritance": null, + "demos": "", + "styledComponent": true, + "cssComponent": false +} diff --git a/docs/src/modules/utils/parseTest.ts b/docs/src/modules/utils/parseTest.ts index 86e6998e87bd36..ce88c0a05f3767 100644 --- a/docs/src/modules/utils/parseTest.ts +++ b/docs/src/modules/utils/parseTest.ts @@ -34,10 +34,7 @@ function findConformanceDescriptor(file: babel.ParseResult): babel.types.ObjectE CallExpression(babelPath) { const { node: callExpression } = babelPath; const { callee } = callExpression; - if ( - t.isIdentifier(callee) && - (callee.name === 'describeConformance' || callee.name === 'describeConformanceV5') - ) { + if (t.isIdentifier(callee) && callee.name.startsWith('describeConformance')) { const [, optionsFactory] = callExpression.arguments; if ( t.isArrowFunctionExpression(optionsFactory) && diff --git a/docs/src/pages/components/switches/UnstyledSwitches.js b/docs/src/pages/components/switches/UnstyledSwitches.js new file mode 100644 index 00000000000000..37667cb6f74c90 --- /dev/null +++ b/docs/src/pages/components/switches/UnstyledSwitches.js @@ -0,0 +1,73 @@ +import * as React from 'react'; +import { styled } from '@material-ui/system'; +import SwitchUnstyled, { + switchUnstyledClasses, +} from '@material-ui/unstyled/SwitchUnstyled'; + +const Root = styled('span')(` + font-size: 0; + position: relative; + display: inline-block; + width: 32px; + height: 20px; + background: #B3C3D3; + border-radius: 10px; + margin: 10px; + cursor: pointer; + + &.${switchUnstyledClasses.disabled} { + opacity: 0.4; + cursor: not-allowed; + } + + &.${switchUnstyledClasses.checked} { + background: #007FFF; + } + + & .${switchUnstyledClasses.thumb} { + display: block; + width: 14px; + height: 14px; + top: 3px; + left: 3px; + border-radius: 16px; + background-color: #FFF; + position: relative; + transition: all 200ms ease; + } + + &.${switchUnstyledClasses.focusVisible} .${switchUnstyledClasses.thumb} { + background-color: rgba(255,255,255,1); + box-shadow: 0 0 1px 8px rgba(0,0,0,0.25); + } + + &.${switchUnstyledClasses.checked} .${switchUnstyledClasses.thumb} { + left: 14px; + top: 3px; + background-color: #FFF; + } + + & .${switchUnstyledClasses.input} { + cursor: inherit; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + opacity: 0; + z-index: 1; + margin: 0; + }`); + +export default function UnstyledSwitches() { + const label = { componentsProps: { input: { 'aria-label': 'Demo switch' } } }; + + return ( +
+ + + + +
+ ); +} diff --git a/docs/src/pages/components/switches/UnstyledSwitches.tsx b/docs/src/pages/components/switches/UnstyledSwitches.tsx new file mode 100644 index 00000000000000..37667cb6f74c90 --- /dev/null +++ b/docs/src/pages/components/switches/UnstyledSwitches.tsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +import { styled } from '@material-ui/system'; +import SwitchUnstyled, { + switchUnstyledClasses, +} from '@material-ui/unstyled/SwitchUnstyled'; + +const Root = styled('span')(` + font-size: 0; + position: relative; + display: inline-block; + width: 32px; + height: 20px; + background: #B3C3D3; + border-radius: 10px; + margin: 10px; + cursor: pointer; + + &.${switchUnstyledClasses.disabled} { + opacity: 0.4; + cursor: not-allowed; + } + + &.${switchUnstyledClasses.checked} { + background: #007FFF; + } + + & .${switchUnstyledClasses.thumb} { + display: block; + width: 14px; + height: 14px; + top: 3px; + left: 3px; + border-radius: 16px; + background-color: #FFF; + position: relative; + transition: all 200ms ease; + } + + &.${switchUnstyledClasses.focusVisible} .${switchUnstyledClasses.thumb} { + background-color: rgba(255,255,255,1); + box-shadow: 0 0 1px 8px rgba(0,0,0,0.25); + } + + &.${switchUnstyledClasses.checked} .${switchUnstyledClasses.thumb} { + left: 14px; + top: 3px; + background-color: #FFF; + } + + & .${switchUnstyledClasses.input} { + cursor: inherit; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + opacity: 0; + z-index: 1; + margin: 0; + }`); + +export default function UnstyledSwitches() { + const label = { componentsProps: { input: { 'aria-label': 'Demo switch' } } }; + + return ( +
+ + + + +
+ ); +} diff --git a/docs/src/pages/components/switches/UnstyledSwitchesMaterial.js b/docs/src/pages/components/switches/UnstyledSwitchesMaterial.js new file mode 100644 index 00000000000000..b10b4a1793bb26 --- /dev/null +++ b/docs/src/pages/components/switches/UnstyledSwitchesMaterial.js @@ -0,0 +1,435 @@ +/* eslint-disable no-restricted-imports, react/prop-types */ +import * as React from 'react'; +import clsx from 'clsx'; +import { + SwitchUnstyled, + unstable_composeClasses as composeClasses, +} from '@material-ui/unstyled'; +import { alpha, darken, lighten, useThemeProps, styled } from '@material-ui/system'; +import { ThemeProvider, createTheme } from '@material-ui/core/styles'; +import { capitalize } from '@material-ui/core/utils'; +import { + useFormControl, + switchClasses, + getSwitchUtilityClass, +} from '@material-ui/core'; +import TouchRipple from '@material-ui/core/ButtonBase/TouchRipple'; +import useTouchRipple from '@material-ui/core/useTouchRipple'; + +const useUtilityClasses = (styleProps) => { + const { classes, edge, size, color, checked, disabled, focusVisible } = styleProps; + + const slots = { + root: [ + edge && `edge${capitalize(edge)}`, + `size${capitalize(size)}`, + `color${capitalize(color)}`, + ], + switchBase: [ + 'switchBase', + `color${capitalize(color)}`, + focusVisible && 'focusVisible', + checked && 'checked', + disabled && 'disabled', + ], + track: ['track'], + }; + + return composeClasses(slots, getSwitchUtilityClass, classes); +}; + +const SwitchTrack = styled('span', { + name: 'MuiSwitch', + slot: 'Track', + overridesResolver: (props, styles) => styles.track, +})(({ theme }) => ({ + height: '100%', + width: '100%', + borderRadius: 14 / 2, + zIndex: -1, + transition: theme.transitions.create(['opacity', 'background-color'], { + duration: theme.transitions.duration.shortest, + }), + backgroundColor: + theme.palette.mode === 'light' + ? theme.palette.common.black + : theme.palette.common.white, + opacity: theme.palette.mode === 'light' ? 0.38 : 0.3, +})); + +const SwitchBase = styled('span', { + name: 'MuiSwitch', + slot: 'SwitchBase', + overridesResolver: (props, styles) => { + const { styleProps } = props; + + return { + ...styles.switchBase, + ...styles.input, + ...(styleProps.color !== 'default' && + styles[`color${capitalize(styleProps.color)}`]), + }; + }, +})( + ({ theme, styleProps }) => ({ + position: 'absolute', + top: 0, + left: 0, + zIndex: 1, + color: + theme.palette.mode === 'light' + ? theme.palette.common.white + : theme.palette.grey[300], + transition: theme.transitions.create(['left', 'transform'], { + duration: theme.transitions.duration.shortest, + }), + padding: 9, + borderRadius: '50%', + ...(styleProps.edge === 'start' && { + marginLeft: styleProps.size === 'small' ? -3 : -12, + }), + ...(styleProps.edge === 'end' && { + marginRight: styleProps.size === 'small' ? -3 : -12, + }), + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + boxSizing: 'border-box', + WebkitTapHighlightColor: 'transparent', + backgroundColor: 'transparent', + outline: 0, + border: 0, + margin: 0, + cursor: 'pointer', + userSelect: 'none', + verticalAlign: 'middle', + MozAppearance: 'none', + WebkitAppearance: 'none', + textDecoration: 'none', + '&::-moz-focus-inner': { + borderStyle: 'none', + }, + '@media print': { + colorAdjust: 'exact', + }, + [`&.${switchClasses.checked}`]: { + transform: 'translateX(20px)', + }, + [`&.${switchClasses.disabled}`]: { + color: + theme.palette.mode === 'light' + ? theme.palette.grey[100] + : theme.palette.grey[600], + pointerEvents: 'none', + cursor: 'default', + }, + [`&.${switchClasses.checked} + .${switchClasses.track}`]: { + opacity: 0.5, + }, + [`&.${switchClasses.disabled} + .${switchClasses.track}`]: { + opacity: theme.palette.mode === 'light' ? 0.12 : 0.2, + }, + }), + ({ theme, styleProps }) => ({ + '&:hover': { + backgroundColor: alpha( + theme.palette.action.active, + theme.palette.action.hoverOpacity, + ), + '@media (hover: none)': { + backgroundColor: 'transparent', + }, + }, + ...(styleProps.color !== 'default' && { + [`&.${switchClasses.checked}`]: { + color: theme.palette[styleProps.color].main, + '&:hover': { + backgroundColor: alpha( + theme.palette[styleProps.color].main, + theme.palette.action.hoverOpacity, + ), + '@media (hover: none)': { + backgroundColor: 'transparent', + }, + }, + [`&.${switchClasses.disabled}`]: { + color: + theme.palette.mode === 'light' + ? lighten(theme.palette[styleProps.color].main, 0.62) + : darken(theme.palette[styleProps.color].main, 0.55), + }, + }, + [`&.${switchClasses.checked} + .${switchClasses.track}`]: { + backgroundColor: theme.palette[styleProps.color].main, + }, + }), + }), +); + +const SwitchRoot = styled('span', { + name: 'MuiSwitch', + slot: 'Root', + overridesResolver: (props, styles) => { + const { styleProps } = props; + + return { + ...styles.root, + ...(styleProps.edge && styles[`edge${capitalize(styleProps.edge)}`]), + ...styles[`size${capitalize(styleProps.size)}`], + ...styles.input, + ...(styleProps.color !== 'default' && + styles[`color${capitalize(styleProps.color)}`]), + }; + }, +})(({ styleProps }) => ({ + display: 'inline-flex', + width: 34 + 12 * 2, + height: 14 + 12 * 2, + overflow: 'hidden', + padding: 12, + boxSizing: 'border-box', + position: 'relative', + flexShrink: 0, + zIndex: 0, + verticalAlign: 'middle', + '@media print': { + colorAdjust: 'exact', + }, + ...(styleProps.edge === 'start' && { + marginLeft: -8, + }), + ...(styleProps.edge === 'end' && { + marginRight: -8, + }), + ...(styleProps.size === 'small' && { + width: 40, + height: 24, + padding: 7, + [`& .${switchClasses.thumb}`]: { + width: 16, + height: 16, + }, + [`& .${switchClasses.switchBase}`]: { + padding: 4, + [`&.${switchClasses.checked}`]: { + transform: 'translateX(16px)', + }, + }, + }), +})); + +const SwitchInput = styled('input', { + name: 'MuiSwitch', + slot: 'Input', + skipSx: true, +})({ + cursor: 'inherit', + position: 'absolute', + opacity: 0, + width: '300%', + height: '100%', + top: 0, + left: '-100%', + margin: 0, + padding: 0, + zIndex: 1, +}); + +const SwitchThumb = styled( + ({ isChecked, icon, checkedIcon, className }) => { + if (!isChecked && icon) { + return icon; + } + + if (isChecked && checkedIcon) { + return checkedIcon; + } + + return ; + }, + { + name: 'MuiSwitch', + slot: 'Thumb', + overridesResolver: (props, styles) => styles.thumb, + }, +)(({ theme }) => ({ + boxShadow: theme.shadows[1], + backgroundColor: 'currentColor', + width: 20, + height: 20, + borderRadius: '50%', +})); + +const SwitchLayout = React.forwardRef((props, ref) => { + const { + className, + disableRipple, + disableTouchRipple, + disableFocusRipple, + styleProps, + TouchRippleProps, + children, + onFocus, + onBlur, + ...other + } = props; + + const { checked, disabled, focusVisible } = styleProps; + + const rippleRef = React.useRef(null); + + const { enableTouchRipple, getRippleHandlers } = useTouchRipple({ + rippleRef, + focusVisible, + disabled, + disableRipple, + disableTouchRipple, + disableFocusRipple, + }); + + const rippleHandlers = getRippleHandlers({ + onBlur, + }); + + const classes = useUtilityClasses({ + ...styleProps, + checked, + disabled, + focusVisible, + }); + + return ( + + + {children} + {enableTouchRipple && ( + + )} + + + + ); +}); + +const Switch = React.forwardRef(function Switch(inProps, ref) { + const props = useThemeProps({ props: inProps, name: 'MuiSwitch' }); + + const { + checked: checkedProp, + checkedIcon, + className, + color = 'primary', + disabled: disabledProp, + disableFocusRipple = false, + disableRipple = false, + disableTouchRipple = false, + edge = false, + icon, + inputProps, + onBlur, + onFocus, + size = 'medium', + TouchRippleProps, + ...other + } = props; + + const muiFormControl = useFormControl(); + + const handleFocus = (event) => { + onFocus?.(event); + + if (muiFormControl && muiFormControl.onFocus) { + muiFormControl.onFocus(event); + } + }; + + const handleBlur = (event) => { + onBlur?.(event); + + if (muiFormControl && muiFormControl.onBlur) { + muiFormControl.onBlur(event); + } + }; + + let disabled = disabledProp; + + if (muiFormControl) { + if (typeof disabled === 'undefined') { + disabled = muiFormControl.disabled; + } + } + + const styleProps = { + ...props, + color, + edge, + size, + disableFocusRipple, + disableRipple, + disableTouchRipple, + }; + + const components = { + Root: SwitchLayout, + Input: SwitchInput, + Thumb: SwitchThumb, + }; + + const componentsProps = { + root: { + className, + styleProps, + disableFocusRipple, + disableRipple, + disableTouchRipple, + onBlur: handleBlur, + onFocus: handleFocus, + TouchRippleProps, + }, + input: { + styleProps, + ...inputProps, + }, + thumb: { + styleProps, + icon, + checkedIcon, + defaultThumbClassName: switchClasses.thumb, + }, + }; + + return ( + + ); +}); + +const label = { inputProps: { 'aria-label': 'Switch demo' } }; + +export default function UseSwitchesCustom() { + return ( +
+ + + + + + +
+ ); +} diff --git a/docs/src/pages/components/switches/UseSwitchesBasic.js b/docs/src/pages/components/switches/UseSwitchesBasic.js new file mode 100644 index 00000000000000..ca314dc286fa42 --- /dev/null +++ b/docs/src/pages/components/switches/UseSwitchesBasic.js @@ -0,0 +1,88 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { styled } from '@material-ui/system'; +import { useSwitch } from '@material-ui/unstyled/SwitchUnstyled'; + +const BasicSwitchRoot = 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 BasicSwitchInput = styled('input')(` + cursor: inherit; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + opacity: 0; + z-index: 1; + margin: 0; +`); + +const BasicSwitchThumb = 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: rgba(255,255,255,1); + box-shadow: 0 0 1px 8px rgba(0,0,0,0.25); + } + + &.Switch-checked { + left: 14px; + top: 3px; + background-color: #FFF; + } +`); + +function BasicSwitch(props) { + const { getInputProps, checked, disabled, focusVisible } = useSwitch(props); + + const stateClasses = { + 'Switch-checked': checked, + 'Switch-disabled': disabled, + 'Switch-focusVisible': focusVisible, + }; + + return ( + + + + + ); +} + +export default function UseSwitchesBasic() { + return ( +
+ + + + +
+ ); +} diff --git a/docs/src/pages/components/switches/UseSwitchesBasic.tsx b/docs/src/pages/components/switches/UseSwitchesBasic.tsx new file mode 100644 index 00000000000000..3ab1747730677b --- /dev/null +++ b/docs/src/pages/components/switches/UseSwitchesBasic.tsx @@ -0,0 +1,88 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { styled } from '@material-ui/system'; +import { useSwitch, UseSwitchProps } from '@material-ui/unstyled/SwitchUnstyled'; + +const BasicSwitchRoot = 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 BasicSwitchInput = styled('input')(` + cursor: inherit; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + opacity: 0; + z-index: 1; + margin: 0; +`); + +const BasicSwitchThumb = 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: rgba(255,255,255,1); + box-shadow: 0 0 1px 8px rgba(0,0,0,0.25); + } + + &.Switch-checked { + left: 14px; + top: 3px; + background-color: #FFF; + } +`); + +function BasicSwitch(props: UseSwitchProps) { + const { getInputProps, checked, disabled, focusVisible } = useSwitch(props); + + const stateClasses = { + 'Switch-checked': checked, + 'Switch-disabled': disabled, + 'Switch-focusVisible': focusVisible, + }; + + return ( + + + + + ); +} + +export default function UseSwitchesBasic() { + return ( +
+ + + + +
+ ); +} diff --git a/docs/src/pages/components/switches/UseSwitchesCustom.js b/docs/src/pages/components/switches/UseSwitchesCustom.js new file mode 100644 index 00000000000000..085ff97af5edde --- /dev/null +++ b/docs/src/pages/components/switches/UseSwitchesCustom.js @@ -0,0 +1,95 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { styled } from '@material-ui/system'; +import { useSwitch } from '@material-ui/unstyled/SwitchUnstyled'; + +const SwitchRoot = styled('span')(` + display: inline-block; + position: relative; + width: 62px; + height: 34px; + padding: 7px; +`); + +const SwitchInput = styled('input')(` + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + opacity: 0; + z-index: 1; + margin: 0; + cursor: pointer; +`); + +const SwitchThumb = styled('span')( + ({ theme }) => ` + position: absolute; + display: block; + background-color: ${theme.palette.mode === 'dark' ? '#003892' : '#001e3c'}; + width: 32px; + height: 32px; + border-radius: 16px; + top: 1px; + left: 7px; + transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1); + + &:before { + display: block; + content: ""; + width: 100%; + height: 100%; + background: url('data:image/svg+xml;utf8,') center center no-repeat; + } + + &.focusVisible { + background-color: #79B; + } + + &.checked { + transform: translateX(16px); + + &:before { + background-image: url('data:image/svg+xml;utf8,'); + } + } +`, +); + +const SwitchTrack = styled('span')( + ({ theme }) => ` + background-color: ${theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be'}; + border-radius: 10px; + width: 100%; + height: 100%; + display: block; +`, +); + +function MUISwitch(props) { + const { getInputProps, checked, disabled, focusVisible } = useSwitch(props); + + const stateClasses = { + checked, + disabled, + focusVisible, + }; + + return ( + + + + + + + ); +} + +export default function UseSwitchesCustom() { + return ; +} diff --git a/docs/src/pages/components/switches/UseSwitchesCustom.tsx b/docs/src/pages/components/switches/UseSwitchesCustom.tsx new file mode 100644 index 00000000000000..7dbeeaba86d759 --- /dev/null +++ b/docs/src/pages/components/switches/UseSwitchesCustom.tsx @@ -0,0 +1,95 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { styled } from '@material-ui/system'; +import { useSwitch, UseSwitchProps } from '@material-ui/unstyled/SwitchUnstyled'; + +const SwitchRoot = styled('span')(` + display: inline-block; + position: relative; + width: 62px; + height: 34px; + padding: 7px; +`); + +const SwitchInput = styled('input')(` + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + opacity: 0; + z-index: 1; + margin: 0; + cursor: pointer; +`); + +const SwitchThumb = styled('span')( + ({ theme }) => ` + position: absolute; + display: block; + background-color: ${theme.palette.mode === 'dark' ? '#003892' : '#001e3c'}; + width: 32px; + height: 32px; + border-radius: 16px; + top: 1px; + left: 7px; + transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1); + + &:before { + display: block; + content: ""; + width: 100%; + height: 100%; + background: url('data:image/svg+xml;utf8,') center center no-repeat; + } + + &.focusVisible { + background-color: #79B; + } + + &.checked { + transform: translateX(16px); + + &:before { + background-image: url('data:image/svg+xml;utf8,'); + } + } +`, +); + +const SwitchTrack = styled('span')( + ({ theme }) => ` + background-color: ${theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be'}; + border-radius: 10px; + width: 100%; + height: 100%; + display: block; +`, +); + +function MUISwitch(props: UseSwitchProps) { + const { getInputProps, checked, disabled, focusVisible } = useSwitch(props); + + const stateClasses = { + checked, + disabled, + focusVisible, + }; + + return ( + + + + + + + ); +} + +export default function UseSwitchesCustom() { + return ; +} diff --git a/docs/src/pages/components/switches/UseSwitchesMaterial.js b/docs/src/pages/components/switches/UseSwitchesMaterial.js new file mode 100644 index 00000000000000..8ae78b8a8e0132 --- /dev/null +++ b/docs/src/pages/components/switches/UseSwitchesMaterial.js @@ -0,0 +1,399 @@ +/* eslint-disable no-restricted-imports, react/prop-types */ +import * as React from 'react'; +import clsx from 'clsx'; +import { + unstable_composeClasses as composeClasses, + useSwitch, +} from '@material-ui/unstyled'; +import { alpha, darken, lighten, useThemeProps, styled } from '@material-ui/system'; +import { ThemeProvider, createTheme } from '@material-ui/core/styles'; +import { capitalize } from '@material-ui/core/utils'; +import { + useFormControl, + switchClasses, + getSwitchUtilityClass, +} from '@material-ui/core'; +import TouchRipple from '@material-ui/core/ButtonBase/TouchRipple'; +import useTouchRipple from '@material-ui/core/useTouchRipple'; + +const useUtilityClasses = (styleProps) => { + const { classes, edge, size, color, checked, disabled, focusVisible } = styleProps; + + const slots = { + root: [ + 'root', + checked && 'checked', + disabled && 'disabled', + edge && `edge${capitalize(edge)}`, + `size${capitalize(size)}`, + `color${capitalize(color)}`, + ], + switchBase: [ + 'switchBase', + `color${capitalize(color)}`, + focusVisible && 'focusVisible', + checked && 'checked', + disabled && 'disabled', + ], + thumb: ['thumb'], + track: ['track'], + input: ['input'], + }; + + const composedClasses = composeClasses(slots, getSwitchUtilityClass, classes); + + return { + ...classes, + ...composedClasses, + }; +}; + +const SwitchTrack = styled('span', { + name: 'MuiSwitch', + slot: 'Track', + overridesResolver: (props, styles) => styles.track, +})(({ theme }) => ({ + height: '100%', + width: '100%', + borderRadius: 14 / 2, + zIndex: -1, + transition: theme.transitions.create(['opacity', 'background-color'], { + duration: theme.transitions.duration.shortest, + }), + backgroundColor: + theme.palette.mode === 'light' + ? theme.palette.common.black + : theme.palette.common.white, + opacity: theme.palette.mode === 'light' ? 0.38 : 0.3, +})); + +const SwitchBase = styled('span', { + name: 'MuiSwitch', + slot: 'SwitchBase', + overridesResolver: (props, styles) => { + const { styleProps } = props; + + return { + ...styles.switchBase, + ...styles.input, + ...(styleProps.color !== 'default' && + styles[`color${capitalize(styleProps.color)}`]), + }; + }, +})( + ({ theme, styleProps }) => ({ + position: 'absolute', + top: 0, + left: 0, + zIndex: 1, + color: + theme.palette.mode === 'light' + ? theme.palette.common.white + : theme.palette.grey[300], + transition: theme.transitions.create(['left', 'transform'], { + duration: theme.transitions.duration.shortest, + }), + padding: 9, + borderRadius: '50%', + ...(styleProps.edge === 'start' && { + marginLeft: styleProps.size === 'small' ? -3 : -12, + }), + ...(styleProps.edge === 'end' && { + marginRight: styleProps.size === 'small' ? -3 : -12, + }), + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + boxSizing: 'border-box', + WebkitTapHighlightColor: 'transparent', + backgroundColor: 'transparent', + outline: 0, + border: 0, + margin: 0, + cursor: 'pointer', + userSelect: 'none', + verticalAlign: 'middle', + MozAppearance: 'none', + WebkitAppearance: 'none', + textDecoration: 'none', + '&::-moz-focus-inner': { + borderStyle: 'none', + }, + '@media print': { + colorAdjust: 'exact', + }, + [`&.${switchClasses.checked}`]: { + transform: 'translateX(20px)', + }, + [`&.${switchClasses.disabled}`]: { + color: + theme.palette.mode === 'light' + ? theme.palette.grey[100] + : theme.palette.grey[600], + pointerEvents: 'none', + cursor: 'default', + }, + [`&.${switchClasses.checked} + .${switchClasses.track}`]: { + opacity: 0.5, + }, + [`&.${switchClasses.disabled} + .${switchClasses.track}`]: { + opacity: theme.palette.mode === 'light' ? 0.12 : 0.2, + }, + }), + ({ theme, styleProps }) => ({ + '&:hover': { + backgroundColor: alpha( + theme.palette.action.active, + theme.palette.action.hoverOpacity, + ), + '@media (hover: none)': { + backgroundColor: 'transparent', + }, + }, + ...(styleProps.color !== 'default' && { + [`&.${switchClasses.checked}`]: { + color: theme.palette[styleProps.color].main, + '&:hover': { + backgroundColor: alpha( + theme.palette[styleProps.color].main, + theme.palette.action.hoverOpacity, + ), + '@media (hover: none)': { + backgroundColor: 'transparent', + }, + }, + [`&.${switchClasses.disabled}`]: { + color: + theme.palette.mode === 'light' + ? lighten(theme.palette[styleProps.color].main, 0.62) + : darken(theme.palette[styleProps.color].main, 0.55), + }, + }, + [`&.${switchClasses.checked} + .${switchClasses.track}`]: { + backgroundColor: theme.palette[styleProps.color].main, + }, + }), + }), +); + +const SwitchRoot = styled('span', { + name: 'MuiSwitch', + slot: 'Root', + overridesResolver: (props, styles) => { + const { styleProps } = props; + + return { + ...styles.root, + ...(styleProps.edge && styles[`edge${capitalize(styleProps.edge)}`]), + ...styles[`size${capitalize(styleProps.size)}`], + ...styles.input, + ...(styleProps.color !== 'default' && + styles[`color${capitalize(styleProps.color)}`]), + }; + }, +})(({ styleProps }) => ({ + display: 'inline-flex', + width: 34 + 12 * 2, + height: 14 + 12 * 2, + overflow: 'hidden', + padding: 12, + boxSizing: 'border-box', + position: 'relative', + flexShrink: 0, + zIndex: 0, + verticalAlign: 'middle', + '@media print': { + colorAdjust: 'exact', + }, + ...(styleProps.edge === 'start' && { + marginLeft: -8, + }), + ...(styleProps.edge === 'end' && { + marginRight: -8, + }), + ...(styleProps.size === 'small' && { + width: 40, + height: 24, + padding: 7, + [`& .${switchClasses.thumb}`]: { + width: 16, + height: 16, + }, + [`& .${switchClasses.switchBase}`]: { + padding: 4, + [`&.${switchClasses.checked}`]: { + transform: 'translateX(16px)', + }, + }, + }), +})); + +const SwitchThumb = styled('span', { + name: 'MuiSwitch', + slot: 'Thumb', + overridesResolver: (props, styles) => styles.thumb, +})(({ theme }) => ({ + boxShadow: theme.shadows[1], + backgroundColor: 'currentColor', + width: 20, + height: 20, + borderRadius: '50%', +})); + +const SwitchInput = styled('input', { + name: 'MuiSwitch', + slot: 'Input', + skipSx: true, +})({ + cursor: 'inherit', + position: 'absolute', + opacity: 0, + width: '300%', + height: '100%', + top: 0, + left: '-100%', + margin: 0, + padding: 0, + zIndex: 1, +}); + +const renderThumb = (isChecked, icon, checkedIcon, defaultThumbClassName) => { + if (!isChecked && icon) { + return icon; + } + + if (isChecked && checkedIcon) { + return checkedIcon; + } + + return ; +}; + +const Switch = React.forwardRef(function Switch(inProps, ref) { + const props = useThemeProps({ props: inProps, name: 'MuiSwitch' }); + + const { + checked: checkedProp, + checkedIcon, + className, + color = 'primary', + defaultChecked, + disabled: disabledProp, + disableFocusRipple = false, + disableRipple = false, + disableTouchRipple = false, + edge = false, + icon, + inputProps, + onBlur, + onChange, + onFocus, + readOnly, + size = 'medium', + TouchRippleProps, + ...other + } = props; + + const rippleRef = React.useRef(null); + + const muiFormControl = useFormControl(); + + const handleFocus = (event) => { + onFocus?.(event); + + if (muiFormControl && muiFormControl.onFocus) { + muiFormControl.onFocus(event); + } + }; + + const handleBlur = (event) => { + onBlur?.(event); + + if (muiFormControl && muiFormControl.onBlur) { + muiFormControl.onBlur(event); + } + }; + + let disabled = disabledProp; + if (muiFormControl) { + if (typeof disabled === 'undefined') { + disabled = muiFormControl.disabled; + } + } + + const { + getInputProps, + checked, + disabled: disabledState, + focusVisible, + } = useSwitch({ + ...props, + disabled, + }); + + const styleProps = { + ...props, + checked, + disabled: disabledState, + focusVisible, + color, + edge, + size, + }; + + const { enableTouchRipple, getRippleHandlers } = useTouchRipple({ + rippleRef, + focusVisible, + disabled: disabledState, + disableRipple, + disableTouchRipple, + disableFocusRipple, + }); + + const rippleHandlers = getRippleHandlers({ + onBlur: handleBlur, + }); + + const classes = useUtilityClasses(styleProps); + + return ( + + + + {renderThumb(checked, icon, checkedIcon, classes.thumb)} + {enableTouchRipple && ( + + )} + + + + ); +}); + +const label = { inputProps: { 'aria-label': 'Switch demo' } }; + +export default function UseSwitchesMaterial() { + return ( +
+ + + + + + +
+ ); +} diff --git a/docs/src/pages/components/switches/switches.md b/docs/src/pages/components/switches/switches.md index d22345fba2928d..1c1e7f34370724 100644 --- a/docs/src/pages/components/switches/switches.md +++ b/docs/src/pages/components/switches/switches.md @@ -1,6 +1,6 @@ --- title: React Switch component -components: Switch, FormControl, FormGroup, FormLabel, FormControlLabel +components: Switch, FormControl, FormGroup, FormLabel, FormControlLabel, SwitchUnstyled githubLabel: 'component: Switch' materialDesign: https://material.io/components/selection-controls#switches --- @@ -57,6 +57,47 @@ Here are some examples of customizing the component. You can learn more about th 🎨 If you are looking for inspiration, you can check [MUI Treasury's customization examples](https://mui-treasury.com/styles/switch). +## Unstyled switches + +The switch also comes with an unstyled version. It's ideal for doing heavy customizations and minimizing bundle size. + +```jsx +import SwitchUnstyled from '@material-ui/unstyled/SwitchUnstyled'; +``` + +The `SwitchUnstyled` component provides default components and assigns CSS classes you can style entirely on your own. +You are free to choose any styling solution - plain CSS classes, a CSS framework, Emotion, etc. +It is also possible to replace these default components by other HTML elements or custom components. + +There are three components you can override by the `components` prop: `Root`, `Thumb` and `Input`. Each one's props can be set using the `componentsProps` object. + +{{"demo": "pages/components/switches/UnstyledSwitches.js"}} + +### Recreation of Material-UI's Switch + +{{"demo": "pages/components/switches/UnstyledSwitchesMaterial.js"}} + +### useSwitch hook + +For the ultimate customizability, a `useSwitch` hook is available. +It accepts almost the same options as the SwitchUnstyled component minus the `component`, `components`, and `componentsProps` props. + +```jsx +import { useSwitch } from '@material-ui/unstyled/SwitchUnstyled'; +``` + +#### Basic example + +{{"demo": "pages/components/switches/UseSwitchesBasic.js"}} + +#### Customized look and feel + +{{"demo": "pages/components/switches/UseSwitchesCustom.js"}} + +#### Recreation of Material-UI's Switch + +{{"demo": "pages/components/switches/UseSwitchesMaterial.js"}} + ## Label placement You can change the placement of the label: diff --git a/docs/src/pagesApi.js b/docs/src/pagesApi.js index 29730c6fda3e47..247ded85b3d231 100644 --- a/docs/src/pagesApi.js +++ b/docs/src/pagesApi.js @@ -132,6 +132,7 @@ module.exports = [ { pathname: '/api-docs/svg-icon' }, { pathname: '/api-docs/swipeable-drawer' }, { pathname: '/api-docs/switch' }, + { pathname: '/api-docs/switch-unstyled' }, { pathname: '/api-docs/tab' }, { pathname: '/api-docs/tab-context' }, { pathname: '/api-docs/table' }, diff --git a/docs/translations/api-docs/switch-unstyled/switch-unstyled-de.json b/docs/translations/api-docs/switch-unstyled/switch-unstyled-de.json new file mode 100644 index 00000000000000..f93d4cbd8c7985 --- /dev/null +++ b/docs/translations/api-docs/switch-unstyled/switch-unstyled-de.json @@ -0,0 +1 @@ +{ "componentDescription": "", "propDescriptions": {}, "classDescriptions": {} } diff --git a/docs/translations/api-docs/switch-unstyled/switch-unstyled-es.json b/docs/translations/api-docs/switch-unstyled/switch-unstyled-es.json new file mode 100644 index 00000000000000..f93d4cbd8c7985 --- /dev/null +++ b/docs/translations/api-docs/switch-unstyled/switch-unstyled-es.json @@ -0,0 +1 @@ +{ "componentDescription": "", "propDescriptions": {}, "classDescriptions": {} } diff --git a/docs/translations/api-docs/switch-unstyled/switch-unstyled-fr.json b/docs/translations/api-docs/switch-unstyled/switch-unstyled-fr.json new file mode 100644 index 00000000000000..f93d4cbd8c7985 --- /dev/null +++ b/docs/translations/api-docs/switch-unstyled/switch-unstyled-fr.json @@ -0,0 +1 @@ +{ "componentDescription": "", "propDescriptions": {}, "classDescriptions": {} } diff --git a/docs/translations/api-docs/switch-unstyled/switch-unstyled-ja.json b/docs/translations/api-docs/switch-unstyled/switch-unstyled-ja.json new file mode 100644 index 00000000000000..f93d4cbd8c7985 --- /dev/null +++ b/docs/translations/api-docs/switch-unstyled/switch-unstyled-ja.json @@ -0,0 +1 @@ +{ "componentDescription": "", "propDescriptions": {}, "classDescriptions": {} } diff --git a/docs/translations/api-docs/switch-unstyled/switch-unstyled-pt.json b/docs/translations/api-docs/switch-unstyled/switch-unstyled-pt.json new file mode 100644 index 00000000000000..f93d4cbd8c7985 --- /dev/null +++ b/docs/translations/api-docs/switch-unstyled/switch-unstyled-pt.json @@ -0,0 +1 @@ +{ "componentDescription": "", "propDescriptions": {}, "classDescriptions": {} } diff --git a/docs/translations/api-docs/switch-unstyled/switch-unstyled-ru.json b/docs/translations/api-docs/switch-unstyled/switch-unstyled-ru.json new file mode 100644 index 00000000000000..f93d4cbd8c7985 --- /dev/null +++ b/docs/translations/api-docs/switch-unstyled/switch-unstyled-ru.json @@ -0,0 +1 @@ +{ "componentDescription": "", "propDescriptions": {}, "classDescriptions": {} } diff --git a/docs/translations/api-docs/switch-unstyled/switch-unstyled-zh.json b/docs/translations/api-docs/switch-unstyled/switch-unstyled-zh.json new file mode 100644 index 00000000000000..f93d4cbd8c7985 --- /dev/null +++ b/docs/translations/api-docs/switch-unstyled/switch-unstyled-zh.json @@ -0,0 +1 @@ +{ "componentDescription": "", "propDescriptions": {}, "classDescriptions": {} } diff --git a/docs/translations/api-docs/switch-unstyled/switch-unstyled.json b/docs/translations/api-docs/switch-unstyled/switch-unstyled.json new file mode 100644 index 00000000000000..4e4001f4926436 --- /dev/null +++ b/docs/translations/api-docs/switch-unstyled/switch-unstyled.json @@ -0,0 +1,16 @@ +{ + "componentDescription": "The foundation for building custom-styled switches.", + "propDescriptions": { + "checked": "If true, the component is checked.", + "className": "Class name applied to the root element.", + "component": "The component used for the Root slot. Either a string to use a HTML element or a component. This is equivalent to components.Root. If both are provided, the component is used.", + "components": "The components used for each slot inside the Switch. Either a string to use a HTML element or a component.", + "componentsProps": "The props used for each slot inside the Switch.", + "defaultChecked": "The default checked state. Use when the component is not controlled.", + "disabled": "If true, the component is disabled.", + "onChange": "Callback fired when the state is changed.

Signature:
function(event: object) => void
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": "If true, the component is read only.", + "required": "If true, the input element is required." + }, + "classDescriptions": {} +} diff --git a/packages/material-ui-system/src/createStyled.js b/packages/material-ui-system/src/createStyled.js index 2b55fcf31f2adb..a1aaae95486604 100644 --- a/packages/material-ui-system/src/createStyled.js +++ b/packages/material-ui-system/src/createStyled.js @@ -52,8 +52,9 @@ const variantsResolver = (props, styles, theme, name) => { return variantsStyles; }; -export const shouldForwardProp = (prop) => - prop !== 'styleProps' && prop !== 'theme' && prop !== 'sx' && prop !== 'as'; +export const shouldForwardProp = (prop) => { + return prop !== 'styleProps' && prop !== 'theme' && prop !== 'sx' && prop !== 'as'; +}; export const systemDefaultTheme = createTheme(); diff --git a/packages/material-ui-unstyled/package.json b/packages/material-ui-unstyled/package.json index 1bacd83923fc3c..516821ace5b0c8 100644 --- a/packages/material-ui-unstyled/package.json +++ b/packages/material-ui-unstyled/package.json @@ -48,6 +48,7 @@ }, "dependencies": { "@babel/runtime": "^7.4.4", + "@emotion/is-prop-valid": "^1.1.0", "@material-ui/utils": "5.0.0-beta.0", "clsx": "^1.0.4", "prop-types": "^15.7.2", diff --git a/packages/material-ui-unstyled/src/SwitchUnstyled/SwitchUnstyled.test.tsx b/packages/material-ui-unstyled/src/SwitchUnstyled/SwitchUnstyled.test.tsx new file mode 100644 index 00000000000000..d08d6d18ad3f76 --- /dev/null +++ b/packages/material-ui-unstyled/src/SwitchUnstyled/SwitchUnstyled.test.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; +import { createMount, createClientRender, describeConformanceUnstyled } from 'test/utils'; +import SwitchUnstyled, { + SwitchState, + switchUnstyledClasses, +} from '@material-ui/unstyled/SwitchUnstyled'; +import { expect } from 'chai'; + +describe('', () => { + const mount = createMount(); + const render = createClientRender(); + + describeConformanceUnstyled(, () => ({ + inheritComponent: 'span', + render, + mount, + refInstanceof: window.HTMLSpanElement, + testComponentPropWith: 'span', + muiName: 'MuiSwitch', + slots: { + root: { + expectedClassName: switchUnstyledClasses.root, + }, + thumb: { + expectedClassName: switchUnstyledClasses.thumb, + }, + input: { + testWithElement: 'input', + expectedClassName: switchUnstyledClasses.input, + }, + }, + })); + + describe('componentState', () => { + it('passes the styleProps prop to all the slots', () => { + interface CustomSlotProps { + styleProps: SwitchState; + children?: React.ReactNode; + } + + const CustomSlot = React.forwardRef( + ({ styleProps: sp, children }: CustomSlotProps, ref: React.Ref) => { + return ( +
+ {children} +
+ ); + }, + ); + + const components = { + Root: CustomSlot, + Input: CustomSlot, + Thumb: CustomSlot, + }; + + const { getAllByTestId } = render( + , + ); + const renderedComponents = getAllByTestId('custom'); + + expect(renderedComponents.length).to.equal(3); + for (let i = 0; i < renderedComponents.length; i += 1) { + expect(renderedComponents[i]).to.have.attribute('data-checked', 'true'); + expect(renderedComponents[i]).to.have.attribute('data-disabled', 'true'); + expect(renderedComponents[i]).to.have.attribute('data-readonly', 'false'); + expect(renderedComponents[i]).to.have.attribute('data-focusvisible', 'false'); + } + }); + }); +}); diff --git a/packages/material-ui-unstyled/src/SwitchUnstyled/SwitchUnstyled.tsx b/packages/material-ui-unstyled/src/SwitchUnstyled/SwitchUnstyled.tsx new file mode 100644 index 00000000000000..4a01265a4dea50 --- /dev/null +++ b/packages/material-ui-unstyled/src/SwitchUnstyled/SwitchUnstyled.tsx @@ -0,0 +1,211 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import clsx from 'clsx'; +import useSwitch, { SwitchState, UseSwitchProps } from './useSwitch'; +import classes from './switchUnstyledClasses'; +import { isHostComponent } from '../utils'; + +export interface SwitchUnstyledProps extends UseSwitchProps { + /** + * Class name applied to the root element. + */ + className?: string; + /** + * The component used for the Root slot. + * Either a string to use a HTML element or a component. + * This is equivalent to `components.Root`. If both are provided, the `component` is used. + */ + component?: React.ElementType; + /** + * The components used for each slot inside the Switch. + * Either a string to use a HTML element or a component. + * @default {} + */ + components?: { + Root?: React.ElementType; + Thumb?: React.ElementType; + Input?: React.ElementType; + }; + + /** + * The props used for each slot inside the Switch. + * @default {} + */ + componentsProps?: { + root?: {}; + thumb?: {}; + input?: {}; + }; +} + +const appendStyleProps = ( + component: React.ElementType, + componentsProps: Record, + state: SwitchState, +) => { + if (!isHostComponent(component)) { + componentsProps.styleProps = { ...componentsProps.styleProps, ...state }; + } +}; + +/** + * The foundation for building custom-styled switches. + * + * Demos: + * + * - [Switches](https://material-ui.com/components/switches/) + * + * API: + * + * - [SwitchUnstyled API](https://material-ui.com/api/switch-unstyled/) + */ +const SwitchUnstyled = React.forwardRef(function SwitchUnstyled( + props: SwitchUnstyledProps, + ref: React.ForwardedRef, +) { + const { + checked: checkedProp, + className, + component, + components = {}, + componentsProps = {}, + defaultChecked, + disabled: disabledProp, + onBlur, + onChange, + onFocus, + onFocusVisible, + readOnly: readOnlyProp, + required, + ...otherProps + } = props; + + const Root: React.ElementType = component ?? components.Root ?? 'span'; + const rootProps: any = { ...otherProps, ...componentsProps.root }; + + const Thumb: React.ElementType = components.Thumb ?? 'span'; + const thumbProps: any = componentsProps.thumb ?? {}; + + const Input: React.ElementType = components.Input ?? 'input'; + const inputProps: any = componentsProps.input ?? {}; + + const useSwitchProps = { + checked: checkedProp, + defaultChecked, + disabled: disabledProp, + onBlur, + onChange, + onFocus, + onFocusVisible, + readOnly: readOnlyProp, + }; + + const { getInputProps, checked, disabled, focusVisible, readOnly } = useSwitch(useSwitchProps); + + const styleProps: SwitchState = { + ...props, + checked, + disabled, + focusVisible, + readOnly, + }; + + appendStyleProps(Root, rootProps, styleProps); + appendStyleProps(Input, inputProps, styleProps); + appendStyleProps(Thumb, thumbProps, styleProps); + + const stateClasses = { + [classes.checked]: checked, + [classes.disabled]: disabled, + [classes.focusVisible]: focusVisible, + [classes.readOnly]: readOnly, + }; + + return ( + + + + + ); +}); + +SwitchUnstyled.propTypes /* remove-proptypes */ = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * If `true`, the component is checked. + */ + checked: PropTypes.bool, + /** + * Class name applied to the root element. + */ + className: PropTypes.string, + /** + * The component used for the Root slot. + * Either a string to use a HTML element or a component. + * This is equivalent to `components.Root`. If both are provided, the `component` is used. + */ + component: PropTypes.elementType, + /** + * The components used for each slot inside the Switch. + * Either a string to use a HTML element or a component. + * @default {} + */ + components: PropTypes.shape({ + Input: PropTypes.elementType, + Root: PropTypes.elementType, + Thumb: PropTypes.elementType, + }), + /** + * The props used for each slot inside the Switch. + * @default {} + */ + componentsProps: PropTypes.object, + /** + * The default checked state. Use when the component is not controlled. + */ + defaultChecked: PropTypes.bool, + /** + * If `true`, the component is disabled. + */ + disabled: PropTypes.bool, + /** + * @ignore + */ + onBlur: PropTypes.func, + /** + * Callback fired when the state is changed. + * + * @param {object} 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: PropTypes.func, + /** + * @ignore + */ + onFocus: PropTypes.func, + /** + * @ignore + */ + onFocusVisible: PropTypes.func, + /** + * If `true`, the component is read only. + */ + readOnly: PropTypes.bool, + /** + * If `true`, the `input` element is required. + */ + required: PropTypes.bool, +} as any; + +export default SwitchUnstyled; diff --git a/packages/material-ui-unstyled/src/SwitchUnstyled/index.ts b/packages/material-ui-unstyled/src/SwitchUnstyled/index.ts new file mode 100644 index 00000000000000..ee88158a20e4bf --- /dev/null +++ b/packages/material-ui-unstyled/src/SwitchUnstyled/index.ts @@ -0,0 +1,6 @@ +export { default } from './SwitchUnstyled'; +export * from './SwitchUnstyled'; +export { default as useSwitch } from './useSwitch'; +export * from './useSwitch'; +export { default as switchUnstyledClasses } from './switchUnstyledClasses'; +export * from './switchUnstyledClasses'; diff --git a/packages/material-ui-unstyled/src/SwitchUnstyled/switchUnstyledClasses.ts b/packages/material-ui-unstyled/src/SwitchUnstyled/switchUnstyledClasses.ts new file mode 100644 index 00000000000000..8d979870ca1fde --- /dev/null +++ b/packages/material-ui-unstyled/src/SwitchUnstyled/switchUnstyledClasses.ts @@ -0,0 +1,37 @@ +import generateUtilityClass from '../generateUtilityClass'; +import generateUtilityClasses from '../generateUtilityClasses'; + +export interface SwitchUnstyledClasses { + /** Class applied to the root element. */ + root: string; + /** Class applied to the internal input element */ + input: string; + /** Class applied to the thumb element */ + thumb: string; + /** Class applied to the root element if the switch is checked */ + checked: string; + /** Class applied to the root element if the switch is disabled */ + disabled: string; + /** Class applied to the root element if the switch has visible focus */ + focusVisible: string; + /** Class applied to the root element if the switch is read-only */ + readOnly: string; +} + +export type SwitchUnstyledClassKey = keyof SwitchUnstyledClasses; + +export function getSwitchUnstyledUtilityClass(slot: string): string { + return generateUtilityClass('MuiSwitch', slot); +} + +const switchUnstyledClasses: SwitchUnstyledClasses = generateUtilityClasses('MuiSwitch', [ + 'root', + 'input', + 'thumb', + 'checked', + 'disabled', + 'focusVisible', + 'readOnly', +]); + +export default switchUnstyledClasses; diff --git a/packages/material-ui-unstyled/src/SwitchUnstyled/useSwitch.test.tsx b/packages/material-ui-unstyled/src/SwitchUnstyled/useSwitch.test.tsx new file mode 100644 index 00000000000000..02b74d1a75d5ea --- /dev/null +++ b/packages/material-ui-unstyled/src/SwitchUnstyled/useSwitch.test.tsx @@ -0,0 +1,86 @@ +import { expect } from 'chai'; +import React from 'react'; +import { spy } from 'sinon'; +import { act, createClientRender } from 'test/utils'; +import { useSwitch, UseSwitchProps, UseSwitchResult } from '@material-ui/unstyled/SwitchUnstyled'; + +const TestComponent = React.forwardRef( + ({ useSwitchProps }: { useSwitchProps?: UseSwitchProps }, ref) => { + const switchDefinition = useSwitch(useSwitchProps ?? {}); + React.useImperativeHandle(ref, () => switchDefinition, [switchDefinition]); + return null; + }, +); + +describe('useSwitch', () => { + const render = createClientRender(); + const invokeUseSwitch = (props: UseSwitchProps) => { + const ref = React.createRef(); + render(); + return ref.current as UseSwitchResult; + }; + + describe('getInputProps', () => { + it('should include the incoming uncontrolled props in the output', () => { + const props: UseSwitchProps = { + defaultChecked: true, + disabled: true, + readOnly: true, + required: true, + }; + + const { getInputProps } = invokeUseSwitch(props); + const inputProps = getInputProps(); + + expect(inputProps.defaultChecked).to.equal(true); + expect(inputProps.disabled).to.equal(true); + expect(inputProps.readOnly).to.equal(true); + expect(inputProps.required).to.equal(true); + }); + + it('should include the incoming controlled prop in the output', () => { + const props = { + checked: true, + }; + + const { getInputProps } = invokeUseSwitch(props); + const inputProps = getInputProps(); + + expect(inputProps!.checked).to.equal(true); + }); + + it('should call the provided event handlers when respective events are fired', () => { + const props = { + onChange: spy(), + onFocus: spy(), + onFocusVisible: spy(), + onBlur: spy(), + }; + + const dummyChangeEvent = { + nativeEvent: { + defaultPrevented: false, + }, + target: { + checked: true, + }, + } as React.ChangeEvent; + + const dummyFocusEvent = {} as React.FocusEvent; + const dummyBlurEvent = {} as React.FocusEvent; + + act(() => { + const { getInputProps } = invokeUseSwitch(props); + const inputProps = getInputProps(); + inputProps.onChange(dummyChangeEvent); + inputProps.onFocus(dummyFocusEvent); + inputProps.onBlur(dummyBlurEvent); + }); + + expect(props.onChange.calledWith(dummyChangeEvent)).to.equal(true); + expect(props.onFocus.calledWith(dummyFocusEvent)).to.equal(true); + expect(props.onFocusVisible.calledWith(dummyFocusEvent)).to.equal(true); + expect(props.onBlur.calledWith(dummyBlurEvent)).to.equal(true); + }); + }); +}); diff --git a/packages/material-ui-unstyled/src/SwitchUnstyled/useSwitch.ts b/packages/material-ui-unstyled/src/SwitchUnstyled/useSwitch.ts new file mode 100644 index 00000000000000..bc478b095c749d --- /dev/null +++ b/packages/material-ui-unstyled/src/SwitchUnstyled/useSwitch.ts @@ -0,0 +1,185 @@ +import * as React from 'react'; +import { + unstable_useControlled as useControlled, + unstable_useEventCallback as useEventCallback, + unstable_useForkRef as useForkRef, + unstable_useIsFocusVisible as useIsFocusVisible, +} from '@material-ui/utils'; + +export interface SwitchState { + checked: Readonly; + disabled: Readonly; + readOnly: Readonly; + focusVisible: Readonly; +} + +export interface UseSwitchResult extends SwitchState { + /** + * Returns props for an HTML `input` element that is a part of a Switch. + */ + getInputProps: (otherProps?: React.HTMLAttributes) => SwitchInputProps; +} + +/** + * Props used by an HTML `input` element that is a part of a Switch. + */ +export interface SwitchInputProps { + checked?: boolean; + defaultChecked?: boolean; + disabled?: boolean; + onBlur: React.FocusEventHandler; + onChange: React.ChangeEventHandler; + onFocus: React.FocusEventHandler; + readOnly?: boolean; + ref: React.Ref; + required?: boolean; +} + +export interface UseSwitchProps { + /** + * 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. + */ + disabled?: boolean; + onBlur?: React.FocusEventHandler; + /** + * Callback fired when the state is changed. + * + * @param {object} 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; + onFocus?: React.FocusEventHandler; + onFocusVisible?: React.FocusEventHandler; + /** + * If `true`, the component is read only. + */ + readOnly?: boolean; + /** + * If `true`, the `input` element is required. + */ + required?: boolean; +} + +/** + * The basic building block for creating custom switches. + * + * Demos: + * + * - [Switches](https://material-ui.com/components/switches/) + */ +export default function useSwitch(props: UseSwitchProps) { + const { + checked: checkedProp, + defaultChecked, + disabled, + onBlur, + onChange, + onFocus, + onFocusVisible, + readOnly, + required, + } = props; + + const [checked, setCheckedState] = useControlled({ + controlled: checkedProp, + default: Boolean(defaultChecked), + name: 'Switch', + state: 'checked', + }); + + const handleInputChange = useEventCallback( + (event: React.ChangeEvent, otherHandler?: React.FormEventHandler) => { + // Workaround for https://github.com/facebook/react/issues/9023 + if (event.nativeEvent.defaultPrevented) { + return; + } + + setCheckedState(event.target.checked); + onChange?.(event); + otherHandler?.(event); + }, + ); + + const { + isFocusVisibleRef, + onBlur: handleBlurVisible, + onFocus: handleFocusVisible, + ref: focusVisibleRef, + } = useIsFocusVisible(); + + const [focusVisible, setFocusVisible] = React.useState(false); + if (disabled && focusVisible) { + setFocusVisible(false); + } + + React.useEffect(() => { + isFocusVisibleRef.current = focusVisible; + }, [focusVisible, isFocusVisibleRef]); + + const inputRef = React.useRef(null); + + const handleFocus = useEventCallback( + (event: React.FocusEvent, otherHandler?: React.FocusEventHandler) => { + // Fix for https://github.com/facebook/react/issues/7769 + if (!inputRef.current) { + inputRef.current = event.currentTarget; + } + + handleFocusVisible(event); + if (isFocusVisibleRef.current === true) { + setFocusVisible(true); + onFocusVisible?.(event); + } + + onFocus?.(event); + otherHandler?.(event); + }, + ); + + const handleBlur = useEventCallback( + (event: React.FocusEvent, otherHandler?: React.FocusEventHandler) => { + handleBlurVisible(event); + + if (isFocusVisibleRef.current === false) { + setFocusVisible(false); + } + + onBlur?.(event); + otherHandler?.(event); + }, + ); + + const handleRefChange = useForkRef(focusVisibleRef, inputRef); + + const getInputProps = (otherProps: React.HTMLAttributes = {}) => ({ + checked: checkedProp, + defaultChecked, + disabled, + readOnly, + required, + type: 'checkbox', + ...otherProps, + onChange: (event: React.ChangeEvent) => + handleInputChange(event, otherProps.onChange), + onFocus: (event: React.FocusEvent) => handleFocus(event, otherProps.onFocus), + onBlur: (event: React.FocusEvent) => handleBlur(event, otherProps.onBlur), + ref: handleRefChange, + }); + + return { + checked, + disabled: Boolean(disabled), + focusVisible, + getInputProps, + readOnly: Boolean(readOnly), + } as UseSwitchResult; +} diff --git a/packages/material-ui-unstyled/src/index.d.ts b/packages/material-ui-unstyled/src/index.d.ts index 5e2c860322674f..472ffc529c46dd 100644 --- a/packages/material-ui-unstyled/src/index.d.ts +++ b/packages/material-ui-unstyled/src/index.d.ts @@ -10,6 +10,9 @@ export * from './ModalUnstyled'; export { default as SliderUnstyled } from './SliderUnstyled'; export * from './SliderUnstyled'; +export { default as SwitchUnstyled } from './SwitchUnstyled'; +export * from './SwitchUnstyled'; + export { default as Portal } from './Portal'; export * from './Portal'; diff --git a/packages/material-ui-unstyled/src/index.js b/packages/material-ui-unstyled/src/index.js index 87391679d5687a..d58845ae969702 100644 --- a/packages/material-ui-unstyled/src/index.js +++ b/packages/material-ui-unstyled/src/index.js @@ -7,6 +7,9 @@ export * from './BadgeUnstyled'; export { default as SliderUnstyled } from './SliderUnstyled'; export * from './SliderUnstyled'; +export { default as SwitchUnstyled } from './SwitchUnstyled'; +export * from './SwitchUnstyled'; + export { default as ModalUnstyled } from './ModalUnstyled'; export * from './ModalUnstyled'; diff --git a/packages/material-ui-unstyled/src/utils/index.d.ts b/packages/material-ui-unstyled/src/utils/index.d.ts deleted file mode 100644 index ad997d95525494..00000000000000 --- a/packages/material-ui-unstyled/src/utils/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -/* eslint-disable import/prefer-default-export */ - -export function isHostComponent(component: React.ElementType): boolean; diff --git a/packages/material-ui-unstyled/src/utils/index.js b/packages/material-ui-unstyled/src/utils/index.ts similarity index 100% rename from packages/material-ui-unstyled/src/utils/index.js rename to packages/material-ui-unstyled/src/utils/index.ts diff --git a/packages/material-ui-unstyled/src/utils/isHostComponent.js b/packages/material-ui-unstyled/src/utils/isHostComponent.js deleted file mode 100644 index 79aabd52adc747..00000000000000 --- a/packages/material-ui-unstyled/src/utils/isHostComponent.js +++ /dev/null @@ -1,5 +0,0 @@ -function isHostComponent(element) { - return typeof element === 'string'; -} - -export default isHostComponent; diff --git a/packages/material-ui-unstyled/src/utils/isHostComponent.ts b/packages/material-ui-unstyled/src/utils/isHostComponent.ts new file mode 100644 index 00000000000000..94e4c950a06d49 --- /dev/null +++ b/packages/material-ui-unstyled/src/utils/isHostComponent.ts @@ -0,0 +1,10 @@ +import React from 'react'; + +/** + * Determines if a given element is a DOM element name (i.e. not a React component). + */ +function isHostComponent(element: React.ElementType) { + return typeof element === 'string'; +} + +export default isHostComponent; diff --git a/packages/material-ui-unstyled/tsconfig.build.json b/packages/material-ui-unstyled/tsconfig.build.json index 722cf118a0eedf..a45de38c5d2fd4 100644 --- a/packages/material-ui-unstyled/tsconfig.build.json +++ b/packages/material-ui-unstyled/tsconfig.build.json @@ -10,6 +10,6 @@ "outDir": "build", "rootDir": "./src" }, - "include": ["src/**/*.ts"], - "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"] + "include": ["src/**/*.ts*"], + "exclude": ["src/**/*.spec.ts*", "src/**/*.test.ts*"] } diff --git a/packages/material-ui/src/ButtonBase/TouchRipple.d.ts b/packages/material-ui/src/ButtonBase/TouchRipple.d.ts index b26831ed4e979b..24fde425c0a987 100644 --- a/packages/material-ui/src/ButtonBase/TouchRipple.d.ts +++ b/packages/material-ui/src/ButtonBase/TouchRipple.d.ts @@ -4,6 +4,21 @@ import { TouchRippleClasses, TouchRippleClassKey } from './touchRippleClasses'; export { TouchRippleClassKey }; +export interface StartActionOptions { + pulsate?: boolean; + center?: boolean; +} + +export interface TouchRippleActions { + start: ( + event?: React.SyntheticEvent, + options?: StartActionOptions, + callback?: () => void, + ) => void; + pulsate: (event?: React.SyntheticEvent) => void; + stop: (event?: React.SyntheticEvent, callback?: () => void) => void; +} + export type TouchRippleProps = StandardProps> & { center?: boolean; /** @@ -12,6 +27,6 @@ export type TouchRippleProps = StandardProps> classes?: Partial; }; -declare const TouchRipple: React.JSXElementConstructor; +declare const TouchRipple: React.ForwardRefRenderFunction; export default TouchRipple; diff --git a/packages/material-ui/src/Switch/Switch.test.js b/packages/material-ui/src/Switch/Switch.test.js index 1e5c427a88b184..c69c81140a9c95 100644 --- a/packages/material-ui/src/Switch/Switch.test.js +++ b/packages/material-ui/src/Switch/Switch.test.js @@ -63,6 +63,20 @@ describe('', () => { expect(getByRole('checkbox')).to.have.property('readOnly', true); }); + specify('renders a custom icon when provided', () => { + const { getByTestId } = render(} />); + + expect(getByTestId('icon')).toBeVisible(); + }); + + specify('renders a custom checked icon when provided', () => { + const { getByTestId } = render( + } />, + ); + + expect(getByTestId('icon')).toBeVisible(); + }); + specify('the Checked state changes after change events', () => { const { getByRole } = render(); diff --git a/packages/material-ui/src/useTouchRipple/index.ts b/packages/material-ui/src/useTouchRipple/index.ts new file mode 100644 index 00000000000000..31e422f495fd10 --- /dev/null +++ b/packages/material-ui/src/useTouchRipple/index.ts @@ -0,0 +1 @@ +export { default } from './useTouchRipple'; diff --git a/packages/material-ui/src/useTouchRipple/useTouchRipple.ts b/packages/material-ui/src/useTouchRipple/useTouchRipple.ts new file mode 100644 index 00000000000000..5f444d8e570026 --- /dev/null +++ b/packages/material-ui/src/useTouchRipple/useTouchRipple.ts @@ -0,0 +1,161 @@ +import * as React from 'react'; +import { TouchRippleActions } from '../ButtonBase/TouchRipple'; +import { useEventCallback } from '../utils'; + +interface UseTouchRippleProps { + disabled: boolean; + disableFocusRipple?: boolean; + disableRipple?: boolean; + disableTouchRipple?: boolean; + focusVisible: boolean; + rippleRef: React.RefObject; +} + +interface RippleEventHandlers { + onBlur: React.FocusEventHandler; + onContextMenu: React.MouseEventHandler; + onDragLeave: React.DragEventHandler; + onKeyDown: React.KeyboardEventHandler; + onKeyUp: React.KeyboardEventHandler; + onMouseDown: React.MouseEventHandler; + onMouseLeave: React.MouseEventHandler; + onMouseUp: React.MouseEventHandler; + onTouchEnd: React.TouchEventHandler; + onTouchMove: React.TouchEventHandler; + onTouchStart: React.TouchEventHandler; +} + +const useTouchRipple = (props: UseTouchRippleProps) => { + const { + disabled, + disableFocusRipple, + disableRipple, + disableTouchRipple, + focusVisible, + rippleRef, + } = props; + + React.useEffect(() => { + if (focusVisible && !disableFocusRipple && !disableRipple) { + rippleRef.current?.pulsate(); + } + }, [rippleRef, focusVisible, disableFocusRipple, disableRipple]); + + function useRippleHandler( + rippleAction: keyof TouchRippleActions, + eventCallback?: (event: React.SyntheticEvent) => void, + skipRippleAction = disableTouchRipple, + ) { + return useEventCallback((event: React.SyntheticEvent) => { + eventCallback?.(event); + + if (!skipRippleAction && rippleRef.current) { + rippleRef.current[rippleAction](event); + } + + return true; + }); + } + + const keydownRef = React.useRef(false); + const handleKeyDown = useEventCallback((event: React.KeyboardEvent) => { + if ( + !disableFocusRipple && + !keydownRef.current && + focusVisible && + rippleRef.current && + event.key === ' ' + ) { + keydownRef.current = true; + rippleRef.current.stop(event, () => { + rippleRef?.current?.start(event); + }); + } + }); + + const handleKeyUp = useEventCallback((event: React.KeyboardEvent) => { + // calling preventDefault in keyUp on a - + ); } diff --git a/docs/src/pages/components/buttons/ColorButtons.tsx b/docs/src/pages/components/buttons/ColorButtons.tsx index 5a5a6a50b83b69..3ebe9c6dce08ca 100644 --- a/docs/src/pages/components/buttons/ColorButtons.tsx +++ b/docs/src/pages/components/buttons/ColorButtons.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; -import Box from '@material-ui/core/Box'; +import Stack from '@material-ui/core/Stack'; import Button from '@material-ui/core/Button'; export default function ColorButtons() { return ( - :not(style)': { m: 1 } }}> + - + ); } diff --git a/docs/src/pages/components/buttons/ContainedButtons.js b/docs/src/pages/components/buttons/ContainedButtons.js index 091153f333bc6f..f861c48084543a 100644 --- a/docs/src/pages/components/buttons/ContainedButtons.js +++ b/docs/src/pages/components/buttons/ContainedButtons.js @@ -4,7 +4,7 @@ import Stack from '@material-ui/core/Stack'; export default function ContainedButtons() { return ( - + diff --git a/docs/src/pages/components/buttons/IconLabelButtons.tsx b/docs/src/pages/components/buttons/IconLabelButtons.tsx index c94b1927d15aa5..4259e743a3f9b8 100644 --- a/docs/src/pages/components/buttons/IconLabelButtons.tsx +++ b/docs/src/pages/components/buttons/IconLabelButtons.tsx @@ -6,7 +6,7 @@ import Stack from '@material-ui/core/Stack'; export default function IconLabelButtons() { return ( - + diff --git a/docs/src/pages/components/buttons/LoadingButtons.js b/docs/src/pages/components/buttons/LoadingButtons.js index be943f7deeb156..37a78f8d1df82b 100644 --- a/docs/src/pages/components/buttons/LoadingButtons.js +++ b/docs/src/pages/components/buttons/LoadingButtons.js @@ -5,7 +5,7 @@ import Stack from '@material-ui/core/Stack'; export default function LoadingButtons() { return ( - + Submit diff --git a/docs/src/pages/components/buttons/LoadingButtons.tsx b/docs/src/pages/components/buttons/LoadingButtons.tsx index be943f7deeb156..37a78f8d1df82b 100644 --- a/docs/src/pages/components/buttons/LoadingButtons.tsx +++ b/docs/src/pages/components/buttons/LoadingButtons.tsx @@ -5,7 +5,7 @@ import Stack from '@material-ui/core/Stack'; export default function LoadingButtons() { return ( - + Submit diff --git a/docs/src/pages/components/buttons/OutlinedButtons.js b/docs/src/pages/components/buttons/OutlinedButtons.js index d8e6f89bc2a05f..6f2f4b07043999 100644 --- a/docs/src/pages/components/buttons/OutlinedButtons.js +++ b/docs/src/pages/components/buttons/OutlinedButtons.js @@ -4,7 +4,7 @@ import Stack from '@material-ui/core/Stack'; export default function OutlinedButtons() { return ( - + diff --git a/docs/src/pages/components/buttons/TextButtons.tsx b/docs/src/pages/components/buttons/TextButtons.tsx index 5b96bc4abf3aba..99b2d43f2c8c79 100644 --- a/docs/src/pages/components/buttons/TextButtons.tsx +++ b/docs/src/pages/components/buttons/TextButtons.tsx @@ -4,7 +4,7 @@ import Stack from '@material-ui/core/Stack'; export default function TextButtons() { return ( - + diff --git a/docs/src/pages/components/buttons/UploadButtons.js b/docs/src/pages/components/buttons/UploadButtons.js index f2292ae294f7b9..1a2b9b7f4e8868 100644 --- a/docs/src/pages/components/buttons/UploadButtons.js +++ b/docs/src/pages/components/buttons/UploadButtons.js @@ -11,7 +11,7 @@ const Input = styled('input')({ export default function UploadButtons() { return ( - +