react-use#useKey TypeScript Examples

The following examples show how to use react-use#useKey. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: comment-box.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
IssueCommentBox = (props: IProps) => {
  const ESC_TARGET = 'issue-comment-box';
  const { onSave = () => {}, editAuth } = props;
  const loginUser = userStore.useStore((s) => s.loginUser);

  const [state, updater] = useUpdate({
    visible: false,
    disableSave: true,
  });

  const valueRef = React.useRef('');
  const focusRef = React.useRef(true);

  const submit = () => {
    const saveVal = valueRef.current.trim();
    if (saveVal.length) {
      onSave(saveVal);
      valueRef.current = '';
      focusRef.current = false;
      updater.visible(false);
    }
  };

  const enterEsc = useEscScope(ESC_TARGET, () => {
    updater.visible(false);
  });

  // fn in useKey will not get newest state, so we need to use ref
  useKey('Enter', (e) => {
    if (focusRef.current && (isWin ? e.shiftKey : e.metaKey)) {
      submit();
    }
  });

  return (
    <div
      className={`absolute flex items-start z-10 rounded-sm p-4 shadow-card-lg bg-white bottom-0`}
      style={{ left: 'calc(12% - 16px)', right: 'calc(12% - 16px)' }}
    >
      <UserInfo.RenderWithAvatar avatarSize="default" id={loginUser.id} showName={false} className="mr-3" />
      {state.visible ? (
        <>
          <span className="issue-md-arrow" />
          <div className="flex-1 overflow-auto">
            <MarkdownEditor
              value={valueRef.current}
              placeholder={i18n.t('dop:Comment ({meta} + Enter to send, Esc to collapse)', {
                meta: isWin ? 'Shift' : 'Cmd',
              })}
              onFocus={() => {
                focusRef.current = true;
              }}
              onBlur={() => {
                focusRef.current = false;
              }}
              autoFocus
              className="w-full"
              onChange={(val: string) => {
                valueRef.current = val;
                updater.disableSave(!val.trim().length);
              }}
              style={{ height: '200px' }}
              maxLength={3000}
            />

            <div className="mt-2">
              <Button className="mr-3" type="primary" disabled={state.disableSave} onClick={() => submit()}>
                {i18n.t('dop:Post')}
              </Button>
              <Button onClick={() => updater.visible(false)}>{i18n.t('dop:Collapse')}</Button>
            </div>
          </div>
        </>
      ) : (
        <div
          className="issue-comment-arrow h-8 leading-8 bg-default-04 rounded-sm cursor-pointer px-3 flex-1 hover:text-purple-deep"
          onClick={() => {
            updater.visible(true);
            enterEsc();
          }}
        >
          {i18n.t('dop:Click here to comment')}
        </div>
      )}
    </div>
  );
}
Example #2
Source File: FilesystemBrowser.tsx    From flood with GNU General Public License v3.0 4 votes vote down vote up
FilesystemBrowser: FC<FilesystemBrowserProps> = memo(
  ({directory, selectable, onItemSelection, onYieldFocus}: FilesystemBrowserProps) => {
    const [cursor, setCursor] = useState<number | null>(null);
    const [errorResponse, setErrorResponse] = useState<{data?: NodeJS.ErrnoException} | null>(null);
    const [separator, setSeparator] = useState<string>(directory.includes('/') ? '/' : '\\');
    const [directories, setDirectories] = useState<string[] | null>(null);
    const [files, setFiles] = useState<string[] | null>(null);

    const listRef = useRef<HTMLUListElement>(null);

    const lastSegmentIndex = directory.lastIndexOf(separator) + 1;
    const currentDirectory = lastSegmentIndex > 0 ? directory.substr(0, lastSegmentIndex) : directory;
    const lastSegment = directory.substr(lastSegmentIndex);

    useEffect(() => {
      if (!currentDirectory) {
        return;
      }

      setCursor(null);
      setDirectories(null);
      setFiles(null);

      FloodActions.fetchDirectoryList(currentDirectory)
        .then(({files: fetchedFiles, directories: fetchedDirectories, separator: fetchedSeparator}) => {
          setDirectories(fetchedDirectories);
          setFiles(fetchedFiles);
          setSeparator(fetchedSeparator);
          setErrorResponse(null);
        })
        .catch(({response}) => {
          setErrorResponse(response);
        });
    }, [currentDirectory]);

    useEffect(() => {
      if (listRef.current != null && cursor != null) {
        const element = (listRef.current.children[cursor] as HTMLLIElement)?.children[0] as HTMLDivElement;
        if (element?.tabIndex === 0) {
          element.focus();
        } else {
          setCursor(
            Array.from(listRef.current.children).findIndex((e) => (e.children[0] as HTMLDivElement).tabIndex === 0),
          );
        }
      }
    }, [cursor]);

    useKey('ArrowUp', (e) => {
      e.preventDefault();
      setCursor((prevCursor) => {
        if (prevCursor == null || prevCursor - 1 < 0) {
          onYieldFocus?.();
          return null;
        }

        return prevCursor - 1;
      });
    });

    useKey('ArrowDown', (e) => {
      e.preventDefault();
      setCursor((prevCursor) => {
        if (prevCursor != null) {
          return prevCursor + 1;
        }
        return 0;
      });
    });

    let errorMessage: string | null = null;
    let listItems: ReactNodeArray = [];

    if ((directories == null && selectable === 'directories') || (files == null && selectable === 'files')) {
      errorMessage = 'filesystem.fetching';
    }

    if (errorResponse && errorResponse.data && errorResponse.data.code) {
      errorMessage = MESSAGES[errorResponse.data.code as keyof typeof MESSAGES] || 'filesystem.error.unknown';
    }

    if (!directory) {
      errorMessage = 'filesystem.error.no.input';
    } else {
      const parentDirectory = `${currentDirectory.split(separator).slice(0, -2).join(separator)}${separator}`;
      const parentDirectoryElement = (
        <li
          css={[
            listItemStyle,
            listItemSelectableStyle,
            {
              '@media (max-width: 720px)': headerStyle,
            },
          ]}
          key={parentDirectory}
        >
          <button type="button" onClick={() => onItemSelection?.(parentDirectory, true)}>
            <Arrow css={{transform: 'scale(0.75) rotate(180deg)'}} />
            ..
          </button>
        </li>
      );

      const isDirectorySelectable = selectable !== 'files';
      const directoryMatched = lastSegment ? termMatch(directories, (subDirectory) => subDirectory, lastSegment) : [];

      const inputDirectoryElement =
        !directoryMatched.includes(lastSegment) && selectable === 'directories' && lastSegment && !errorMessage
          ? (() => {
              const inputDestination = `${currentDirectory}${lastSegment}${separator}`;
              return [
                <li
                  css={[
                    listItemStyle,
                    listItemSelectableStyle,
                    {fontWeight: 'bold', '@media (max-width: 720px)': {display: 'none'}},
                  ]}
                  key={inputDestination}
                >
                  <button type="button" onClick={() => onItemSelection?.(inputDestination, false)}>
                    <FolderClosedOutlined />
                    <span css={{whiteSpace: 'pre-wrap'}}>{lastSegment}</span>
                    <em css={{fontWeight: 'lighter'}}>
                      {' - '}
                      <Trans id="filesystem.error.enoent" />
                    </em>
                  </button>
                </li>,
              ];
            })()
          : [];

      const directoryList: ReactNodeArray =
        (directories?.length &&
          sort(directories.slice())
            .desc((subDirectory) => directoryMatched.includes(subDirectory))
            .map((subDirectory) => {
              const destination = `${currentDirectory}${subDirectory}${separator}`;
              return (
                <li
                  css={[
                    listItemStyle,
                    isDirectorySelectable ? listItemSelectableStyle : undefined,
                    directoryMatched.includes(subDirectory) ? {fontWeight: 'bold'} : undefined,
                  ]}
                  key={destination}
                >
                  <button
                    type="button"
                    disabled={!isDirectorySelectable}
                    tabIndex={isDirectorySelectable ? 0 : -1}
                    onClick={isDirectorySelectable ? () => onItemSelection?.(destination, true) : undefined}
                  >
                    <FolderClosedSolid />
                    {subDirectory}
                  </button>
                </li>
              );
            })) ||
        [];

      const isFileSelectable = selectable !== 'directories';
      const fileMatched = lastSegment ? termMatch(files, (file) => file, lastSegment) : [];
      const fileList: ReactNodeArray =
        (files?.length &&
          sort(files.slice())
            .desc((file) => fileMatched.includes(file))
            .map((file) => {
              const destination = `${currentDirectory}${file}`;
              return (
                <li
                  css={[
                    listItemStyle,
                    isFileSelectable ? listItemSelectableStyle : undefined,
                    fileMatched.includes(file) ? {fontWeight: 'bold'} : undefined,
                  ]}
                  key={destination}
                >
                  <button
                    type="button"
                    disabled={!isFileSelectable}
                    tabIndex={isFileSelectable ? 0 : -1}
                    onClick={isFileSelectable ? () => onItemSelection?.(destination, false) : undefined}
                  >
                    <File />
                    {file}
                  </button>
                </li>
              );
            })) ||
        [];

      if (directoryList.length === 0 && fileList.length === 0 && !errorMessage) {
        errorMessage = 'filesystem.empty.directory';
      }

      listItems = [parentDirectoryElement, ...inputDirectoryElement, ...directoryList, ...fileList];
    }

    return (
      <div
        css={{
          color: foregroundColor,
          listStyle: 'none',
          padding: '3px 0px',
          '.icon': {
            fill: 'currentColor',
            height: '14px',
            width: '14px',
            marginRight: `${25 * (1 / 5)}px`,
            marginTop: '-3px',
            verticalAlign: 'middle',
          },
        }}
      >
        {currentDirectory && (
          <li
            css={[
              listItemStyle,
              headerStyle,
              itemPadding,
              {
                whiteSpace: 'pre-wrap',
                wordBreak: 'break-all',
                '.icon': {
                  transform: 'scale(0.9)',
                  marginTop: '-2px !important',
                },
                '@media (max-width: 720px)': {
                  display: 'none',
                },
              },
            ]}
          >
            <FolderOpenSolid />
            {currentDirectory}
          </li>
        )}
        <ul ref={listRef}>{listItems}</ul>
        {errorMessage && (
          <div css={[listItemStyle, itemPadding, {opacity: 1}]}>
            <em>
              <Trans id={errorMessage} />
            </em>
          </div>
        )}
      </div>
    );
  },
)
Example #3
Source File: Select.tsx    From flood with GNU General Public License v3.0 4 votes vote down vote up
Select: FC<SelectProps> = ({
  additionalClassNames,
  children,
  defaultID,
  disabled,
  label,
  labelOffset,
  persistentPlaceholder,
  priority,
  shrink,
  grow,
  matchTriggerWidth,
  width,
  id,
  menuAlign,
  onOpen,
  onClose,
  onSelect,
}: SelectProps) => {
  const menuRef = useRef<HTMLDivElement>(null);
  const inputRef = useRef<HTMLInputElement>(null);
  const triggerRef = useRef<HTMLButtonElement>(null);

  const [isOpen, setIsOpen] = useState<boolean>(false);
  const [selectedID, setSelectedID] = useState<string | number>(
    defaultID ??
      (
        (children as ReactNodeArray)?.find(
          (child) => (child as ReactElement<SelectItemProps>)?.props?.id != null,
        ) as ReactElement<SelectItemProps>
      )?.props.id ??
      '',
  );

  const classes = classnames('select form__element', additionalClassNames, {
    'form__element--disabled': disabled,
    'form__element--label-offset': labelOffset,
    'select--is-open': isOpen,
  });

  const selectedItem = Children.toArray(children).find((child, index) => {
    const item = child as ReactElement<SelectItemProps>;
    return (
      (persistentPlaceholder && item.props.isPlaceholder) ||
      (!selectedID && index === 0) ||
      item.props.id === selectedID
    );
  });

  useKey('Escape', (event) => {
    event.preventDefault();
    setIsOpen(false);
  });

  useEvent(
    'scroll',
    (event: Event) => {
      if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
        setIsOpen(false);
      }
    },
    window,
    {capture: true},
  );

  useEffect(() => {
    if (isOpen) {
      onOpen?.();
    } else {
      onClose?.();
    }
  }, [isOpen, onClose, onOpen]);

  useEffect(() => {
    onSelect?.(selectedID);

    if (inputRef.current != null) {
      dispatchChangeEvent(inputRef.current);
    }
  }, [onSelect, selectedID]);

  return (
    <FormRowItem shrink={shrink} grow={grow} width={width}>
      {label && (
        <label className="form__element__label" htmlFor={`${id}`}>
          {label}
        </label>
      )}
      <div className={classes}>
        <input
          className="input input--hidden"
          name={`${id}`}
          onChange={() => undefined}
          tabIndex={-1}
          ref={inputRef}
          type="text"
          value={selectedID}
        />
        <Button
          additionalClassNames="select__button"
          buttonRef={triggerRef}
          addonPlacement="after"
          onClick={() => {
            if (!disabled) {
              setIsOpen(!isOpen);
            }
          }}
          priority={priority}
          wrap={false}
        >
          <FormElementAddon className="select__indicator">
            <Chevron />
          </FormElementAddon>
          {selectedItem && cloneElement(selectedItem as ReactElement, {isTrigger: true})}
        </Button>
        <Portal>
          <ContextMenu
            onOverlayClick={() => {
              setIsOpen(!isOpen);
            }}
            isIn={isOpen}
            matchTriggerWidth={matchTriggerWidth}
            menuAlign={menuAlign}
            ref={menuRef}
            triggerRef={triggerRef}
          >
            {Children.toArray(children).reduce((accumulator: Array<ReactElement>, child) => {
              const item = child as ReactElement<SelectItemProps>;

              if (item.props.isPlaceholder) {
                return accumulator;
              }

              accumulator.push(
                cloneElement(child as ReactElement, {
                  onClick: (selection: string | number) => {
                    setIsOpen(false);
                    setSelectedID(selection);
                  },
                  isSelected: item.props.id === selectedID,
                }),
              );

              return accumulator;
            }, [])}
          </ContextMenu>
        </Portal>
      </div>
    </FormRowItem>
  );
}
Example #4
Source File: AddInput.tsx    From nextjs-hasura-fullstack with MIT License 4 votes vote down vote up
AddInput: React.FC<AddInputProps> = (props) => {
  const {
    onSubmit,
    isCreating,
    setIsCreating,
    loading,
    className,
    label,
    placeholder,
    onClickAway,
  } = props

  const [value, setValue] = React.useState('')

  const reset = React.useCallback(() => {
    setIsCreating(false)
    setValue('')
  }, [])

  const handleClickAway = React.useCallback(() => {
    onClickAway(value)
    reset()
  }, [onClickAway, reset, value])

  const ref = React.useRef<HTMLInputElement>(null)

  useClickAway(ref, () => handleClickAway())
  useKey('Escape', () => handleClickAway())

  const handleSubmit = React.useCallback(() => {
    onSubmit(value)
    reset()
  }, [onSubmit, reset, value])

  const cls = clsx(className, `flex w-full h-10`)

  return (
    <div className={cls}>
      {!isCreating && (
        <Button
          isLoading={loading}
          isBlock
          className={``}
          onClick={() => {
            setIsCreating(true)
          }}
          variant="light"
          iconLeft={
            <svg
              xmlns="http://www.w3.org/2000/svg"
              viewBox="0 0 20 20"
              fill="currentColor"
            >
              <path
                fillRule="evenodd"
                d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z"
                clipRule="evenodd"
              />
            </svg>
          }
        >
          {label}
        </Button>
      )}
      {isCreating && (
        <div className={`flex items-center justify-between`} ref={ref}>
          <Input
            // eslint-disable-next-line jsx-a11y/no-autofocus
            autoFocus={true}
            placeholder={placeholder ?? `New ${label}`}
            value={value}
            onChange={(e) => {
              setValue(e.target.value)
            }}
            className={``}
            onKeyDown={(e) => {
              if (e.key === 'Enter') {
                handleSubmit()
              }
            }}
          />
          <ButtonIcon
            className={`flex-grow ml-2`}
            size="base"
            onClick={(e) => {
              e.preventDefault()
              handleSubmit()
            }}
            icon={
              <svg
                xmlns="http://www.w3.org/2000/svg"
                fill="none"
                viewBox="0 0 24 24"
                stroke="currentColor"
              >
                <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth={2}
                  d="M5 13l4 4L19 7"
                />
              </svg>
            }
          />
        </div>
      )}
    </div>
  )
}