Chore: Rewrite ConfirmButton (#84402)

* convert to function component, clean up + fix a11y

* use position: fixed;

* fix
This commit is contained in:
Ashley Harrison 2024-03-14 15:50:44 +00:00 committed by GitHub
parent 81a63efab2
commit 321148511b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 109 additions and 133 deletions

View File

@ -1,14 +1,13 @@
import { cx, css } from '@emotion/css';
import React, { PureComponent, ReactElement } from 'react';
import React, { ReactElement, useEffect, useRef, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { stylesFactory, withTheme2 } from '../../themes';
import { Themeable2 } from '../../types';
import { useStyles2 } from '../../themes';
import { ComponentSize } from '../../types/size';
import { Button, ButtonVariant } from '../Button';
export interface Props extends Themeable2 {
export interface Props {
/** Confirm action callback */
onConfirm(): void;
children: string | ReactElement;
@ -24,182 +23,161 @@ export interface Props extends Themeable2 {
confirmVariant?: ButtonVariant;
/** Hide confirm actions when after of them is clicked */
closeOnConfirm?: boolean;
/** Move focus to button when mounted */
autoFocus?: boolean;
/** Optional on click handler for the original button */
onClick?(): void;
/** Callback for the cancel action */
onCancel?(): void;
}
interface State {
showConfirm: boolean;
}
export const ConfirmButton = ({
children,
className,
closeOnConfirm,
confirmText = 'Save',
confirmVariant = 'primary',
disabled = false,
onCancel,
onClick,
onConfirm,
size = 'md',
}: Props) => {
const mainButtonRef = useRef<HTMLButtonElement>(null);
const confirmButtonRef = useRef<HTMLButtonElement>(null);
const [showConfirm, setShowConfirm] = useState(false);
const [shouldRestoreFocus, setShouldRestoreFocus] = useState(false);
const styles = useStyles2(getStyles);
class UnThemedConfirmButton extends PureComponent<Props, State> {
mainButtonRef = React.createRef<HTMLButtonElement>();
confirmButtonRef = React.createRef<HTMLButtonElement>();
state: State = {
showConfirm: false,
};
onClickButton = (event: React.MouseEvent<HTMLButtonElement>) => {
if (event) {
event.preventDefault();
}
this.setState(
{
showConfirm: true,
},
() => {
if (this.props.autoFocus && this.confirmButtonRef.current) {
this.confirmButtonRef.current.focus();
}
useEffect(() => {
if (showConfirm) {
confirmButtonRef.current?.focus();
setShouldRestoreFocus(true);
} else {
if (shouldRestoreFocus) {
mainButtonRef.current?.focus();
setShouldRestoreFocus(false);
}
);
if (this.props.onClick) {
this.props.onClick();
}
};
}, [shouldRestoreFocus, showConfirm]);
onClickCancel = (event: React.MouseEvent<HTMLButtonElement>) => {
const onClickButton = (event: React.MouseEvent<HTMLButtonElement>) => {
if (event) {
event.preventDefault();
}
this.setState(
{
showConfirm: false,
},
() => {
this.mainButtonRef.current?.focus();
}
);
if (this.props.onCancel) {
this.props.onCancel();
}
setShowConfirm(true);
onClick?.();
};
onConfirm = (event: React.MouseEvent<HTMLButtonElement>) => {
const onClickCancel = (event: React.MouseEvent<HTMLButtonElement>) => {
if (event) {
event.preventDefault();
}
this.props.onConfirm();
if (this.props.closeOnConfirm) {
this.setState({
showConfirm: false,
});
setShowConfirm(false);
mainButtonRef.current?.focus();
onCancel?.();
};
const onClickConfirm = (event: React.MouseEvent<HTMLButtonElement>) => {
if (event) {
event.preventDefault();
}
onConfirm?.();
if (closeOnConfirm) {
setShowConfirm(false);
}
};
render() {
const {
theme,
className,
size,
disabled,
confirmText,
confirmVariant: confirmButtonVariant,
children,
} = this.props;
const styles = getStyles(theme);
const buttonClass = cx(
className,
this.state.showConfirm ? styles.buttonHide : styles.buttonShow,
disabled && styles.buttonDisabled
);
const confirmButtonClass = cx(
styles.confirmButton,
this.state.showConfirm ? styles.confirmButtonShow : styles.confirmButtonHide
);
const buttonClass = cx(className, styles.mainButton, {
[styles.mainButtonHide]: showConfirm,
});
const confirmButtonClass = cx(styles.confirmButton, {
[styles.confirmButtonHide]: !showConfirm,
});
const confirmButtonContainerClass = cx(styles.confirmButtonContainer, {
[styles.confirmButtonContainerHide]: !showConfirm,
});
const onClick = disabled ? () => {} : this.onClickButton;
return (
<span className={styles.buttonContainer}>
<div className={cx(disabled && styles.disabled)}>
<span className={buttonClass}>
{typeof children === 'string' ? (
<Button size={size} fill="text" onClick={onClick} ref={this.mainButtonRef}>
{children}
</Button>
) : (
React.cloneElement(children, { onClick, ref: this.mainButtonRef })
)}
</span>
</div>
return (
<div className={styles.container}>
<span className={buttonClass}>
{typeof children === 'string' ? (
<Button disabled={disabled} size={size} fill="text" onClick={onClickButton} ref={mainButtonRef}>
{children}
</Button>
) : (
React.cloneElement(children, { disabled, onClick: onClickButton, ref: mainButtonRef })
)}
</span>
<div className={confirmButtonContainerClass}>
<span className={confirmButtonClass}>
<Button size={size} variant={confirmButtonVariant} onClick={this.onConfirm} ref={this.confirmButtonRef}>
<Button size={size} variant={confirmVariant} onClick={onClickConfirm} ref={confirmButtonRef}>
{confirmText}
</Button>
<Button size={size} fill="text" onClick={this.onClickCancel}>
<Button size={size} fill="text" onClick={onClickCancel}>
Cancel
</Button>
</span>
</span>
);
}
}
</div>
</div>
);
};
ConfirmButton.displayName = 'ConfirmButton';
export const ConfirmButton = withTheme2(UnThemedConfirmButton);
const getStyles = stylesFactory((theme: GrafanaTheme2) => {
const getStyles = (theme: GrafanaTheme2) => {
return {
buttonContainer: css({
display: 'flex',
container: css({
alignItems: 'center',
display: 'flex',
justifyContent: 'flex-end',
position: 'relative',
}),
buttonDisabled: css({
textDecoration: 'none',
color: theme.colors.text.primary,
opacity: 0.65,
pointerEvents: 'none',
}),
buttonShow: css({
mainButton: css({
opacity: 1,
transition: 'opacity 0.1s ease',
transition: theme.transitions.create(['opacity'], {
duration: theme.transitions.duration.shortest,
easing: theme.transitions.easing.easeOut,
}),
zIndex: 2,
}),
buttonHide: css({
mainButtonHide: css({
opacity: 0,
transition: 'opacity 0.1s ease, visibility 0 0.1s',
transition: theme.transitions.create(['opacity', 'visibility'], {
duration: theme.transitions.duration.shortest,
easing: theme.transitions.easing.easeIn,
}),
visibility: 'hidden',
zIndex: 0,
}),
confirmButtonContainer: css({
overflow: 'visible',
position: 'absolute',
right: 0,
}),
confirmButtonContainerHide: css({
overflow: 'hidden',
}),
confirmButton: css({
alignItems: 'flex-start',
background: theme.colors.background.primary,
display: 'flex',
position: 'absolute',
pointerEvents: 'none',
}),
confirmButtonShow: css({
zIndex: 1,
opacity: 1,
transition: 'opacity 0.08s ease-out, transform 0.1s ease-out',
transform: 'translateX(0)',
pointerEvents: 'all',
transform: 'translateX(0)',
transition: theme.transitions.create(['opacity', 'transform'], {
duration: theme.transitions.duration.shortest,
easing: theme.transitions.easing.easeOut,
}),
zIndex: 1,
}),
confirmButtonHide: css({
opacity: 0,
pointerEvents: 'none',
transform: 'translateX(100%)',
transition: theme.transitions.create(['opacity', 'transform', 'visibility'], {
duration: theme.transitions.duration.shortest,
easing: theme.transitions.easing.easeIn,
}),
visibility: 'hidden',
transition: 'opacity 0.12s ease-in, transform 0.14s ease-in, visibility 0s 0.12s',
transform: 'translateX(100px)',
}),
disabled: css({
cursor: 'not-allowed',
}),
};
});
// Declare defaultProps directly on the themed component so they are displayed
// in the props table
ConfirmButton.defaultProps = {
size: 'md',
confirmText: 'Save',
disabled: false,
confirmVariant: 'primary',
};
ConfirmButton.displayName = 'ConfirmButton';

View File

@ -245,7 +245,6 @@ class UnThemedOrgRow extends PureComponent<OrgRowProps> {
confirmVariant="destructive"
onCancel={this.onCancelClick}
onConfirm={this.onOrgRemove}
autoFocus
>
Remove from organization
</ConfirmButton>

View File

@ -78,7 +78,6 @@ class BaseUserSessions extends PureComponent<Props, State> {
confirmText="Confirm logout"
confirmVariant="destructive"
onConfirm={this.onSessionRevoke(session.id)}
autoFocus
>
Force logout
</ConfirmButton>