From 321148511bf01d4c52f963fd7044954a31b9e397 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 14 Mar 2024 15:50:44 +0000 Subject: [PATCH] Chore: Rewrite `ConfirmButton` (#84402) * convert to function component, clean up + fix a11y * use position: fixed; * fix --- .../ConfirmButton/ConfirmButton.tsx | 240 ++++++++---------- public/app/features/admin/UserOrgs.tsx | 1 - public/app/features/admin/UserSessions.tsx | 1 - 3 files changed, 109 insertions(+), 133 deletions(-) diff --git a/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx index e12ed647455..da6705b0adb 100644 --- a/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx +++ b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.tsx @@ -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(null); + const confirmButtonRef = useRef(null); + const [showConfirm, setShowConfirm] = useState(false); + const [shouldRestoreFocus, setShouldRestoreFocus] = useState(false); + const styles = useStyles2(getStyles); -class UnThemedConfirmButton extends PureComponent { - mainButtonRef = React.createRef(); - confirmButtonRef = React.createRef(); - state: State = { - showConfirm: false, - }; - - onClickButton = (event: React.MouseEvent) => { - 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) => { + const onClickButton = (event: React.MouseEvent) => { 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) => { + + const onClickCancel = (event: React.MouseEvent) => { 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) => { + 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 ( - -
- - {typeof children === 'string' ? ( - - ) : ( - React.cloneElement(children, { onClick, ref: this.mainButtonRef }) - )} - -
+ return ( +
+ + {typeof children === 'string' ? ( + + ) : ( + React.cloneElement(children, { disabled, onClick: onClickButton, ref: mainButtonRef }) + )} + +
- - - - ); - } -} +
+
+ ); +}; +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'; diff --git a/public/app/features/admin/UserOrgs.tsx b/public/app/features/admin/UserOrgs.tsx index a9b2c6c6241..5583380263a 100644 --- a/public/app/features/admin/UserOrgs.tsx +++ b/public/app/features/admin/UserOrgs.tsx @@ -245,7 +245,6 @@ class UnThemedOrgRow extends PureComponent { confirmVariant="destructive" onCancel={this.onCancelClick} onConfirm={this.onOrgRemove} - autoFocus > Remove from organization diff --git a/public/app/features/admin/UserSessions.tsx b/public/app/features/admin/UserSessions.tsx index 1e63b44448e..1129dc7201f 100644 --- a/public/app/features/admin/UserSessions.tsx +++ b/public/app/features/admin/UserSessions.tsx @@ -78,7 +78,6 @@ class BaseUserSessions extends PureComponent { confirmText="Confirm logout" confirmVariant="destructive" onConfirm={this.onSessionRevoke(session.id)} - autoFocus > Force logout