فهرست منبع

use AutosizeSubmittableInput and omit ClosableTextInput

Yuki Takei 1 سال پیش
والد
کامیت
1ba06fc06f

+ 12 - 9
apps/app/src/components/Bookmarks/BookmarkFolderItem.tsx

@@ -2,6 +2,7 @@ import type { FC } from 'react';
 import { useCallback, useState } from 'react';
 
 import type { IPageToDeleteWithMeta } from '@growi/core';
+import { useTranslation } from 'react-i18next';
 import { DropdownToggle } from 'reactstrap';
 
 import {
@@ -32,6 +33,8 @@ type BookmarkFolderItemProps = {
 }
 
 export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderItemProps) => {
+  const { t } = useTranslation();
+
   const BASE_FOLDER_PADDING = 15;
   const acceptedTypes: DragItemType[] = [DRAG_ITEM_TYPE.FOLDER, DRAG_ITEM_TYPE.BOOKMARK];
   const {
@@ -257,12 +260,13 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
             <FolderIcon isOpen={isOpen} />
           </div>
           {isRenameAction ? (
-            <BookmarkFolderNameInput
-              onPressEnter={rename}
-              onBlur={rename}
-              onPressEscape={cancel}
-              value={name}
-            />
+            <div className="flex-fill">
+              <BookmarkFolderNameInput
+                value={name}
+                onSubmit={rename}
+                onCancel={cancel}
+              />
+            </div>
           ) : (
             <>
               <div className="grw-foldertree-title-anchor ps-1">
@@ -304,9 +308,8 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
       {isCreateAction && (
         <div className="flex-fill">
           <BookmarkFolderNameInput
-            onPressEnter={create}
-            onBlur={create}
-            onPressEscape={cancel}
+            onSubmit={create}
+            onCancel={cancel}
           />
         </div>
       )}

+ 45 - 8
apps/app/src/components/Bookmarks/BookmarkFolderNameInput.tsx

@@ -1,22 +1,59 @@
+import type { ChangeEvent } from 'react';
+import { useCallback, useState } from 'react';
+
 import { useTranslation } from 'next-i18next';
+import type { AutosizeInputProps } from 'react-input-autosize';
+import { debounce } from 'throttle-debounce';
+
+import type { InputValidationResult } from '~/client/util/use-input-validator';
+import { ValidationTarget, useInputValidator } from '~/client/util/use-input-validator';
 
-import { ValidationTarget } from '~/client/util/use-input-validator';
-import type { ClosableTextInputProps } from '~/components/Common/ClosableTextInput';
-import ClosableTextInput from '~/components/Common/ClosableTextInput';
+import { AutosizeSubmittableInput } from '../Common/SubmittableInput';
+import type { SubmittableInputProps } from '../Common/SubmittableInput/types';
 
 
-type Props = ClosableTextInputProps;
+type Props = Pick<SubmittableInputProps<AutosizeInputProps>, 'value' | 'onSubmit' | 'onCancel'>;
 
 export const BookmarkFolderNameInput = (props: Props): JSX.Element => {
   const { t } = useTranslation();
 
+  const { value, onSubmit, onCancel } = props;
+
+  const [validationResult, setValidationResult] = useState<InputValidationResult>();
+
+
+  const inputValidator = useInputValidator(ValidationTarget.FOLDER);
+
+  const changeHandler = useCallback(async(e: ChangeEvent<HTMLInputElement>) => {
+    const validationResult = inputValidator(e.target.value);
+    setValidationResult(validationResult ?? undefined);
+  }, [inputValidator]);
+  const changeHandlerDebounced = debounce(300, changeHandler);
+
+  const cancelHandler = useCallback(() => {
+    setValidationResult(undefined);
+    onCancel?.();
+  }, [onCancel]);
+
+  const isInvalid = validationResult != null;
+
   return (
-    <div className="flex-fill folder-name-input">
-      <ClosableTextInput
+    <div>
+      <AutosizeSubmittableInput
+        value={value}
+        inputClassName={`form-control ${isInvalid ? 'is-invalid' : ''}`}
         placeholder={t('bookmark_folder.input_placeholder')}
-        validationTarget={ValidationTarget.FOLDER}
-        {...props}
+        aria-describedby={isInvalid ? 'bookmark-folder-name-input-feedback' : undefined}
+        autoFocus
+        onChange={changeHandlerDebounced}
+        onSubmit={onSubmit}
+        onCancel={cancelHandler}
       />
+      { isInvalid && (
+        <div id="bookmark-folder-name-input-feedback" className="invalid-feedback d-block my-1">
+          {validationResult.message}
+        </div>
+      ) }
     </div>
   );
 };

+ 7 - 7
apps/app/src/components/Bookmarks/BookmarkItem.tsx

@@ -12,15 +12,14 @@ import { UncontrolledTooltip, DropdownToggle } from 'reactstrap';
 
 import { bookmark, unbookmark, unlink } from '~/client/services/page-operation';
 import { addBookmarkToFolder, renamePage } from '~/client/util/bookmark-utils';
-import { ValidationTarget } from '~/client/util/use-input-validator';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import type { BookmarkFolderItems, DragItemDataType } from '~/interfaces/bookmark-info';
 import { DRAG_ITEM_TYPE } from '~/interfaces/bookmark-info';
 import { usePutBackPageModal } from '~/stores/modal';
 import { mutateAllPageInfo, useSWRMUTxCurrentPage, useSWRxPageInfo } from '~/stores/page';
 
-import ClosableTextInput from '../Common/ClosableTextInput';
 import { MenuItemType, PageItemControl } from '../Common/Dropdown/PageItemControl';
+import { AutosizeSubmittableInput } from '../Common/SubmittableInput';
 import { PageListItemS } from '../PageList/PageListItemS';
 
 import { BookmarkMoveToRootBtn } from './BookmarkMoveToRootBtn';
@@ -163,13 +162,14 @@ export const BookmarkItem = (props: Props): JSX.Element => {
       >
         { isRenameInputShown
           ? (
-            <ClosableTextInput
+            <AutosizeSubmittableInput
               value={nodePath.basename(bookmarkedPage.path ?? '')}
+              inputClassName="form-control"
               placeholder={t('Input page name')}
-              onPressEnter={rename}
-              onBlur={rename}
-              onPressEscape={() => { setRenameInputShown(false) }}
-              validationTarget={ValidationTarget.PAGE}
+              onSubmit={rename}
+              onCancel={() => { setRenameInputShown(false) }}
+              autoFocus
+              // validationTarget={ValidationTarget.PAGE}
             />
           )
           : <PageListItemS page={bookmarkedPage} pageTitle={pageTitle} isNarrowView />}

+ 0 - 153
apps/app/src/components/Common/ClosableTextInput.tsx

@@ -1,153 +0,0 @@
-import type { CSSProperties, FC } from 'react';
-import React, {
-  memo, useCallback, useEffect, useRef, useState,
-} from 'react';
-
-import { useTranslation } from 'next-i18next';
-import AutosizeInput from 'react-input-autosize';
-
-import type { AlertInfo } from '~/client/util/use-input-validator';
-import { AlertType, inputValidator } from '~/client/util/use-input-validator';
-
-
-// for react-input-autosize
-type InputRefCallback = (instance: HTMLInputElement | null) => void;
-
-export type ClosableTextInputProps = {
-  value?: string
-  placeholder?: string
-  validationTarget?: string,
-  useAutosizeInput?: boolean
-  inputClassName?: string,
-  inputStyle?: CSSProperties,
-  onPressEnter?(inputText: string): void
-  onPressEscape?(inputText: string): void
-  onBlur?(inputText: string): void
-  onChange?(inputText: string): void
-}
-
-const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextInputProps) => {
-  const { t } = useTranslation();
-  const {
-    validationTarget,
-    onPressEnter, onPressEscape, onBlur, onChange,
-  } = props;
-
-  const inputRef = useRef<HTMLInputElement>(null);
-
-  const [inputText, setInputText] = useState(props.value ?? '');
-  const [currentAlertInfo, setAlertInfo] = useState<AlertInfo | null>(null);
-  const [isAbleToShowAlert, setIsAbleToShowAlert] = useState(false);
-  const [isComposing, setComposing] = useState(false);
-
-
-  const createValidation = useCallback(async(inputText: string) => {
-    const alertInfo = await inputValidator(inputText, validationTarget);
-    if (alertInfo && alertInfo.message != null && alertInfo.target != null) {
-      alertInfo.message = t(alertInfo.message, { target: t(alertInfo.target) });
-    }
-    setAlertInfo(alertInfo);
-  }, [t, validationTarget]);
-
-  const changeHandler = useCallback(async(e: React.ChangeEvent<HTMLInputElement>) => {
-    const inputText = e.target.value;
-    createValidation(inputText);
-    setInputText(inputText);
-    setIsAbleToShowAlert(true);
-
-    onChange?.(inputText);
-  }, [createValidation, onChange]);
-
-  const onFocusHandler = useCallback(async(e: React.ChangeEvent<HTMLInputElement>) => {
-    const inputText = e.target.value;
-    await createValidation(inputText);
-  }, [createValidation]);
-
-  const pressEnterHandler = useCallback(() => {
-    if (currentAlertInfo == null) {
-      onPressEnter?.(inputText.trim());
-    }
-  }, [currentAlertInfo, inputText, onPressEnter]);
-
-  const onKeyDownHandler = useCallback((e) => {
-    switch (e.key) {
-      case 'Enter':
-        // Do nothing when composing
-        if (isComposing) {
-          return;
-        }
-        pressEnterHandler();
-        break;
-      case 'Escape':
-        if (isComposing) {
-          return;
-        }
-        onPressEscape?.(inputText.trim());
-        break;
-      default:
-        break;
-    }
-  }, [inputText, isComposing, pressEnterHandler, onPressEscape]);
-
-  /*
-   * Hide when click outside the ref
-   */
-  const onBlurHandler = useCallback(() => {
-    onBlur?.(inputText.trim());
-  }, [inputText, onBlur]);
-
-  // didMount
-  useEffect(() => {
-    // autoFocus
-    if (inputRef?.current == null) {
-      return;
-    }
-    inputRef.current.focus();
-  });
-
-
-  const AlertInfo = () => {
-    if (currentAlertInfo == null) {
-      return <></>;
-    }
-
-    const alertType = currentAlertInfo.type != null ? currentAlertInfo.type : AlertType.ERROR;
-    const alertMessage = currentAlertInfo.message != null ? currentAlertInfo.message : 'Invalid value';
-    const alertTextStyle = alertType === AlertType.ERROR ? 'text-danger' : 'text-warning';
-    const translation = alertType === AlertType.ERROR ? 'Error' : 'Warning';
-    return (
-      <p className={`${alertTextStyle} text-center mt-1`}>{t(translation)}: {alertMessage}</p>
-    );
-  };
-
-  const inputProps = {
-    'data-testid': 'closable-text-input',
-    value: inputText || '',
-    type: 'text',
-    placeholder: props.placeholder,
-    name: 'input',
-    onFocus: onFocusHandler,
-    onChange: changeHandler,
-    onKeyDown: onKeyDownHandler,
-    onCompositionStart: () => setComposing(true),
-    onCompositionEnd: () => setComposing(false),
-    onBlur: onBlurHandler,
-  };
-
-  const inputClassName = `form-control ${props.inputClassName ?? ''}`;
-  const inputStyle = props.inputStyle;
-
-  return (
-    <div>
-      { props.useAutosizeInput
-        ? <AutosizeInput inputRef={inputRef as unknown as InputRefCallback} inputClassName={inputClassName} inputStyle={inputStyle} {...inputProps} />
-        : <input ref={inputRef} className={inputClassName} style={inputStyle} {...inputProps} />
-      }
-      {isAbleToShowAlert && <AlertInfo />}
-    </div>
-  );
-});
-
-ClosableTextInput.displayName = 'ClosableTextInput';
-
-export default ClosableTextInput;

+ 2 - 3
apps/app/src/components/Sidebar/Bookmarks/BookmarkContents.tsx

@@ -58,9 +58,8 @@ export const BookmarkContents = (): JSX.Element => {
       {isCreateAction && (
         <div className="col-12 mb-2 ">
           <BookmarkFolderNameInput
-            onPressEnter={create}
-            onBlur={create}
-            onPressEscape={cancel}
+            onSubmit={create}
+            onCancel={cancel}
           />
         </div>
       )}