123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- import "./ToolIcon.scss";
- import React, { useEffect, useRef, useState } from "react";
- import clsx from "clsx";
- import { useExcalidrawContainer } from "./App";
- import { AbortError } from "../errors";
- import Spinner from "./Spinner";
- import { PointerType } from "../element/types";
- export type ToolButtonSize = "small" | "medium";
- type ToolButtonBaseProps = {
- icon?: React.ReactNode;
- "aria-label": string;
- "aria-keyshortcuts"?: string;
- "data-testid"?: string;
- label?: string;
- title?: string;
- name?: string;
- id?: string;
- size?: ToolButtonSize;
- keyBindingLabel?: string;
- showAriaLabel?: boolean;
- hidden?: boolean;
- visible?: boolean;
- selected?: boolean;
- className?: string;
- isLoading?: boolean;
- };
- type ToolButtonProps =
- | (ToolButtonBaseProps & {
- type: "button";
- children?: React.ReactNode;
- onClick?(event: React.MouseEvent): void;
- })
- | (ToolButtonBaseProps & {
- type: "submit";
- children?: React.ReactNode;
- onClick?(event: React.MouseEvent): void;
- })
- | (ToolButtonBaseProps & {
- type: "icon";
- children?: React.ReactNode;
- onClick?(): void;
- })
- | (ToolButtonBaseProps & {
- type: "radio";
- checked: boolean;
- onChange?(data: { pointerType: PointerType | null }): void;
- onPointerDown?(data: { pointerType: PointerType }): void;
- });
- export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
- const { id: excalId } = useExcalidrawContainer();
- const innerRef = React.useRef(null);
- React.useImperativeHandle(ref, () => innerRef.current);
- const sizeCn = `ToolIcon_size_${props.size}`;
- const [isLoading, setIsLoading] = useState(false);
- const isMountedRef = useRef(true);
- const onClick = async (event: React.MouseEvent) => {
- const ret = "onClick" in props && props.onClick?.(event);
- if (ret && "then" in ret) {
- try {
- setIsLoading(true);
- await ret;
- } catch (error: any) {
- if (!(error instanceof AbortError)) {
- throw error;
- } else {
- console.warn(error);
- }
- } finally {
- if (isMountedRef.current) {
- setIsLoading(false);
- }
- }
- }
- };
- useEffect(
- () => () => {
- isMountedRef.current = false;
- },
- [],
- );
- const lastPointerTypeRef = useRef<PointerType | null>(null);
- if (
- props.type === "button" ||
- props.type === "icon" ||
- props.type === "submit"
- ) {
- const type = (props.type === "icon" ? "button" : props.type) as
- | "button"
- | "submit";
- return (
- <button
- className={clsx(
- "ToolIcon_type_button",
- sizeCn,
- props.className,
- props.visible && !props.hidden
- ? "ToolIcon_type_button--show"
- : "ToolIcon_type_button--hide",
- {
- ToolIcon: !props.hidden,
- "ToolIcon--selected": props.selected,
- "ToolIcon--plain": props.type === "icon",
- },
- )}
- data-testid={props["data-testid"]}
- hidden={props.hidden}
- title={props.title}
- aria-label={props["aria-label"]}
- type={type}
- onClick={onClick}
- ref={innerRef}
- disabled={isLoading || props.isLoading}
- >
- {(props.icon || props.label) && (
- <div className="ToolIcon__icon" aria-hidden="true">
- {props.icon || props.label}
- {props.keyBindingLabel && (
- <span className="ToolIcon__keybinding">
- {props.keyBindingLabel}
- </span>
- )}
- {props.isLoading && <Spinner />}
- </div>
- )}
- {props.showAriaLabel && (
- <div className="ToolIcon__label">
- {props["aria-label"]} {isLoading && <Spinner />}
- </div>
- )}
- {props.children}
- </button>
- );
- }
- return (
- <label
- className={clsx("ToolIcon", props.className)}
- title={props.title}
- onPointerDown={(event) => {
- lastPointerTypeRef.current = event.pointerType || null;
- props.onPointerDown?.({ pointerType: event.pointerType || null });
- }}
- onPointerUp={() => {
- requestAnimationFrame(() => {
- lastPointerTypeRef.current = null;
- });
- }}
- >
- <input
- className={`ToolIcon_type_radio ${sizeCn}`}
- type="radio"
- name={props.name}
- aria-label={props["aria-label"]}
- aria-keyshortcuts={props["aria-keyshortcuts"]}
- data-testid={props["data-testid"]}
- id={`${excalId}-${props.id}`}
- onChange={() => {
- props.onChange?.({ pointerType: lastPointerTypeRef.current });
- }}
- checked={props.checked}
- ref={innerRef}
- />
- <div className="ToolIcon__icon">
- {props.icon}
- {props.keyBindingLabel && (
- <span className="ToolIcon__keybinding">{props.keyBindingLabel}</span>
- )}
- </div>
- </label>
- );
- });
- ToolButton.defaultProps = {
- visible: true,
- className: "",
- size: "medium",
- };
|