For a Material-UI Button component, I would like to have the "focus" styling look the same as "focusVisible" styling. Meaning I want it to have the same ripple effect visible if the button was focused programatically or with the mouse as if the button was focused with the tab key.
A sort-of workaround I have found is to call dispatchEvent(new window.Event("keydown"))
on the element before it is focused, causing keyboard to be the last input type used. This will have the effect of making the button look the way I want UNTIL the onMouseLeave event (from MUI <ButtonBase/>) or another mouse event is fired, causing the visible focus to disappear.
I have figured out how to change the focus styling of the component like this:
import React from "react"
import { withStyles } from "@material-ui/core/styles"
import Button from "@material-ui/core/Button"
const styles = {
root: {
'&:focus': {
border: "3px solid #000000"
}
}
}
const CustomButtonRaw = React.forwardRef((props, ref) => {
const { classes, ...rest } = props
return <Button classes={{root: classes.root}} {...rest} ref={ref}/>
}
const CustomButton = withStyles(styles, { name: "CustomButton" })(CustomButtonRaw)
export default CustomButton
So, I can apply some style to the button when it is in "focus" state. (For ex. I applied a border). But I am missing how to get the styles to apply. I have tried putting the className 'Mui-visibleFocus' on the Button but that did not seem to have an effect. Is there some way to get the styles that would be applied if the Button was in visibleFocus state?
ButtonBase
(which Button
delegates to) has an action prop which provides the ability to set the button's focus-visible state.
ButtonBase
leverages the useImperativeHandle hook for this. To leverage it, you pass a ref into the action
prop and then you can later call actionRef.current.focusVisible()
.
However, this by itself is not sufficient, because there are several mouse and touch events that ButtonBase listens to in order to start/stop the ripple. If you use the disableTouchRipple
prop, it prevents ButtonBase from trying to start/stop the ripple based on those events.
Unfortunately disableTouchRipple
prevents click and touch animations on the button. These can be restored by adding another TouchRipple
element explicitly that you control. My example below shows handling onMouseDown
and onMouseUp
as a proof-of-concept, but an ideal solution would deal with all the different events that ButtonBase
handles.
Here's a working example:
import React from "react";
import Button from "@material-ui/core/Button";
import TouchRipple from "@material-ui/core/ButtonBase/TouchRipple";
const FocusRippleButton = React.forwardRef(function FocusRippleButton(
{ onFocus, onMouseDown, onMouseUp, children, ...other },
ref
) {
const actionRef = React.useRef();
const rippleRef = React.useRef();
const handleFocus = (event) => {
actionRef.current.focusVisible();
if (onFocus) {
onFocus(event);
}
};
const handleMouseUp = (event) => {
rippleRef.current.stop(event);
if (onMouseUp) {
onMouseUp(event);
}
};
const handleMouseDown = (event) => {
rippleRef.current.start(event);
if (onMouseDown) {
onMouseDown(event);
}
};
return (
<Button
ref={ref}
action={actionRef}
disableTouchRipple
onFocus={handleFocus}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
{...other}
>
{children}
<TouchRipple ref={rippleRef} />
</Button>
);
});
export default function App() {
return (
<div className="App">
<FocusRippleButton variant="contained" color="primary">
Button 1
</FocusRippleButton>
<br />
<br />
<FocusRippleButton
variant="contained"
color="primary"
onFocus={() => console.log("Some extra onFocus functionality")}
>
Button 2
</FocusRippleButton>
</div>
);
}