From 30852a2f2471d006b630121aafc811b2713d29e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Mon, 7 Jun 2021 12:06:30 +0200 Subject: [PATCH 01/58] Add initial implementation of useSwitch and SwitchUnstyled --- .../components/switches/UnstyledSwitches.js | 52 ++++++++++++++++ .../pages/components/switches/UseSwitches.js | 59 +++++++++++++++++++ .../src/pages/components/switches/switches.md | 8 +++ .../src/SwitchUnstyled/SwitchUnstyled.tsx | 50 ++++++++++++++++ .../src/SwitchUnstyled/index.ts | 5 ++ .../SwitchUnstyled/switchUnstyledClasses.ts | 23 ++++++++ .../src/SwitchUnstyled/useSwitch.ts | 29 +++++++++ 7 files changed, 226 insertions(+) create mode 100644 docs/src/pages/components/switches/UnstyledSwitches.js create mode 100644 docs/src/pages/components/switches/UseSwitches.js 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.ts diff --git a/docs/src/pages/components/switches/UnstyledSwitches.js b/docs/src/pages/components/switches/UnstyledSwitches.js new file mode 100644 index 00000000000000..851f5e4f25bd7f --- /dev/null +++ b/docs/src/pages/components/switches/UnstyledSwitches.js @@ -0,0 +1,52 @@ +import * as React from 'react'; +import { styled } from '@material-ui/core/styles'; +import SwitchUnstyled, { + switchUnstyledClasses, +} from '@material-ui/unstyled/SwitchUnstyled'; + +const Root = styled('span')({ + display: 'inline-block', + width: '60px', + height: '40px', + background: + 'linear-gradient(90deg, rgba(34,193,195,1) 0%, rgba(87,146,227,1) 100%)', + borderRadius: '20px', + margin: '10px', + cursor: 'pointer', + + [`&.${switchUnstyledClasses.disabled}`]: { + opacity: 0.5, + cursor: 'not-allowed', + }, +}); + +const Thumb = styled('span')({ + display: 'block', + width: '24px', + height: '24px', + top: '8px', + left: '8px', + borderRadius: '16px', + backgroundColor: 'rgba(255,255,255,0.7)', + position: 'relative', + transition: 'all 200ms ease', +}); + +const Input = styled('input')({ + opacity: 0, +}); + +export default function UnstyledSwitches() { + const hiddenInputProps = { + input: { + style: { opacity: 0 }, + }, + }; + + return ( +
+ + +
+ ); +} diff --git a/docs/src/pages/components/switches/UseSwitches.js b/docs/src/pages/components/switches/UseSwitches.js new file mode 100644 index 00000000000000..10a395a6b8b362 --- /dev/null +++ b/docs/src/pages/components/switches/UseSwitches.js @@ -0,0 +1,59 @@ +import * as React from 'react'; +import clsx from 'clsx'; +import { styled } from '@material-ui/core/styles'; +import { useSwitch } from '@material-ui/unstyled/SwitchUnstyled'; + +const FancySwitchElement = styled('span')({ + display: 'inline-block', + width: '60px', + height: '40px', + background: + 'linear-gradient(90deg, rgba(34,193,195,1) 0%, rgba(87,146,227,1) 100%)', + borderRadius: '20px', + margin: '10px', + cursor: 'pointer', + + '& .thumb': { + display: 'block', + width: '24px', + height: '24px', + top: '8px', + left: '8px', + borderRadius: '16px', + backgroundColor: 'rgba(255,255,255,0.7)', + position: 'relative', + transition: 'all 200ms ease', + + '&.checked': { + left: '24px', + top: '4px', + width: '32px', + height: '32px', + backgroundColor: 'rgba(255,255,255,0.9)', + }, + }, + + '&:hover .thumb': { + transform: 'scale(1.2)', + }, +}); + +function FancySwitch(props) { + const { getRootProps, isChecked } = useSwitch(props); + + return ( + + + + ); +} + +export default function UseSwitch() { + const [checked, setChecked] = React.useState(false); + + return ( +
+ setChecked((c) => !c)} /> +
+ ); +} diff --git a/docs/src/pages/components/switches/switches.md b/docs/src/pages/components/switches/switches.md index d22345fba2928d..63a2b610eb8c0a 100644 --- a/docs/src/pages/components/switches/switches.md +++ b/docs/src/pages/components/switches/switches.md @@ -57,6 +57,14 @@ 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 + +{{"demo": "pages/components/switches/UnstyledSwitches.js"}} + +## useSwitch hook + +{{"demo": "pages/components/switches/UseSwitches.js"}} + ## Label placement You can change the placement of the label: 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..f21217959adce1 --- /dev/null +++ b/packages/material-ui-unstyled/src/SwitchUnstyled/SwitchUnstyled.tsx @@ -0,0 +1,50 @@ +import React, { ElementType } from 'react'; +import clsx from 'clsx'; +import useSwitch, { SwitchProps } from './useSwitch'; +import classes from './switchUnstyledClasses'; + +export interface SwitchUnstyledProps extends SwitchProps { + components?: { + Root?: ElementType; + Thumb?: ElementType; + Input?: ElementType; + }; + componentsProps?: { + root?: Record; + thumb?: Record; + input?: Record; + }; +} + +const SwitchUnstyled = function SwitchUnstyled(props: SwitchUnstyledProps) { + const { components = {}, componentsProps = {}, ...otherProps } = props; + + const Root: ElementType = components.Root ?? 'span'; + const rootProps = componentsProps.root ?? {}; + const Thumb: ElementType = components.Thumb ?? 'span'; + const thumbProps = componentsProps.thumb ?? {}; + const Input: ElementType = components.Input ?? 'input'; + const inputProps = componentsProps.input ?? {}; + + const { getRootProps, isChecked } = useSwitch(otherProps); + + const computedClasses = { + [classes.checked]: isChecked, + [classes.disabled]: props.disabled, + }; + + return ( + + + + + ); +}; + +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..040c44d5f8a7eb --- /dev/null +++ b/packages/material-ui-unstyled/src/SwitchUnstyled/index.ts @@ -0,0 +1,5 @@ +export { default } 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..f770387504b13e --- /dev/null +++ b/packages/material-ui-unstyled/src/SwitchUnstyled/switchUnstyledClasses.ts @@ -0,0 +1,23 @@ +import { generateUtilityClass, generateUtilityClasses } from '@material-ui/unstyled'; + +export interface SwitchUnstyledClasses { + root: string; + checked: string; + disabled: string; + input: string; +} + +export type SwitchUnstyledClassKey = keyof SwitchUnstyledClasses; + +export function getSwitchUtilityClass(slot: string): string { + return generateUtilityClass('SwitchUnstyled', slot); +} + +const switchUnstyledClasses: SwitchUnstyledClasses = generateUtilityClasses('SwitchUnstyled', [ + 'root', + 'checked', + 'disabled', + 'input', +]); + +export default switchUnstyledClasses; 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..4d4df875e1a222 --- /dev/null +++ b/packages/material-ui-unstyled/src/SwitchUnstyled/useSwitch.ts @@ -0,0 +1,29 @@ +import React from 'react'; + +export interface SwitchProps { + checked?: boolean; + disabled?: boolean; + onChange?: (checked: boolean) => void; + onKeyDown?: React.KeyboardEventHandler; +} + +export default function useSwitch({ checked, disabled, onChange, onKeyDown }: SwitchProps) { + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!disabled && e.key === ' ') { + e.preventDefault(); + onChange?.(!checked); + } + + onKeyDown?.(e); + }; + + return { + getRootProps: () => ({ + tabIndex: 0, + role: 'checkbox', + onClick: () => !disabled && onChange?.(!checked), + onKeyDown: handleKeyDown, + }), + isChecked: checked, + }; +} From 9b9c56e0c211791e0deb1a53fdfe3186038983c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Mon, 7 Jun 2021 17:18:27 +0200 Subject: [PATCH 02/58] Use checkbox to handle switch state --- .../components/switches/UnstyledSwitches.js | 55 +++++++++++-------- .../pages/components/switches/UseSwitches.js | 54 +++++++++++++----- .../src/SwitchUnstyled/SwitchUnstyled.tsx | 34 ++++++++---- .../SwitchUnstyled/switchUnstyledClasses.ts | 2 + .../src/SwitchUnstyled/useSwitch.ts | 40 +++++++++----- 5 files changed, 123 insertions(+), 62 deletions(-) diff --git a/docs/src/pages/components/switches/UnstyledSwitches.js b/docs/src/pages/components/switches/UnstyledSwitches.js index 851f5e4f25bd7f..90cd68dfd6bdde 100644 --- a/docs/src/pages/components/switches/UnstyledSwitches.js +++ b/docs/src/pages/components/switches/UnstyledSwitches.js @@ -5,11 +5,13 @@ import SwitchUnstyled, { } from '@material-ui/unstyled/SwitchUnstyled'; const Root = styled('span')({ + fontSize: 0, + position: 'relative', display: 'inline-block', - width: '60px', - height: '40px', + width: '40px', + height: '60px', background: - 'linear-gradient(90deg, rgba(34,193,195,1) 0%, rgba(87,146,227,1) 100%)', + 'linear-gradient(60deg, rgba(34,193,195,1) 0%, rgba(87,146,227,1) 100%)', borderRadius: '20px', margin: '10px', cursor: 'pointer', @@ -18,35 +20,40 @@ const Root = styled('span')({ opacity: 0.5, cursor: 'not-allowed', }, -}); -const Thumb = styled('span')({ - display: 'block', - width: '24px', - height: '24px', - top: '8px', - left: '8px', - borderRadius: '16px', - backgroundColor: 'rgba(255,255,255,0.7)', - position: 'relative', - transition: 'all 200ms ease', -}); + [`& .${switchUnstyledClasses.thumb}`]: { + display: 'block', + width: '24px', + height: '24px', + top: '8px', + left: '8px', + borderRadius: '16px', + backgroundColor: 'rgba(255,255,255,0.7)', + position: 'relative', + transition: 'all 200ms ease', + }, -const Input = styled('input')({ - opacity: 0, + [`&.${switchUnstyledClasses.checked} .${switchUnstyledClasses.thumb}`]: { + left: '4px', + top: '24px', + width: '32px', + height: '32px', + backgroundColor: 'rgba(255,255,255,0.9)', + }, }); export default function UnstyledSwitches() { - const hiddenInputProps = { - input: { - style: { opacity: 0 }, - }, - }; + const [checked, setChecked] = React.useState(false); return (
- - + setChecked((c) => !c)} + components={{ Root }} + /> + +
); } diff --git a/docs/src/pages/components/switches/UseSwitches.js b/docs/src/pages/components/switches/UseSwitches.js index 10a395a6b8b362..11d9d770140a2d 100644 --- a/docs/src/pages/components/switches/UseSwitches.js +++ b/docs/src/pages/components/switches/UseSwitches.js @@ -5,13 +5,15 @@ import { useSwitch } from '@material-ui/unstyled/SwitchUnstyled'; const FancySwitchElement = styled('span')({ display: 'inline-block', - width: '60px', - height: '40px', + width: '40px', + height: '60px', background: - 'linear-gradient(90deg, rgba(34,193,195,1) 0%, rgba(87,146,227,1) 100%)', + 'linear-gradient(60deg, rgba(104,34,195,1) 0%, rgba(87,146,227,1) 100%)', borderRadius: '20px', margin: '10px', + position: 'relative', cursor: 'pointer', + fontSize: 0, '& .thumb': { display: 'block', @@ -23,27 +25,49 @@ const FancySwitchElement = styled('span')({ backgroundColor: 'rgba(255,255,255,0.7)', position: 'relative', transition: 'all 200ms ease', + boxShadow: '0 0 15px rgba(0,0,0,0.25)', + }, - '&.checked': { - left: '24px', - top: '4px', - width: '32px', - height: '32px', - backgroundColor: 'rgba(255,255,255,0.9)', - }, + '&.checked .thumb': { + left: '4px', + top: '24px', + width: '32px', + height: '32px', + backgroundColor: 'rgba(255,255,255,0.9)', }, - '&:hover .thumb': { + '&:not(.disabled):hover .thumb': { transform: 'scale(1.2)', }, + + '& input': { + position: 'absolute', + width: '100%', + height: '100%', + top: 0, + left: 0, + opacity: 0, + zIndex: 1, + margin: 0, + cursor: 'inherit', + }, + + '&.disabled': { + opacity: 0.5, + cursor: 'not-allowed', + }, }); function FancySwitch(props) { - const { getRootProps, isChecked } = useSwitch(props); + const { getRootProps, getInputProps, isChecked } = useSwitch(props); return ( - - + + + ); } @@ -54,6 +78,8 @@ export default function UseSwitch() { return (
setChecked((c) => !c)} /> + +
); } diff --git a/packages/material-ui-unstyled/src/SwitchUnstyled/SwitchUnstyled.tsx b/packages/material-ui-unstyled/src/SwitchUnstyled/SwitchUnstyled.tsx index f21217959adce1..c70b6b3d7e2bc7 100644 --- a/packages/material-ui-unstyled/src/SwitchUnstyled/SwitchUnstyled.tsx +++ b/packages/material-ui-unstyled/src/SwitchUnstyled/SwitchUnstyled.tsx @@ -1,5 +1,6 @@ import React, { ElementType } from 'react'; import clsx from 'clsx'; +import { styled } from '@material-ui/system'; import useSwitch, { SwitchProps } from './useSwitch'; import classes from './switchUnstyledClasses'; @@ -16,17 +17,34 @@ export interface SwitchUnstyledProps extends SwitchProps { }; } +const BarelyStyledRoot = styled('span')({ + position: 'relative', + cursor: 'pointer', +}); + +const BarelyStyledInput = styled('input')({ + position: 'absolute', + width: '100%', + height: '100%', + top: 0, + left: 0, + opacity: 0, + zIndex: 1, + cursor: 'inherit', + margin: 0, +}); + const SwitchUnstyled = function SwitchUnstyled(props: SwitchUnstyledProps) { const { components = {}, componentsProps = {}, ...otherProps } = props; - const Root: ElementType = components.Root ?? 'span'; + const Root: ElementType = components.Root ?? BarelyStyledRoot; const rootProps = componentsProps.root ?? {}; const Thumb: ElementType = components.Thumb ?? 'span'; const thumbProps = componentsProps.thumb ?? {}; - const Input: ElementType = components.Input ?? 'input'; + const Input: ElementType = components.Input ?? BarelyStyledInput; const inputProps = componentsProps.input ?? {}; - const { getRootProps, isChecked } = useSwitch(otherProps); + const { getRootProps, getInputProps, isChecked } = useSwitch(otherProps); const computedClasses = { [classes.checked]: isChecked, @@ -35,14 +53,8 @@ const SwitchUnstyled = function SwitchUnstyled(props: SwitchUnstyledProps) { return ( - - + + ); }; diff --git a/packages/material-ui-unstyled/src/SwitchUnstyled/switchUnstyledClasses.ts b/packages/material-ui-unstyled/src/SwitchUnstyled/switchUnstyledClasses.ts index f770387504b13e..cab1e1fa646e81 100644 --- a/packages/material-ui-unstyled/src/SwitchUnstyled/switchUnstyledClasses.ts +++ b/packages/material-ui-unstyled/src/SwitchUnstyled/switchUnstyledClasses.ts @@ -5,6 +5,7 @@ export interface SwitchUnstyledClasses { checked: string; disabled: string; input: string; + thumb: string; } export type SwitchUnstyledClassKey = keyof SwitchUnstyledClasses; @@ -18,6 +19,7 @@ const switchUnstyledClasses: SwitchUnstyledClasses = generateUtilityClasses('Swi 'checked', 'disabled', 'input', + 'thumb', ]); export default switchUnstyledClasses; diff --git a/packages/material-ui-unstyled/src/SwitchUnstyled/useSwitch.ts b/packages/material-ui-unstyled/src/SwitchUnstyled/useSwitch.ts index 4d4df875e1a222..6456f4f74cd3f6 100644 --- a/packages/material-ui-unstyled/src/SwitchUnstyled/useSwitch.ts +++ b/packages/material-ui-unstyled/src/SwitchUnstyled/useSwitch.ts @@ -1,28 +1,42 @@ -import React from 'react'; +import { useControlled } from '@material-ui/core/utils'; +import React, { ChangeEvent, ChangeEventHandler } from 'react'; export interface SwitchProps { checked?: boolean; + defaultChecked?: boolean; disabled?: boolean; - onChange?: (checked: boolean) => void; + onChange?: ChangeEventHandler<{ checked: boolean }>; onKeyDown?: React.KeyboardEventHandler; } -export default function useSwitch({ checked, disabled, onChange, onKeyDown }: SwitchProps) { - const handleKeyDown = (e: React.KeyboardEvent) => { - if (!disabled && e.key === ' ') { - e.preventDefault(); - onChange?.(!checked); +export default function useSwitch({ + checked: checkedProp, + defaultChecked, + onChange, + ...props +}: SwitchProps) { + const [checked, setCheckedState] = useControlled({ + controlled: checkedProp, + default: Boolean(defaultChecked), + name: 'SwitchUnstyled', + state: 'checked', + }); + + const handleInputChange = (event: ChangeEvent<{ checked: boolean }>) => { + // Workaround for https://github.com/facebook/react/issues/9023 + if (event.nativeEvent.defaultPrevented) { + return; } - onKeyDown?.(e); + setCheckedState(event.target.checked); + onChange?.(event); }; return { - getRootProps: () => ({ - tabIndex: 0, - role: 'checkbox', - onClick: () => !disabled && onChange?.(!checked), - onKeyDown: handleKeyDown, + getRootProps: () => ({}), + getInputProps: () => ({ + ...props, + onChange: handleInputChange, }), isChecked: checked, }; From fe119628ee5fcf97d472405891e9b7889390d419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dudak?= Date: Wed, 9 Jun 2021 12:01:36 +0200 Subject: [PATCH 03/58] Add `ButtonUnstyled` and use it in `SwitchUnstyled` --- .../components/switches/UnstyledSwitches.js | 11 + .../src/ButtonUnstyled/ButtonUnstyled.tsx | 250 ++++++++++++++++++ .../ButtonUnstyled/buttonUnstyledClasses.ts | 17 ++ .../src/SwitchUnstyled/SwitchUnstyled.tsx | 27 +- .../SwitchUnstyled/switchUnstyledClasses.ts | 4 + .../src/SwitchUnstyled/useSwitch.ts | 5 +- 6 files changed, 301 insertions(+), 13 deletions(-) create mode 100644 packages/material-ui-unstyled/src/ButtonUnstyled/ButtonUnstyled.tsx create mode 100644 packages/material-ui-unstyled/src/ButtonUnstyled/buttonUnstyledClasses.ts diff --git a/docs/src/pages/components/switches/UnstyledSwitches.js b/docs/src/pages/components/switches/UnstyledSwitches.js index 90cd68dfd6bdde..17c5b08e75e04c 100644 --- a/docs/src/pages/components/switches/UnstyledSwitches.js +++ b/docs/src/pages/components/switches/UnstyledSwitches.js @@ -33,6 +33,17 @@ const Root = styled('span')({ transition: 'all 200ms ease', }, + [`& .${switchUnstyledClasses.button}`]: { + display: 'block', + height: '100%', + borderRadius: '20px' + }, + + [`& .${switchUnstyledClasses.focusVisible} .${switchUnstyledClasses.thumb}`]: { + backgroundColor: 'rgba(255,255,255,1)', + boxShadow: '0 0 1px 10px rgba(125,230,232,0.7)', + }, + [`&.${switchUnstyledClasses.checked} .${switchUnstyledClasses.thumb}`]: { left: '4px', top: '24px', diff --git a/packages/material-ui-unstyled/src/ButtonUnstyled/ButtonUnstyled.tsx b/packages/material-ui-unstyled/src/ButtonUnstyled/ButtonUnstyled.tsx new file mode 100644 index 00000000000000..e0d64cdf8b5de8 --- /dev/null +++ b/packages/material-ui-unstyled/src/ButtonUnstyled/ButtonUnstyled.tsx @@ -0,0 +1,250 @@ +import React, { ElementType, forwardRef, ReactNode, Ref } from 'react'; +import clsx from 'clsx'; +import { + unstable_useEventCallback as useEventCallback, + unstable_useForkRef as useForkRef, + unstable_useIsFocusVisible as useIsFocusVisible } from '@material-ui/utils'; +import { unstable_composeClasses as composeClasses } from '@material-ui/unstyled'; +import { getButtonUnstyledUtilityClass } from './buttonUnstyledClasses'; + +export interface ButtonBaseActions { + focusVisible(): void; +} + +export interface ButtonUnstyledProps { + className?: string; + components?: { + Root?: ElementType; + }, + componentsProps?: { + root?: Record; + }, + children?: ReactNode; + disabled?: boolean; + action?: React.Ref; + onMouseLeave?: React.MouseEventHandler; + onFocus?: React.FocusEventHandler; + focusVisibleClassName?: string; + onFocusVisible?: React.FocusEventHandler; + onBlur?: React.FocusEventHandler; + onClick?: React.MouseEventHandler; + onKeyDown?: React.KeyboardEventHandler; + onKeyUp?: React.KeyboardEventHandler; + type?: string; + href?: string; + tabIndex?: number | string; +} + +const useUtilityClasses = (styleProps: ButtonUnstyledProps & { focusVisible: boolean }) => { + const { disabled, focusVisible, focusVisibleClassName } = styleProps; + + const slots = { + root: ['root', disabled && 'disabled', focusVisible && 'focusVisible', focusVisible && focusVisibleClassName], + }; + + return composeClasses(slots, getButtonUnstyledUtilityClass, {}); +}; + +function isAnchor(el: HTMLElement | undefined) : el is HTMLAnchorElement { + return el?.tagName === 'A'; +} + +const ButtonUnstyled = forwardRef(function ButtonUnstyled(props: ButtonUnstyledProps, ref: Ref) { + + const { + className, + components = {}, + componentsProps, + children, + disabled, + action, + onBlur, + onClick, + onFocus, + onFocusVisible, + onKeyDown, + onKeyUp, + onMouseLeave, + type, + href, + tabIndex = 0, + focusVisibleClassName, + ...otherProps + } = props; + + const buttonRef = React.useRef(); + + const ButtonRoot = components.Root ?? 'button'; + const buttonRootProps = componentsProps?.root ?? {}; + + const { + isFocusVisibleRef, + onFocus: handleFocusVisible, + onBlur: handleBlurVisible, + ref: focusVisibleRef, + } = useIsFocusVisible(); + + const [focusVisible, setFocusVisible] = React.useState(false); + if (disabled && focusVisible) { + setFocusVisible(false); + } + + React.useEffect(() => { + isFocusVisibleRef.current = focusVisible; + }, [focusVisible, isFocusVisibleRef]); + + React.useImperativeHandle( + action, + () => ({ + focusVisible: () => { + setFocusVisible(true); + buttonRef?.current?.focus(); + }, + }), + [], + ); + + const handleMouseLeave = (event: React.MouseEvent) => { + if (focusVisible) { + event.preventDefault(); + } + + onMouseLeave?.(event); + }; + + const handleBlur = (event: React.FocusEvent) => { + handleBlurVisible(event); + + if (isFocusVisibleRef.current === false) { + setFocusVisible(false); + } + + onBlur?.(event); + }; + + const handleFocus = useEventCallback((event: React.FocusEvent) => { + // Fix for https://github.com/facebook/react/issues/7769 + if (!buttonRef.current) { + buttonRef.current = event.currentTarget; + } + + handleFocusVisible(event); + if (isFocusVisibleRef.current === true) { + setFocusVisible(true); + onFocusVisible?.(event); + } + + if (onFocus) { + onFocus(event); + } + }); + + const isNonNativeButton = () => { + const button = buttonRef.current; + return ButtonRoot && ButtonRoot !== 'button' && !(isAnchor(button) && button?.href); + }; + + /** + * IE11 shim for https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat + */ + const keydownRef = React.useRef(false); + const handleKeyDown = useEventCallback((event: React.KeyboardEvent) => { + // Check if key is already down to avoid repeats being counted as multiple activations + if ( + !keydownRef.current && + focusVisible && + event.key === ' ' + ) { + keydownRef.current = true; + } + + if (event.target === event.currentTarget && isNonNativeButton() && event.key === ' ') { + event.preventDefault(); + } + + if (onKeyDown) { + onKeyDown(event); + } + + // Keyboard accessibility for non interactive elements + if ( + event.target === event.currentTarget && + isNonNativeButton() && + event.key === 'Enter' && + !disabled + ) { + event.preventDefault(); + if (onClick) { + onClick(event as unknown as React.MouseEvent); // HACK :/ + } + } + }); + + const handleKeyUp = useEventCallback((event: React.KeyboardEvent) => { + // calling preventDefault in keyUp on a