diff --git a/packages-internal/test-utils/package.json b/packages-internal/test-utils/package.json index 1f2baa4388a1bb..3aafd98690618f 100644 --- a/packages-internal/test-utils/package.json +++ b/packages-internal/test-utils/package.json @@ -39,6 +39,7 @@ "@emotion/react": "^11.11.4", "@testing-library/dom": "^10.3.1", "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", "chai": "^4.4.1", "chai-dom": "^1.12.0", "dom-accessibility-api": "^0.6.3", diff --git a/packages-internal/test-utils/src/createRenderer.tsx b/packages-internal/test-utils/src/createRenderer.tsx index 42e0f5c02d5099..db91bba4dc3266 100644 --- a/packages-internal/test-utils/src/createRenderer.tsx +++ b/packages-internal/test-utils/src/createRenderer.tsx @@ -15,6 +15,7 @@ import { within, RenderResult, } from '@testing-library/react/pure'; +import { userEvent } from '@testing-library/user-event'; import { useFakeTimers } from 'sinon'; interface Interaction { @@ -268,6 +269,7 @@ interface ServerRenderConfiguration extends RenderConfiguration { export type RenderOptions = Partial; export interface MuiRenderResult extends RenderResult { + user: ReturnType; forceUpdate(): void; /** * convenience helper. Better than repeating all props. @@ -296,6 +298,7 @@ function render( ); const result: MuiRenderResult = { ...testingLibraryRenderResult, + user: userEvent.setup(), forceUpdate() { traceSync('forceUpdate', () => testingLibraryRenderResult.rerender( diff --git a/packages-internal/test-utils/src/focusVisible.ts b/packages-internal/test-utils/src/focusVisible.ts index 8b650bc01ee0ae..ebc2f9cac1a07d 100644 --- a/packages-internal/test-utils/src/focusVisible.ts +++ b/packages-internal/test-utils/src/focusVisible.ts @@ -14,6 +14,10 @@ export function simulatePointerDevice() { fireEvent.pointerDown(document.body); } +export function simulateKeyboardDevice() { + fireEvent.keyDown(document.body, { key: 'TAB' }); +} + /** * See https://bugs.chromium.org/p/chromium/issues/detail?id=1127875 for more details. */ diff --git a/packages-internal/test-utils/src/index.ts b/packages-internal/test-utils/src/index.ts index e80fc539833170..ab14ee78f83aa0 100644 --- a/packages-internal/test-utils/src/index.ts +++ b/packages-internal/test-utils/src/index.ts @@ -8,6 +8,7 @@ export * from './createRenderer'; export { default as focusVisible, simulatePointerDevice, + simulateKeyboardDevice, programmaticFocusTriggersFocusVisible, } from './focusVisible'; export {} from './initMatchers'; diff --git a/packages/mui-joy/src/MenuItem/MenuItem.test.tsx b/packages/mui-joy/src/MenuItem/MenuItem.test.tsx index 45e9dd3b4cf5b4..a81d2248c07f35 100644 --- a/packages/mui-joy/src/MenuItem/MenuItem.test.tsx +++ b/packages/mui-joy/src/MenuItem/MenuItem.test.tsx @@ -92,18 +92,18 @@ describe('Joy ', () => { ]; events.forEach((eventName) => { - it(`should fire ${eventName}`, () => { + it(`should fire ${eventName}`, async () => { const handlerName = `on${eventName[0].toUpperCase()}${eventName.slice(1)}`; const handler = spy(); render(); - fireEvent[eventName](screen.getByRole('menuitem')); + await act(async () => fireEvent[eventName](screen.getByRole('menuitem'))); expect(handler.callCount).to.equal(1); }); }); - it(`should fire focus, keydown, keyup and blur`, () => { + it(`should fire focus, keydown, keyup and blur`, async () => { const handleFocus = spy(); const handleKeyDown = spy(); const handleKeyUp = spy(); @@ -119,17 +119,17 @@ describe('Joy ', () => { ); const menuitem = screen.getByRole('menuitem'); - act(() => { + await act(async () => { menuitem.focus(); }); expect(handleFocus.callCount).to.equal(1); - fireEvent.keyDown(menuitem); + await act(async () => fireEvent.keyDown(menuitem)); expect(handleKeyDown.callCount).to.equal(1); - fireEvent.keyUp(menuitem); + await act(async () => fireEvent.keyUp(menuitem)); expect(handleKeyUp.callCount).to.equal(1); diff --git a/packages/mui-material/src/Button/Button.test.js b/packages/mui-material/src/Button/Button.test.js index 47fa7b0607c42e..5d57fa23d002a0 100644 --- a/packages/mui-material/src/Button/Button.test.js +++ b/packages/mui-material/src/Button/Button.test.js @@ -1,11 +1,12 @@ import * as React from 'react'; import { expect } from 'chai'; -import { act, createRenderer, fireEvent, screen } from '@mui/internal-test-utils'; +import { createRenderer, screen, simulateKeyboardDevice } from '@mui/internal-test-utils'; import { ClassNames } from '@emotion/react'; import { ThemeProvider, createTheme } from '@mui/material/styles'; import Button, { buttonClasses as classes } from '@mui/material/Button'; import ButtonBase, { touchRippleClasses } from '@mui/material/ButtonBase'; import describeConformance from '../../test/describeConformance'; +import * as ripple from '../../test/ripple'; describe(', ); const button = getByRole('button'); - + await ripple.startTouch(button); expect(button.querySelector('.touch-ripple')).not.to.equal(null); }); - it('can disable the ripple', () => { + it('can disable the ripple', async () => { const { getByRole } = render( , ); const button = getByRole('button'); - + await ripple.startTouch(button); expect(button.querySelector('.touch-ripple')).to.equal(null); }); @@ -582,7 +583,7 @@ describe(' , ); + await ripple.startTouch(container.querySelector('button')); expect(container.querySelectorAll(`.${touchRippleClasses.root}`)).to.have.length(1); }); diff --git a/packages/mui-material/src/ButtonBase/ButtonBase.js b/packages/mui-material/src/ButtonBase/ButtonBase.js index 2c0b5b804cbd53..bff7de56039ff2 100644 --- a/packages/mui-material/src/ButtonBase/ButtonBase.js +++ b/packages/mui-material/src/ButtonBase/ButtonBase.js @@ -10,6 +10,7 @@ import { styled } from '../zero-styled'; import { useDefaultProps } from '../DefaultPropsProvider'; import useForkRef from '../utils/useForkRef'; import useEventCallback from '../utils/useEventCallback'; +import useLazyRipple from '../useLazyRipple'; import TouchRipple from './TouchRipple'; import buttonBaseClasses, { getButtonBaseUtilityClass } from './buttonBaseClasses'; @@ -109,8 +110,8 @@ const ButtonBase = React.forwardRef(function ButtonBase(inProps, ref) { const buttonRef = React.useRef(null); - const rippleRef = React.useRef(null); - const handleRippleRef = useForkRef(rippleRef, touchRippleRef); + const ripple = useLazyRipple(); + const handleRippleRef = useForkRef(ripple.ref, touchRippleRef); const [focusVisible, setFocusVisible] = React.useState(false); if (disabled && focusVisible) { @@ -128,19 +129,13 @@ const ButtonBase = React.forwardRef(function ButtonBase(inProps, ref) { [], ); - const [mountedState, setMountedState] = React.useState(false); + const enableTouchRipple = ripple.shouldMount && !disableRipple && !disabled; React.useEffect(() => { - setMountedState(true); - }, []); - - const enableTouchRipple = mountedState && !disableRipple && !disabled; - - React.useEffect(() => { - if (focusVisible && focusRipple && !disableRipple && mountedState) { - rippleRef.current.pulsate(); + if (focusVisible && focusRipple && !disableRipple) { + ripple.pulsate(); } - }, [disableRipple, focusRipple, focusVisible, mountedState]); + }, [disableRipple, focusRipple, focusVisible, ripple]); function useRippleHandler(rippleAction, eventCallback, skipRippleAction = disableTouchRipple) { return useEventCallback((event) => { @@ -149,8 +144,8 @@ const ButtonBase = React.forwardRef(function ButtonBase(inProps, ref) { } const ignore = skipRippleAction; - if (!ignore && rippleRef.current) { - rippleRef.current[rippleAction](event); + if (!ignore) { + ripple[rippleAction](event); } return true; @@ -212,9 +207,9 @@ const ButtonBase = React.forwardRef(function ButtonBase(inProps, ref) { const handleKeyDown = useEventCallback((event) => { // Check if key is already down to avoid repeats being counted as multiple activations - if (focusRipple && !event.repeat && focusVisible && rippleRef.current && event.key === ' ') { - rippleRef.current.stop(event, () => { - rippleRef.current.start(event); + if (focusRipple && !event.repeat && focusVisible && event.key === ' ') { + ripple.stop(event, () => { + ripple.start(event); }); } @@ -243,15 +238,9 @@ const ButtonBase = React.forwardRef(function ButtonBase(inProps, ref) { const handleKeyUp = useEventCallback((event) => { // calling preventDefault in keyUp on a - )); - - // cant match the error message here because flakiness with mocha watchmode - - expect(() => { - render(); - }).toErrorDev('Please make sure the children prop is rendered in this custom component.'); - }); }); describe('prop: type', () => { @@ -1283,9 +1279,10 @@ describe('', () => { }); describe('prop: touchRippleRef', () => { - it('should return a ref', () => { + it('should return a ref', async () => { const ref = React.createRef(); render(); + await ripple.startTouch(screen.getByRole('button')); expect(ref.current).not.to.equal(null); }); }); diff --git a/packages/mui-material/src/ButtonGroup/ButtonGroup.test.js b/packages/mui-material/src/ButtonGroup/ButtonGroup.test.js index ec308440ad9385..efa1bc6a941437 100644 --- a/packages/mui-material/src/ButtonGroup/ButtonGroup.test.js +++ b/packages/mui-material/src/ButtonGroup/ButtonGroup.test.js @@ -6,6 +6,7 @@ import { ThemeProvider, createTheme } from '@mui/material/styles'; import Button, { buttonClasses } from '@mui/material/Button'; import ButtonGroupContext from './ButtonGroupContext'; import describeConformance from '../../test/describeConformance'; +import * as ripple from '../../test/ripple'; describe('', () => { const { render } = createRenderer(); @@ -122,12 +123,13 @@ describe('', () => { expect(button).to.have.class('MuiButton-outlinedSizeLarge'); }); - it('should have a ripple by default', () => { - const { container } = render( + it('should have a ripple', async () => { + const { container, getByRole } = render( , ); + await ripple.startTouch(getByRole('button')); expect(container.querySelector('.touchRipple')).not.to.equal(null); }); @@ -141,12 +143,13 @@ describe('', () => { expect(button).to.have.class('MuiButton-disableElevation'); }); - it('can disable the ripple', () => { - const { container } = render( + it('can disable the ripple', async () => { + const { container, getByRole } = render( , ); + await ripple.startTouch(getByRole('button')); expect(container.querySelector('.touchRipple')).to.equal(null); }); diff --git a/packages/mui-material/src/Chip/Chip.test.js b/packages/mui-material/src/Chip/Chip.test.js index b853a45baa925b..e5d790ea24f0ca 100644 --- a/packages/mui-material/src/Chip/Chip.test.js +++ b/packages/mui-material/src/Chip/Chip.test.js @@ -85,7 +85,6 @@ describe('', () => { expect(container.firstChild).to.have.class('MuiButtonBase-root'); expect(container.firstChild).to.have.tagName('a'); - expect(container.firstChild.querySelector('.MuiTouchRipple-root')).not.to.equal(null); }); it('should disable ripple when MuiButtonBase has disableRipple in theme', () => { diff --git a/packages/mui-material/src/Fab/Fab.test.js b/packages/mui-material/src/Fab/Fab.test.js index fd9cc8c98ae696..4a55e6619cda40 100644 --- a/packages/mui-material/src/Fab/Fab.test.js +++ b/packages/mui-material/src/Fab/Fab.test.js @@ -1,10 +1,11 @@ import * as React from 'react'; import { expect } from 'chai'; -import { createRenderer, act, fireEvent } from '@mui/internal-test-utils'; +import { createRenderer } from '@mui/internal-test-utils'; import Fab, { fabClasses as classes } from '@mui/material/Fab'; import ButtonBase, { touchRippleClasses } from '@mui/material/ButtonBase'; import Icon from '@mui/material/Icon'; import describeConformance from '../../test/describeConformance'; +import * as ripple from '../../test/ripple'; describe('', () => { const { render, renderToString } = createRenderer(); @@ -90,19 +91,19 @@ describe('', () => { expect(button).to.have.class(classes.sizeMedium); }); - it('should have a ripple by default', () => { - const { container } = render(Fab); - + it('should have a ripple', async () => { + const { container, getByRole } = render(Fab); + await ripple.startTouch(getByRole('button')); expect(container.querySelector(`.${touchRippleClasses.root}`)).not.to.equal(null); }); - it('should pass disableRipple to ButtonBase', () => { - const { container } = render(Fab); - + it('should pass disableRipple to ButtonBase', async () => { + const { container, getByRole } = render(Fab); + await ripple.startTouch(getByRole('button')); expect(container.querySelector(`.${touchRippleClasses.root}`)).to.equal(null); }); - it('should have a focusRipple by default', async function test() { + it('should have a focusRipple', async function test() { if (/jsdom/.test(window.navigator.userAgent)) { // JSDOM doesn't support :focus-visible this.skip(); @@ -119,10 +120,7 @@ describe('', () => { ); const button = getByRole('button'); - act(() => { - fireEvent.keyDown(document.body, { key: 'TAB' }); - button.focus(); - }); + await ripple.startFocus(button); expect(button.querySelector('.pulsate-focus-visible')).not.to.equal(null); }); @@ -140,10 +138,7 @@ describe('', () => { ); const button = getByRole('button'); - act(() => { - fireEvent.keyDown(document.body, { key: 'TAB' }); - button.focus(); - }); + await ripple.startFocus(button); expect(button.querySelector('.pulsate-focus-visible')).to.equal(null); }); diff --git a/packages/mui-material/src/IconButton/IconButton.test.js b/packages/mui-material/src/IconButton/IconButton.test.js index 317e11f48b7394..3fad1d8384fbb5 100644 --- a/packages/mui-material/src/IconButton/IconButton.test.js +++ b/packages/mui-material/src/IconButton/IconButton.test.js @@ -8,6 +8,7 @@ import IconButton, { iconButtonClasses as classes } from '@mui/material/IconButt import Icon from '@mui/material/Icon'; import ButtonBase from '@mui/material/ButtonBase'; import describeConformance from '../../test/describeConformance'; +import * as ripple from '../../test/ripple'; describe('', () => { const { render } = createRenderer(); @@ -30,19 +31,21 @@ describe('', () => { expect(getByTestId('icon')).to.have.class(childClassName); }); - it('should have a ripple by default', () => { - const { container } = render( + it('should have a ripple', async () => { + const { container, getByRole } = render( book, ); + await ripple.startTouch(getByRole('button')); expect(container.querySelector('.touch-ripple')).not.to.equal(null); }); - it('can disable the ripple and hover effect', () => { - const { container } = render( + it('can disable the ripple and hover effect', async () => { + const { container, getByRole } = render( book , ); + await ripple.startTouch(getByRole('button')); expect(container.querySelector('.touch-ripple')).to.equal(null); }); diff --git a/packages/mui-material/src/MenuItem/MenuItem.test.js b/packages/mui-material/src/MenuItem/MenuItem.test.js index 6c8513da6fc83b..234fc6db0c36e8 100644 --- a/packages/mui-material/src/MenuItem/MenuItem.test.js +++ b/packages/mui-material/src/MenuItem/MenuItem.test.js @@ -6,6 +6,7 @@ import MenuItem, { menuItemClasses as classes } from '@mui/material/MenuItem'; import ButtonBase from '@mui/material/ButtonBase'; import ListContext from '../List/ListContext'; import describeConformance from '../../test/describeConformance'; +import * as ripple from '../../test/ripple'; describe('', () => { const { render } = createRenderer(); @@ -28,12 +29,11 @@ describe('', () => { expect(menuitem).to.have.property('tabIndex', -1); }); - it('has a ripple when clicked', () => { + it('has a ripple when clicked', async () => { render(); const menuitem = screen.getByRole('menuitem'); - // ripple starts on mousedown - fireEvent.mouseDown(menuitem); + await ripple.startTouch(menuitem); expect(menuitem.querySelectorAll('.ripple-visible')).to.have.length(1); }); @@ -59,18 +59,18 @@ describe('', () => { const events = ['click', 'mouseDown', 'mouseEnter', 'mouseLeave', 'mouseUp', 'touchEnd']; events.forEach((eventName) => { - it(`should fire ${eventName}`, () => { + it(`should fire ${eventName}`, async () => { const handlerName = `on${eventName[0].toUpperCase()}${eventName.slice(1)}`; const handler = spy(); render(); - fireEvent[eventName](screen.getByRole('menuitem')); + await act(async () => fireEvent[eventName](screen.getByRole('menuitem'))); expect(handler.callCount).to.equal(1); }); }); - it(`should fire focus, keydown, keyup and blur`, () => { + it(`should fire focus, keydown, keyup and blur`, async () => { const handleFocus = spy(); const handleKeyDown = spy(); const handleKeyUp = spy(); @@ -85,21 +85,21 @@ describe('', () => { ); const menuitem = screen.getByRole('menuitem'); - act(() => { + await act(async () => { menuitem.focus(); }); expect(handleFocus.callCount).to.equal(1); - fireEvent.keyDown(menuitem); + await act(async () => fireEvent.keyDown(menuitem)); expect(handleKeyDown.callCount).to.equal(1); - fireEvent.keyUp(menuitem); + await act(async () => fireEvent.keyUp(menuitem)); expect(handleKeyUp.callCount).to.equal(1); - menuitem.blur(); + await act(async () => menuitem.blur()); expect(handleKeyDown.callCount).to.equal(1); }); diff --git a/packages/mui-material/src/RadioGroup/RadioGroup.test.js b/packages/mui-material/src/RadioGroup/RadioGroup.test.js index fb7933fffbdda9..e9742f155920f3 100644 --- a/packages/mui-material/src/RadioGroup/RadioGroup.test.js +++ b/packages/mui-material/src/RadioGroup/RadioGroup.test.js @@ -120,7 +120,7 @@ describe('', () => { }); describe('imperative focus()', () => { - it('should focus the first non-disabled radio', () => { + it('should focus the first non-disabled radio', async () => { const actionsRef = React.createRef(); const oneRadioOnFocus = spy(); @@ -132,7 +132,7 @@ describe('', () => { , ); - act(() => { + await act(async () => { actionsRef.current.focus(); }); @@ -162,7 +162,7 @@ describe('', () => { expect(twoRadioOnFocus.callCount).to.equal(0); }); - it('should focus the selected radio', () => { + it('should focus the selected radio', async () => { const actionsRef = React.createRef(); const twoRadioOnFocus = spy(); @@ -175,14 +175,14 @@ describe('', () => { , ); - act(() => { + await act(async () => { actionsRef.current.focus(); }); expect(twoRadioOnFocus.callCount).to.equal(1); }); - it('should focus the non-disabled radio rather than the disabled selected radio', () => { + it('should focus the non-disabled radio rather than the disabled selected radio', async () => { const actionsRef = React.createRef(); const threeRadioOnFocus = spy(); @@ -195,7 +195,7 @@ describe('', () => { , ); - act(() => { + await act(async () => { actionsRef.current.focus(); }); diff --git a/packages/mui-material/src/Select/Select.test.js b/packages/mui-material/src/Select/Select.test.js index 4f092e30f3bb84..86ffb06d56bd7c 100644 --- a/packages/mui-material/src/Select/Select.test.js +++ b/packages/mui-material/src/Select/Select.test.js @@ -95,7 +95,7 @@ describe('', () => { ); const trigger = getByRole('combobox'); - fireEvent.mouseDown(trigger); + await act(async () => fireEvent.mouseDown(trigger)); expect(handleBlur.callCount).to.equal(0); expect(getByRole('listbox')).not.to.equal(null); - act(() => { + await act(async () => { const options = getAllByRole('option'); fireEvent.mouseDown(options[0]); options[0].click(); diff --git a/packages/mui-material/src/SpeedDial/SpeedDial.test.js b/packages/mui-material/src/SpeedDial/SpeedDial.test.js index 4dbf84652a87e2..837a3381d3933b 100644 --- a/packages/mui-material/src/SpeedDial/SpeedDial.test.js +++ b/packages/mui-material/src/SpeedDial/SpeedDial.test.js @@ -115,7 +115,7 @@ describe('', () => { }); describe('prop: onKeyDown', () => { - it('should be called when a key is pressed', () => { + it('should be called when a key is pressed', async () => { const handleKeyDown = spy(); const { getByRole } = render( @@ -123,11 +123,11 @@ describe('', () => { , ); const buttonWrapper = getByRole('button', { expanded: true }); - act(() => { + await act(async () => { fireEvent.keyDown(document.body, { key: 'TAB' }); buttonWrapper.focus(); + fireEvent.keyDown(buttonWrapper, { key: ' ' }); }); - fireEvent.keyDown(buttonWrapper, { key: ' ' }); expect(handleKeyDown.callCount).to.equal(1); expect(handleKeyDown.args[0][0]).to.have.property('key', ' '); }); @@ -173,7 +173,7 @@ describe('', () => { }); describe('keyboard', () => { - it('should open the speed dial and move to the first action without closing', () => { + it('should open the speed dial and move to the first action without closing', async () => { const handleOpen = spy(); const { getByRole, getAllByRole } = render( @@ -182,7 +182,7 @@ describe('', () => { , ); const fab = getByRole('button'); - act(() => { + await act(async () => { fab.focus(); }); clock.tick(); @@ -190,12 +190,12 @@ describe('', () => { expect(handleOpen.callCount).to.equal(1); const actions = getAllByRole('menuitem'); expect(actions.length).to.equal(2); - fireEvent.keyDown(fab, { key: 'ArrowUp' }); + await act(async () => fireEvent.keyDown(fab, { key: 'ArrowUp' })); expect(document.activeElement).to.equal(actions[0]); expect(fab).to.have.attribute('aria-expanded', 'true'); }); - it('should reset the state of the tooltip when the speed dial is closed while it is open', function test() { + it('should reset the state of the tooltip when the speed dial is closed while it is open', async function test() { if (/jsdom/.test(window.navigator.userAgent)) { // JSDOM doesn't support :focus-visible this.skip(); @@ -211,18 +211,18 @@ describe('', () => { const fab = getByRole('button'); const actions = getAllByRole('menuitem'); - act(() => { + await act(async () => { fab.focus(); }); clock.runAll(); expect(fab).to.have.attribute('aria-expanded', 'true'); - fireEvent.keyDown(fab, { key: 'ArrowUp' }); + await act(async () => fireEvent.keyDown(fab, { key: 'ArrowUp' })); clock.runAll(); expect(queryByRole('tooltip')).not.to.equal(null); - fireDiscreteEvent.keyDown(actions[0], { key: 'Escape' }); + await act(async () => fireDiscreteEvent.keyDown(actions[0], { key: 'Escape' })); clock.runAll(); expect(queryByRole('tooltip')).to.equal(null); @@ -250,7 +250,7 @@ describe('', () => { return children; } - const renderSpeedDial = (direction = 'up', actionCount = 4) => { + const renderSpeedDial = async (direction = 'up', actionCount = 4) => { actionButtons = []; fabButton = undefined; @@ -280,7 +280,7 @@ describe('', () => { ))} , ); - act(() => { + await act(async () => { fabButton.focus(); }); }; @@ -304,21 +304,21 @@ describe('', () => { return expectedFocusedElement === document.activeElement; }; - it('displays the actions on focus gain', () => { - renderSpeedDial(); + it('displays the actions on focus gain', async () => { + await renderSpeedDial(); expect(screen.getAllByRole('menuitem')).to.have.lengthOf(4); expect(fabButton).to.have.attribute('aria-expanded', 'true'); }); - it('considers arrow keys with the same initial orientation', () => { - renderSpeedDial(); - fireEvent.keyDown(fabButton, { key: 'left' }); + it('considers arrow keys with the same initial orientation', async () => { + await renderSpeedDial(); + await act(async () => fireEvent.keyDown(fabButton, { key: 'left' })); expect(isActionFocused(0)).to.equal(true); - fireEvent.keyDown(getActionButton(0), { key: 'up' }); + await act(async () => fireEvent.keyDown(getActionButton(0), { key: 'up' })); expect(isActionFocused(0)).to.equal(true); - fireEvent.keyDown(getActionButton(0), { key: 'left' }); + await act(async () => fireEvent.keyDown(getActionButton(0), { key: 'left' })); expect(isActionFocused(1)).to.equal(true); - fireEvent.keyDown(getActionButton(1), { key: 'right' }); + await act(async () => fireEvent.keyDown(getActionButton(1), { key: 'right' })); expect(isActionFocused(0)).to.equal(true); }); @@ -327,33 +327,37 @@ describe('', () => { * tests a combination of arrow keys on a focused SpeedDial */ const itTestCombination = (dialDirection, keys, expected) => { - it(`start dir ${dialDirection} with keys ${keys.join(',')}`, () => { + it(`start dir ${dialDirection} with keys ${keys.join(',')}`, async () => { const [firstKey, ...combination] = keys; const [firstFocusedAction, ...foci] = expected; - renderSpeedDial(dialDirection); + await renderSpeedDial(dialDirection); - fireEvent.keyDown(fabButton, { key: firstKey }); + await act(async () => fireEvent.keyDown(fabButton, { key: firstKey })); expect(isActionFocused(firstFocusedAction)).to.equal( true, `focused action initial ${firstKey} should be ${firstFocusedAction}`, ); - combination.forEach((arrowKey, i) => { + for (let i = 0; i < combination.length; i += 1) { + const arrowKey = combination[i]; const previousFocusedAction = foci[i - 1] || firstFocusedAction; const expectedFocusedAction = foci[i]; const combinationUntilNot = [firstKey, ...combination.slice(0, i + 1)]; - fireEvent.keyDown(getActionButton(previousFocusedAction), { - key: arrowKey, - }); + // eslint-disable-next-line no-await-in-loop + await act(async () => + fireEvent.keyDown(getActionButton(previousFocusedAction), { + key: arrowKey, + }), + ); expect(isActionFocused(expectedFocusedAction)).to.equal( true, `focused action after ${combinationUntilNot.join( ',', )} should be ${expectedFocusedAction}`, ); - }); + } }); }; diff --git a/packages/mui-material/src/Tab/Tab.test.js b/packages/mui-material/src/Tab/Tab.test.js index 45ecdfb89d5322..da0ebf3b394ab5 100644 --- a/packages/mui-material/src/Tab/Tab.test.js +++ b/packages/mui-material/src/Tab/Tab.test.js @@ -1,11 +1,12 @@ import { expect } from 'chai'; import * as React from 'react'; import { spy } from 'sinon'; -import { act, createRenderer, fireEvent } from '@mui/internal-test-utils'; +import { createRenderer, simulatePointerDevice } from '@mui/internal-test-utils'; import Tab, { tabClasses as classes } from '@mui/material/Tab'; import ButtonBase from '@mui/material/ButtonBase'; import { ThemeProvider, createTheme } from '@mui/material/styles'; import describeConformance from '../../test/describeConformance'; +import * as ripple from '../../test/ripple'; describe('', () => { const { render } = createRenderer(); @@ -20,53 +21,44 @@ describe('', () => { skip: ['componentProp', 'componentsProp'], })); - it('should have a ripple by default', () => { + it('should have a ripple', async () => { const { container } = render(); - + await ripple.startTouch(container.querySelector('button')); expect(container.querySelector('.touch-ripple')).not.to.equal(null); }); - it('can disable the ripple', () => { + it('can disable the ripple', async () => { const { container } = render( , ); + await ripple.startTouch(container.querySelector('button')); expect(container.querySelector('.touch-ripple')).to.equal(null); }); - it('should have a focusRipple by default', function test() { + it('should have a focusRipple', async function test() { if (/jsdom/.test(window.navigator.userAgent)) { // JSDOM doesn't support :focus-visible this.skip(); } - const { container, getByRole } = render( + const { container } = render( , ); - // simulate pointer device - fireEvent.pointerDown(document.body); + simulatePointerDevice(); - act(() => { - fireEvent.keyDown(document.body, { key: 'Tab' }); - // jsdom doesn't actually support tab focus, we need to do it manually - getByRole('tab').focus(); - }); + await ripple.startFocus(container.querySelector('button')); expect(container.querySelector('.focus-ripple')).not.to.equal(null); }); - it('can disable the focusRipple', () => { - const { container, getByRole } = render( + it('can disable the focusRipple', async () => { + const { container } = render( , ); - // simulate pointer device - fireEvent.pointerDown(document.body); + simulatePointerDevice(); - act(() => { - fireEvent.keyDown(document.body, { key: 'Tab' }); - // jsdom doesn't actually support tab focus, we need to do it manually - getByRole('tab').focus(); - }); + await ripple.startFocus(container.querySelector('button')); expect(container.querySelector('.focus-ripple')).to.equal(null); }); diff --git a/packages/mui-material/src/Tabs/Tabs.test.js b/packages/mui-material/src/Tabs/Tabs.test.js index de3be255ec46f7..42e141f71b79d5 100644 --- a/packages/mui-material/src/Tabs/Tabs.test.js +++ b/packages/mui-material/src/Tabs/Tabs.test.js @@ -459,7 +459,7 @@ describe('', () => { expect(handleChange.callCount).to.equal(0); }); - it('when `selectionFollowsFocus` should call if an unselected tab gets focused', () => { + it('when `selectionFollowsFocus` should call if an unselected tab gets focused', async () => { const handleChange = spy(); const { getAllByRole } = render( @@ -469,7 +469,7 @@ describe('', () => { ); const [, lastTab] = getAllByRole('tab'); - act(() => { + await act(async () => { lastTab.focus(); }); @@ -477,7 +477,7 @@ describe('', () => { expect(handleChange.firstCall.args[1]).to.equal(1); }); - it('when `selectionFollowsFocus` should not call if an selected tab gets focused', () => { + it('when `selectionFollowsFocus` should not call if an selected tab gets focused', async () => { const handleChange = spy(); const { getAllByRole } = render( @@ -487,7 +487,7 @@ describe('', () => { ); const [firstTab] = getAllByRole('tab'); - act(() => { + await act(async () => { firstTab.focus(); }); @@ -912,7 +912,7 @@ describe('', () => { describe(`when focus is on a tab element in a ${orientation} ${direction} tablist`, () => { describe(previousItemKey, () => { - it('moves focus to the last tab without activating it if focus is on the first tab', () => { + it('moves focus to the last tab without activating it if focus is on the first tab', async () => { const handleChange = spy(); const handleKeyDown = spy(); const { getAllByRole } = render( @@ -929,11 +929,11 @@ describe('', () => { { wrapper }, ); const [firstTab, , lastTab] = getAllByRole('tab'); - act(() => { + await act(async () => { firstTab.focus(); }); - fireEvent.keyDown(firstTab, { key: previousItemKey }); + await act(async () => fireEvent.keyDown(firstTab, { key: previousItemKey })); expect(lastTab).toHaveFocus(); expect(handleChange.callCount).to.equal(0); @@ -941,7 +941,7 @@ describe('', () => { expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); - it('when `selectionFollowsFocus` moves focus to the last tab while activating it if focus is on the first tab', () => { + it('when `selectionFollowsFocus` moves focus to the last tab while activating it if focus is on the first tab', async () => { const handleChange = spy(); const handleKeyDown = spy(); const { getAllByRole } = render( @@ -959,11 +959,11 @@ describe('', () => { { wrapper }, ); const [firstTab, , lastTab] = getAllByRole('tab'); - act(() => { + await act(async () => { firstTab.focus(); }); - fireEvent.keyDown(firstTab, { key: previousItemKey }); + await act(async () => fireEvent.keyDown(firstTab, { key: previousItemKey })); expect(lastTab).toHaveFocus(); expect(handleChange.callCount).to.equal(1); @@ -972,7 +972,7 @@ describe('', () => { expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); - it('moves focus to the previous tab without activating it', () => { + it('moves focus to the previous tab without activating it', async () => { const handleChange = spy(); const handleKeyDown = spy(); const { getAllByRole } = render( @@ -989,11 +989,11 @@ describe('', () => { { wrapper }, ); const [firstTab, secondTab] = getAllByRole('tab'); - act(() => { + await act(async () => { secondTab.focus(); }); - fireEvent.keyDown(secondTab, { key: previousItemKey }); + await act(async () => fireEvent.keyDown(secondTab, { key: previousItemKey })); expect(firstTab).toHaveFocus(); expect(handleChange.callCount).to.equal(0); @@ -1001,7 +1001,7 @@ describe('', () => { expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); - it('when `selectionFollowsFocus` moves focus to the previous tab while activating it', () => { + it('when `selectionFollowsFocus` moves focus to the previous tab while activating it', async () => { const handleChange = spy(); const handleKeyDown = spy(); const { getAllByRole } = render( @@ -1019,11 +1019,11 @@ describe('', () => { { wrapper }, ); const [firstTab, secondTab] = getAllByRole('tab'); - act(() => { + await act(async () => { secondTab.focus(); }); - fireEvent.keyDown(secondTab, { key: previousItemKey }); + await act(async () => fireEvent.keyDown(secondTab, { key: previousItemKey })); expect(firstTab).toHaveFocus(); expect(handleChange.callCount).to.equal(1); @@ -1032,7 +1032,7 @@ describe('', () => { expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); - it('skips over disabled tabs', () => { + it('skips over disabled tabs', async () => { const handleKeyDown = spy(); const { getAllByRole } = render( ', () => { { wrapper }, ); const [firstTab, , lastTab] = getAllByRole('tab'); - act(() => { + await act(async () => { lastTab.focus(); }); - fireEvent.keyDown(lastTab, { key: previousItemKey }); + await act(async () => fireEvent.keyDown(lastTab, { key: previousItemKey })); expect(firstTab).toHaveFocus(); expect(handleKeyDown.callCount).to.equal(1); @@ -1061,7 +1061,7 @@ describe('', () => { }); describe(nextItemKey, () => { - it('moves focus to the first tab without activating it if focus is on the last tab', () => { + it('moves focus to the first tab without activating it if focus is on the last tab', async () => { const handleChange = spy(); const handleKeyDown = spy(); const { getAllByRole } = render( @@ -1078,11 +1078,11 @@ describe('', () => { { wrapper }, ); const [firstTab, , lastTab] = getAllByRole('tab'); - act(() => { + await act(async () => { lastTab.focus(); }); - fireEvent.keyDown(lastTab, { key: nextItemKey }); + await act(async () => fireEvent.keyDown(lastTab, { key: nextItemKey })); expect(firstTab).toHaveFocus(); expect(handleChange.callCount).to.equal(0); @@ -1090,7 +1090,7 @@ describe('', () => { expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); - it('when `selectionFollowsFocus` moves focus to the first tab while activating it if focus is on the last tab', () => { + it('when `selectionFollowsFocus` moves focus to the first tab while activating it if focus is on the last tab', async () => { const handleChange = spy(); const handleKeyDown = spy(); const { getAllByRole } = render( @@ -1108,11 +1108,11 @@ describe('', () => { { wrapper }, ); const [firstTab, , lastTab] = getAllByRole('tab'); - act(() => { + await act(async () => { lastTab.focus(); }); - fireEvent.keyDown(lastTab, { key: nextItemKey }); + await act(async () => fireEvent.keyDown(lastTab, { key: nextItemKey })); expect(firstTab).toHaveFocus(); expect(handleChange.callCount).to.equal(1); @@ -1121,7 +1121,7 @@ describe('', () => { expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); - it('moves focus to the next tab without activating it it', () => { + it('moves focus to the next tab without activating it it', async () => { const handleChange = spy(); const handleKeyDown = spy(); const { getAllByRole } = render( @@ -1138,11 +1138,11 @@ describe('', () => { { wrapper }, ); const [, secondTab, lastTab] = getAllByRole('tab'); - act(() => { + await act(async () => { secondTab.focus(); }); - fireEvent.keyDown(secondTab, { key: nextItemKey }); + await act(async () => fireEvent.keyDown(secondTab, { key: nextItemKey })); expect(lastTab).toHaveFocus(); expect(handleChange.callCount).to.equal(0); @@ -1150,7 +1150,7 @@ describe('', () => { expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); - it('when `selectionFollowsFocus` moves focus to the next tab while activating it it', () => { + it('when `selectionFollowsFocus` moves focus to the next tab while activating it it', async () => { const handleChange = spy(); const handleKeyDown = spy(); const { getAllByRole } = render( @@ -1168,11 +1168,11 @@ describe('', () => { { wrapper }, ); const [, secondTab, lastTab] = getAllByRole('tab'); - act(() => { + await act(async () => { secondTab.focus(); }); - fireEvent.keyDown(secondTab, { key: nextItemKey }); + await act(async () => fireEvent.keyDown(secondTab, { key: nextItemKey })); expect(lastTab).toHaveFocus(); expect(handleChange.callCount).to.equal(1); @@ -1181,7 +1181,7 @@ describe('', () => { expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); - it('skips over disabled tabs', () => { + it('skips over disabled tabs', async () => { const handleKeyDown = spy(); const { getAllByRole } = render( ', () => { { wrapper }, ); const [firstTab, , lastTab] = getAllByRole('tab'); - act(() => { + await act(async () => { firstTab.focus(); }); - fireEvent.keyDown(firstTab, { key: nextItemKey }); + await act(async () => fireEvent.keyDown(firstTab, { key: nextItemKey })); expect(lastTab).toHaveFocus(); expect(handleKeyDown.callCount).to.equal(1); @@ -1213,7 +1213,7 @@ describe('', () => { describe('when focus is on a tab regardless of orientation', () => { describe('Home', () => { - it('moves focus to the first tab without activating it', () => { + it('moves focus to the first tab without activating it', async () => { const handleChange = spy(); const handleKeyDown = spy(); const { getAllByRole } = render( @@ -1224,11 +1224,11 @@ describe('', () => { , ); const [firstTab, , lastTab] = getAllByRole('tab'); - act(() => { + await act(async () => { lastTab.focus(); }); - fireEvent.keyDown(lastTab, { key: 'Home' }); + await act(async () => fireEvent.keyDown(lastTab, { key: 'Home' })); expect(firstTab).toHaveFocus(); expect(handleChange.callCount).to.equal(0); @@ -1236,7 +1236,7 @@ describe('', () => { expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); - it('when `selectionFollowsFocus` moves focus to the first tab without activating it', () => { + it('when `selectionFollowsFocus` moves focus to the first tab without activating it', async () => { const handleChange = spy(); const handleKeyDown = spy(); const { getAllByRole } = render( @@ -1247,11 +1247,11 @@ describe('', () => { , ); const [firstTab, , lastTab] = getAllByRole('tab'); - act(() => { + await act(async () => { lastTab.focus(); }); - fireEvent.keyDown(lastTab, { key: 'Home' }); + await act(async () => fireEvent.keyDown(lastTab, { key: 'Home' })); expect(firstTab).toHaveFocus(); expect(handleChange.callCount).to.equal(1); @@ -1260,7 +1260,7 @@ describe('', () => { expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); - it('moves focus to first non-disabled tab', () => { + it('moves focus to first non-disabled tab', async () => { const handleKeyDown = spy(); const { getAllByRole } = render( @@ -1270,11 +1270,11 @@ describe('', () => { , ); const [, secondTab, lastTab] = getAllByRole('tab'); - act(() => { + await act(async () => { lastTab.focus(); }); - fireEvent.keyDown(lastTab, { key: 'Home' }); + await act(async () => fireEvent.keyDown(lastTab, { key: 'Home' })); expect(secondTab).toHaveFocus(); expect(handleKeyDown.callCount).to.equal(1); @@ -1283,7 +1283,7 @@ describe('', () => { }); describe('End', () => { - it('moves focus to the last tab without activating it', () => { + it('moves focus to the last tab without activating it', async () => { const handleChange = spy(); const handleKeyDown = spy(); const { getAllByRole } = render( @@ -1294,11 +1294,11 @@ describe('', () => { , ); const [firstTab, , lastTab] = getAllByRole('tab'); - act(() => { + await act(async () => { firstTab.focus(); }); - fireEvent.keyDown(firstTab, { key: 'End' }); + await act(async () => fireEvent.keyDown(firstTab, { key: 'End' })); expect(lastTab).toHaveFocus(); expect(handleChange.callCount).to.equal(0); @@ -1306,7 +1306,7 @@ describe('', () => { expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); - it('when `selectionFollowsFocus` moves focus to the last tab without activating it', () => { + it('when `selectionFollowsFocus` moves focus to the last tab without activating it', async () => { const handleChange = spy(); const handleKeyDown = spy(); const { getAllByRole } = render( @@ -1317,11 +1317,11 @@ describe('', () => { , ); const [firstTab, , lastTab] = getAllByRole('tab'); - act(() => { + await act(async () => { firstTab.focus(); }); - fireEvent.keyDown(firstTab, { key: 'End' }); + await act(async () => fireEvent.keyDown(firstTab, { key: 'End' })); expect(lastTab).toHaveFocus(); expect(handleChange.callCount).to.equal(1); @@ -1330,7 +1330,7 @@ describe('', () => { expect(handleKeyDown.firstCall.args[0]).to.have.property('defaultPrevented', true); }); - it('moves focus to first non-disabled tab', () => { + it('moves focus to first non-disabled tab', async () => { const handleKeyDown = spy(); const { getAllByRole } = render( @@ -1340,11 +1340,11 @@ describe('', () => { , ); const [firstTab, secondTab] = getAllByRole('tab'); - act(() => { + await act(async () => { firstTab.focus(); }); - fireEvent.keyDown(firstTab, { key: 'End' }); + await act(async () => fireEvent.keyDown(firstTab, { key: 'End' })); expect(secondTab).toHaveFocus(); expect(handleKeyDown.callCount).to.equal(1); diff --git a/packages/mui-material/src/internal/SwitchBase.test.js b/packages/mui-material/src/internal/SwitchBase.test.js index fef9508d39395e..52432fc37037e3 100644 --- a/packages/mui-material/src/internal/SwitchBase.test.js +++ b/packages/mui-material/src/internal/SwitchBase.test.js @@ -7,6 +7,7 @@ import FormControl, { useFormControl } from '../FormControl'; import ButtonBase from '../ButtonBase'; import classes from './switchBaseClasses'; import describeConformance from '../../test/describeConformance'; +import * as ripple from '../../test/ripple'; describe('', () => { const { render } = createRenderer(); @@ -43,8 +44,8 @@ describe('', () => { expect(buttonInside.childNodes[1]).to.have.text('unchecked'); }); - it('should have a ripple by default', () => { - const { getByTestId } = render( + it('should have a ripple', async () => { + const { container, getByTestId } = render( ', () => { />, ); + await ripple.startTouch(container.querySelector('input')); + expect(getByTestId('TouchRipple')).not.to.equal(null); }); @@ -64,8 +67,8 @@ describe('', () => { expect(container.firstChild).to.have.class(classes.edgeStart); }); - it('can disable the ripple ', () => { - const { queryByTestId } = render( + it('can disable the ripple ', async () => { + const { container, queryByTestId } = render( ', () => { />, ); + await ripple.startTouch(container.querySelector('input')); + expect(queryByTestId('TouchRipple')).to.equal(null); }); diff --git a/packages/mui-material/src/useLazyRipple/index.ts b/packages/mui-material/src/useLazyRipple/index.ts new file mode 100644 index 00000000000000..10ca57cab4eec4 --- /dev/null +++ b/packages/mui-material/src/useLazyRipple/index.ts @@ -0,0 +1,2 @@ +'use client'; +export { default } from './useLazyRipple'; diff --git a/packages/mui-material/src/useLazyRipple/useLazyRipple.ts b/packages/mui-material/src/useLazyRipple/useLazyRipple.ts new file mode 100644 index 00000000000000..e0e2819e0c4fc5 --- /dev/null +++ b/packages/mui-material/src/useLazyRipple/useLazyRipple.ts @@ -0,0 +1,106 @@ +'use client'; +import * as React from 'react'; +import useLazyRef from '@mui/utils/useLazyRef'; +import { TouchRippleActions } from '../ButtonBase/TouchRipple'; + +type ControlledPromise = Promise & { + resolve: Function; + reject: Function; +}; + +/** + * Lazy initialization container for the Ripple instance. This improves + * performance by delaying mounting the ripple until it's needed. + */ +export class LazyRipple { + /** React ref to the ripple instance */ + ref: React.MutableRefObject; + + /** If the ripple component should be mounted */ + shouldMount: boolean; + + /** Promise that resolves when the ripple component is mounted */ + private mounted: ControlledPromise | null; + + /** If the ripple component has been mounted */ + private didMount: boolean; + + /** React state hook setter */ + private setShouldMount: React.Dispatch | null; + + static create() { + return new LazyRipple(); + } + + static use() { + /* eslint-disable */ + const ripple = useLazyRef(LazyRipple.create).current; + const [shouldMount, setShouldMount] = React.useState(false); + + ripple.shouldMount = shouldMount; + ripple.setShouldMount = setShouldMount; + + React.useEffect(ripple.mountEffect, [shouldMount]); + /* eslint-enable */ + + return ripple; + } + + constructor() { + this.ref = { current: null }; + this.mounted = null; + this.didMount = false; + this.shouldMount = false; + this.setShouldMount = null; + } + + mount() { + if (!this.mounted) { + this.mounted = createControlledPromise(); + this.shouldMount = true; + this.setShouldMount!(this.shouldMount); + } + return this.mounted; + } + + mountEffect = () => { + if (this.shouldMount && !this.didMount) { + if (this.ref.current !== null) { + this.didMount = true; + this.mounted!.resolve(); + } + } + }; + + /* Ripple API */ + + start(...args: Parameters) { + this.mount().then(() => this.ref.current?.start(...args)); + } + + stop(...args: Parameters) { + this.mount().then(() => this.ref.current?.stop(...args)); + } + + pulsate(...args: Parameters) { + this.mount().then(() => this.ref.current?.pulsate(...args)); + } +} + +export default function useLazyRipple() { + return LazyRipple.use(); +} + +function createControlledPromise(): ControlledPromise { + let resolve: Function; + let reject: Function; + + const p = new Promise((resolveFn, rejectFn) => { + resolve = resolveFn; + reject = rejectFn; + }) as ControlledPromise; + p.resolve = resolve!; + p.reject = reject!; + + return p; +} diff --git a/packages/mui-material/src/useTouchRipple/index.ts b/packages/mui-material/src/useTouchRipple/index.ts deleted file mode 100644 index 28dfd75c5e9136..00000000000000 --- a/packages/mui-material/src/useTouchRipple/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -'use client'; -export { default } from './useTouchRipple'; diff --git a/packages/mui-material/src/useTouchRipple/useTouchRipple.ts b/packages/mui-material/src/useTouchRipple/useTouchRipple.ts deleted file mode 100644 index 8773c5d227a5c2..00000000000000 --- a/packages/mui-material/src/useTouchRipple/useTouchRipple.ts +++ /dev/null @@ -1,159 +0,0 @@ -'use client'; -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, - skipRippleAction = disableTouchRipple, - ) { - return useEventCallback((event: React.SyntheticEvent) => { - 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