feat(component): add autoFocusConfirmButton for confirm-modal (#7801)

close #5813

<div class='graphite__hidden'>
          <div>🎥 Video uploaded on Graphite:</div>
            <a href="https://app.graphite.dev/media/video/LakojjjzZNf6ogjOVwKE/aff35b76-9f73-4d15-b2cb-c25b03e2e2c3.mp4">
              <img src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/LakojjjzZNf6ogjOVwKE/aff35b76-9f73-4d15-b2cb-c25b03e2e2c3.mp4">
            </a>
          </div>
<video src="https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/LakojjjzZNf6ogjOVwKE/aff35b76-9f73-4d15-b2cb-c25b03e2e2c3.mp4">CleanShot 2024-08-09 at 11.25.46.mp4</video>
This commit is contained in:
CatsJuice
2024-08-09 05:50:22 +00:00
parent b2c00a2618
commit 26fd9a4a1c
5 changed files with 76 additions and 33 deletions

View File

@@ -7,6 +7,7 @@ import type {
} from 'react';
import { cloneElement, forwardRef, useCallback } from 'react';
import { useAutoFocus } from '../../hooks';
import { Loading } from '../loading';
import { Tooltip, type TooltipProps } from '../tooltip';
import * as styles from './button.css';
@@ -120,12 +121,15 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
tooltip,
tooltipShortcut,
tooltipOptions,
autoFocus,
onClick,
...otherProps
},
ref
upstreamRef
) => {
const ref = useAutoFocus<HTMLButtonElement>(autoFocus);
const handleClick = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
if (loading || disabled) return;
@@ -134,11 +138,22 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
[disabled, loading, onClick]
);
const buttonRef = (el: HTMLButtonElement | null) => {
ref.current = el;
if (upstreamRef) {
if (typeof upstreamRef === 'function') {
upstreamRef(el);
} else {
upstreamRef.current = el;
}
}
};
return (
<Tooltip content={tooltip} shortcut={tooltipShortcut} {...tooltipOptions}>
<button
{...otherProps}
ref={ref}
ref={buttonRef}
className={clsx(styles.button, className)}
data-loading={loading || undefined}
data-block={block || undefined}

View File

@@ -8,14 +8,9 @@ import type {
KeyboardEventHandler,
ReactNode,
} from 'react';
import {
forwardRef,
useCallback,
useEffect,
useLayoutEffect,
useRef,
} from 'react';
import { forwardRef, useCallback, useEffect } from 'react';
import { useAutoFocus, useAutoSelect } from '../../hooks';
import { input, inputWrapper } from './style.css';
export type InputProps = {
@@ -55,30 +50,31 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
}: InputProps,
upstreamRef: ForwardedRef<HTMLInputElement>
) {
const inputRef = useRef<HTMLInputElement | null>(null);
useLayoutEffect(() => {
if (inputRef.current && (autoFocus || autoSelect)) {
// to avoid clicking on something focusable(e.g MenuItem),
// then the input will not be focused
setTimeout(() => {
inputRef.current?.focus();
}, 0);
if (autoSelect) {
inputRef.current?.select();
const focusRef = useAutoFocus<HTMLInputElement>(autoFocus);
const selectRef = useAutoSelect<HTMLInputElement>(autoSelect);
const inputRef = (el: HTMLInputElement | null) => {
focusRef.current = el;
selectRef.current = el;
if (upstreamRef) {
if (typeof upstreamRef === 'function') {
upstreamRef(el);
} else {
upstreamRef.current = el;
}
}
}, [autoFocus, autoSelect, upstreamRef]);
};
// use native blur event to get event after unmount
// don't use useLayoutEffect here, because the cleanup function will be called before unmount
useEffect(() => {
if (!onBlur) return;
inputRef.current?.addEventListener('blur', onBlur as any);
selectRef.current?.addEventListener('blur', onBlur as any);
return () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
inputRef.current?.removeEventListener('blur', onBlur as any);
selectRef.current?.removeEventListener('blur', onBlur as any);
};
}, [onBlur]);
}, [onBlur, selectRef]);
return (
<div
@@ -105,16 +101,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
large: size === 'large',
'extra-large': size === 'extraLarge',
})}
ref={ref => {
inputRef.current = ref;
if (upstreamRef) {
if (typeof upstreamRef === 'function') {
upstreamRef(ref);
} else {
upstreamRef.current = ref;
}
}
}}
ref={inputRef}
disabled={disabled}
style={inputStyle}
onChange={useCallback(

View File

@@ -17,6 +17,11 @@ export interface ConfirmModalProps extends ModalProps {
cancelText?: React.ReactNode;
cancelButtonOptions?: Omit<ButtonProps, 'children'>;
reverseFooter?: boolean;
/**
* Auto focus on confirm button when modal opened
* @default true
*/
autoFocusConfirm?: boolean;
}
export const ConfirmModal = ({
@@ -30,6 +35,7 @@ export const ConfirmModal = ({
onConfirm,
onCancel,
width = 480,
autoFocusConfirm = true,
...props
}: ConfirmModalProps) => {
const onConfirmClick = useCallback(() => {
@@ -73,6 +79,7 @@ export const ConfirmModal = ({
<Button
onClick={onConfirmClick}
data-testid="confirm-modal-confirm"
autoFocus={autoFocusConfirm}
{...confirmButtonOptions}
>
{confirmText}