Kaynağa Gözat

Add and use clsx (classnames alternative) (#2249)

Co-authored-by: David Luzar <luzar.david@gmail.com>
Danila 4 yıl önce
ebeveyn
işleme
b50c54f855

+ 5 - 0
package-lock.json

@@ -5399,6 +5399,11 @@
         "mimic-response": "^1.0.0"
       }
     },
+    "clsx": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz",
+      "integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA=="
+    },
     "co": {
       "version": "4.6.0",
       "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",

+ 1 - 0
package.json

@@ -29,6 +29,7 @@
     "@types/react-dom": "16.9.8",
     "@types/socket.io-client": "1.4.34",
     "browser-nativefs": "0.11.0",
+    "clsx": "1.1.1",
     "firebase": "7.23.0",
     "i18next-browser-languagedetector": "6.0.1",
     "lodash.throttle": "4.1.1",

+ 1 - 1
src/components/Avatar.tsx

@@ -9,7 +9,7 @@ type AvatarProps = {
 };
 
 export const Avatar = ({ children, color, onClick }: AvatarProps) => (
-  <div className={`Avatar`} style={{ background: color }} onClick={onClick}>
+  <div className="Avatar" style={{ background: color }} onClick={onClick}>
     {children}
   </div>
 );

+ 2 - 1
src/components/ButtonSelect.tsx

@@ -1,4 +1,5 @@
 import React from "react";
+import clsx from "clsx";
 
 export const ButtonSelect = <T extends Object>({
   options,
@@ -15,7 +16,7 @@ export const ButtonSelect = <T extends Object>({
     {options.map((option) => (
       <label
         key={option.text}
-        className={value === option.value ? "active" : ""}
+        className={clsx({ active: value === option.value })}
       >
         <input
           type="radio"

+ 8 - 5
src/components/ContextMenu.tsx

@@ -1,6 +1,7 @@
 import React from "react";
-import { Popover } from "./Popover";
 import { render, unmountComponentAtNode } from "react-dom";
+import clsx from "clsx";
+import { Popover } from "./Popover";
 
 import "./ContextMenu.scss";
 
@@ -20,11 +21,13 @@ const ContextMenu = ({ options, onCloseRequest, top, left }: Props) => {
   const isDarkTheme = !!document
     .querySelector(".excalidraw")
     ?.classList.contains("Appearance_dark");
-  const wrapperClasses = `excalidraw ${
-    isDarkTheme ? "Appearance_dark Appearance_dark-background-none" : ""
-  }`;
+
   return (
-    <div className={wrapperClasses}>
+    <div
+      className={clsx("excalidraw", {
+        "Appearance_dark Appearance_dark-background-none": isDarkTheme,
+      })}
+    >
       <Popover
         onCloseRequest={onCloseRequest}
         top={top}

+ 2 - 1
src/components/Dialog.tsx

@@ -1,4 +1,5 @@
 import React, { useEffect, useRef } from "react";
+import clsx from "clsx";
 import { Modal } from "./Modal";
 import { Island } from "./Island";
 import { t } from "../i18n";
@@ -68,7 +69,7 @@ export const Dialog = (props: {
 
   return (
     <Modal
-      className={`${props.className ?? ""} Dialog`}
+      className={clsx("Dialog", props.className)}
       labelledBy="dialog-title"
       maxWidth={props.maxWidth}
       onCloseRequest={props.onCloseRequest}

+ 6 - 1
src/components/FixedSideContainer.tsx

@@ -1,6 +1,7 @@
 import "./FixedSideContainer.scss";
 
 import React from "react";
+import clsx from "clsx";
 
 type FixedSideContainerProps = {
   children: React.ReactNode;
@@ -14,7 +15,11 @@ export const FixedSideContainer = ({
   className,
 }: FixedSideContainerProps) => (
   <div
-    className={`FixedSideContainer FixedSideContainer_side_${side} ${className}`}
+    className={clsx(
+      "FixedSideContainer",
+      `FixedSideContainer_side_${side}`,
+      className,
+    )}
   >
     {children}
   </div>

+ 2 - 1
src/components/Island.tsx

@@ -1,6 +1,7 @@
 import "./Island.scss";
 
 import React from "react";
+import clsx from "clsx";
 
 type IslandProps = {
   children: React.ReactNode;
@@ -12,7 +13,7 @@ type IslandProps = {
 export const Island = React.forwardRef<HTMLDivElement, IslandProps>(
   ({ children, padding, className, style }, ref) => (
     <div
-      className={`${className ?? ""} Island`}
+      className={clsx("Island", className)}
       style={{ "--padding": padding, ...style } as React.CSSProperties}
       ref={ref}
     >

+ 4 - 3
src/components/LanguageList.tsx

@@ -1,4 +1,5 @@
 import React from "react";
+import clsx from "clsx";
 import * as i18n from "../i18n";
 
 export const LanguageList = ({
@@ -14,9 +15,9 @@ export const LanguageList = ({
 }) => (
   <React.Fragment>
     <select
-      className={`dropdown-select dropdown-select__language${
-        floating ? " dropdown-select--floating" : ""
-      }`}
+      className={clsx("dropdown-select dropdown-select__language", {
+        "dropdown-select--floating": floating,
+      })}
       onChange={({ target }) => onChange(target.value)}
       value={currentLanguage}
       aria-label={i18n.t("buttons.selectLanguage")}

+ 33 - 22
src/components/LayerUI.tsx

@@ -44,6 +44,7 @@ import { ToolButton } from "./ToolButton";
 import { saveLibraryAsJSON, importLibraryFromJSON } from "../data/json";
 import { muteFSAbortError } from "../utils";
 import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
+import clsx from "clsx";
 
 interface LayerUIProps {
   actionManager: ActionManager;
@@ -294,9 +295,9 @@ const LayerUI = ({
   // TODO: Extend tooltip component and use here.
   const renderEncryptedIcon = () => (
     <a
-      className={`encrypted-icon tooltip zen-mode-visibility ${
-        zenModeEnabled ? "zen-mode-visibility--hidden" : ""
-      }`}
+      className={clsx("encrypted-icon tooltip zen-mode-visibility", {
+        "zen-mode-visibility--hidden": zenModeEnabled,
+      })}
       href="https://blog.excalidraw.com/end-to-end-encryption/"
       target="_blank"
       rel="noopener noreferrer"
@@ -362,7 +363,9 @@ const LayerUI = ({
   const renderCanvasActions = () => (
     <Section
       heading="canvasActions"
-      className={`zen-mode-transition ${zenModeEnabled && "transition-left"}`}
+      className={clsx("zen-mode-transition", {
+        "transition-left": zenModeEnabled,
+      })}
     >
       {/* the zIndex ensures this menu has higher stacking order,
          see https://github.com/excalidraw/excalidraw/pull/1445 */}
@@ -399,7 +402,9 @@ const LayerUI = ({
   const renderSelectedShapeActions = () => (
     <Section
       heading="selectedShapeActions"
-      className={`zen-mode-transition ${zenModeEnabled && "transition-left"}`}
+      className={clsx("zen-mode-transition", {
+        "transition-left": zenModeEnabled,
+      })}
     >
       <Island className={CLASSES.SHAPE_ACTIONS_MENU} padding={2}>
         <SelectedShapeActions
@@ -447,7 +452,7 @@ const LayerUI = ({
         <div className="App-menu App-menu_top">
           <Stack.Col
             gap={4}
-            className={zenModeEnabled && "disable-pointerEvents"}
+            className={clsx({ "disable-pointerEvents": zenModeEnabled })}
           >
             {renderCanvasActions()}
             {shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
@@ -456,7 +461,10 @@ const LayerUI = ({
             {(heading) => (
               <Stack.Col gap={4} align="start">
                 <Stack.Row gap={1}>
-                  <Island padding={1} className={zenModeEnabled && "zen-mode"}>
+                  <Island
+                    padding={1}
+                    className={clsx({ "zen-mode": zenModeEnabled })}
+                  >
                     <HintViewer appState={appState} elements={elements} />
                     {heading}
                     <Stack.Row gap={1}>
@@ -479,9 +487,9 @@ const LayerUI = ({
             )}
           </Section>
           <UserList
-            className={`zen-mode-transition ${
-              zenModeEnabled && "transition-right"
-            }`}
+            className={clsx("zen-mode-transition", {
+              "transition-right": zenModeEnabled,
+            })}
           >
             {Array.from(appState.collaborators)
               // Collaborator is either not initialized or is actually the current user.
@@ -503,9 +511,9 @@ const LayerUI = ({
   const renderBottomAppMenu = () => {
     return (
       <div
-        className={`App-menu App-menu_bottom zen-mode-transition ${
-          zenModeEnabled && "App-menu_bottom--transition-left"
-        }`}
+        className={clsx("App-menu App-menu_bottom zen-mode-transition", {
+          "App-menu_bottom--transition-left": zenModeEnabled,
+        })}
       >
         <Stack.Col gap={2}>
           <Section heading="canvasActions">
@@ -525,9 +533,9 @@ const LayerUI = ({
   const renderFooter = () => (
     <footer role="contentinfo" className="layer-ui__wrapper__footer">
       <div
-        className={`zen-mode-transition ${
-          zenModeEnabled && "transition-right disable-pointerEvents"
-        }`}
+        className={clsx("zen-mode-transition", {
+          "transition-right disable-pointerEvents": zenModeEnabled,
+        })}
       >
         <LanguageList
           onChange={async (lng) => {
@@ -540,9 +548,9 @@ const LayerUI = ({
         {actionManager.renderAction("toggleShortcuts")}
       </div>
       <button
-        className={`disable-zen-mode ${
-          zenModeEnabled && "disable-zen-mode--visible"
-        }`}
+        className={clsx("disable-zen-mode", {
+          "disable-zen-mode--visible": zenModeEnabled,
+        })}
         onClick={toggleZenMode}
       >
         {t("buttons.exitZenMode")}
@@ -594,9 +602,12 @@ const LayerUI = ({
       {renderBottomAppMenu()}
       {
         <aside
-          className={`layer-ui__wrapper__github-corner zen-mode-transition ${
-            zenModeEnabled && "transition-right"
-          }`}
+          className={clsx(
+            "layer-ui__wrapper__github-corner zen-mode-transition",
+            {
+              "transition-right": zenModeEnabled,
+            },
+          )}
         >
           <GitHubCorner appearance={appState.appearance} />
         </aside>

+ 7 - 6
src/components/LibraryUnit.tsx

@@ -1,4 +1,5 @@
 import React, { useRef, useEffect, useState } from "react";
+import clsx from "clsx";
 import { exportToSvg } from "../scene/export";
 import { close } from "../components/icons";
 
@@ -63,16 +64,16 @@ export const LibraryUnit = ({
 
   return (
     <div
-      className={`library-unit ${
-        elements || pendingElements ? "library-unit__active" : ""
-      }`}
+      className={clsx("library-unit", {
+        "library-unit__active": elements || pendingElements,
+      })}
       onMouseEnter={() => setIsHovered(true)}
       onMouseLeave={() => setIsHovered(false)}
     >
       <div
-        className={`library-unit__dragger ${
-          !!pendingElements ? "library-unit__pulse" : ""
-        }`}
+        className={clsx("library-unit__dragger", {
+          "library-unit__pulse": !!pendingElements,
+        })}
         ref={ref}
         draggable={!!elements}
         onClick={!!elements || !!pendingElements ? onClick : undefined}

+ 8 - 5
src/components/LockIcon.tsx

@@ -1,6 +1,7 @@
 import "./ToolIcon.scss";
 
 import React from "react";
+import clsx from "clsx";
 
 type LockIconSize = "s" | "m";
 
@@ -41,13 +42,15 @@ const ICONS = {
 };
 
 export const LockIcon = (props: LockIconProps) => {
-  const sizeCn = `ToolIcon_size_${props.size || DEFAULT_SIZE}`;
-
   return (
     <label
-      className={`ToolIcon ToolIcon__lock ToolIcon_type_floating ${sizeCn} zen-mode-visibility ${
-        props.zenModeEnabled ? "zen-mode-visibility--hidden" : ""
-      }`}
+      className={clsx(
+        "ToolIcon ToolIcon__lock ToolIcon_type_floating zen-mode-visibility",
+        `ToolIcon_size_${props.size || DEFAULT_SIZE}`,
+        {
+          "zen-mode-visibility--hidden": props.zenModeEnabled,
+        },
+      )}
       title={`${props.title} — Q`}
     >
       <input

+ 2 - 1
src/components/Modal.tsx

@@ -2,6 +2,7 @@ import "./Modal.scss";
 
 import React, { useState, useLayoutEffect } from "react";
 import { createPortal } from "react-dom";
+import clsx from "clsx";
 import { KEYS } from "../keys";
 
 export const Modal = (props: {
@@ -26,7 +27,7 @@ export const Modal = (props: {
 
   return createPortal(
     <div
-      className={`Modal ${props.className ?? ""}`}
+      className={clsx("Modal", props.className)}
       role="dialog"
       aria-modal="true"
       onKeyDown={handleKeydown}

+ 4 - 3
src/components/RoomDialog.tsx

@@ -1,4 +1,5 @@
 import React, { useState, useEffect, useRef } from "react";
+import clsx from "clsx";
 import { ToolButton } from "./ToolButton";
 import { t } from "../i18n";
 import useIsMobile from "../is-mobile";
@@ -154,9 +155,9 @@ export const RoomDialog = ({
   return (
     <>
       <ToolButton
-        className={`RoomDialog-modalButton ${
-          isCollaborating ? "is-collaborating" : ""
-        }`}
+        className={clsx("RoomDialog-modalButton", {
+          "is-collaborating": isCollaborating,
+        })}
         onClick={() => setModalIsShown(true)}
         icon={users}
         type="button"

+ 3 - 2
src/components/Stack.tsx

@@ -1,6 +1,7 @@
 import "./Stack.scss";
 
 import React from "react";
+import clsx from "clsx";
 
 type StackProps = {
   children: React.ReactNode;
@@ -19,7 +20,7 @@ const RowStack = ({
 }: StackProps) => {
   return (
     <div
-      className={`Stack Stack_horizontal ${className || ""}`}
+      className={clsx("Stack Stack_horizontal", className)}
       style={
         {
           "--gap": gap,
@@ -42,7 +43,7 @@ const ColStack = ({
 }: StackProps) => {
   return (
     <div
-      className={`Stack Stack_vertical ${className || ""}`}
+      className={clsx("Stack Stack_vertical", className)}
       style={
         {
           "--gap": gap,

+ 12 - 8
src/components/ToolButton.tsx

@@ -1,6 +1,7 @@
 import "./ToolIcon.scss";
 
 import React from "react";
+import clsx from "clsx";
 
 type ToolIconSize = "s" | "m";
 
@@ -45,15 +46,18 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
   if (props.type === "button") {
     return (
       <button
-        className={`ToolIcon_type_button ${
-          !props.hidden ? "ToolIcon" : ""
-        } ${sizeCn}${props.selected ? " ToolIcon--selected" : ""} ${
-          props.className
-        } ${
+        className={clsx(
+          "ToolIcon_type_button",
+          sizeCn,
+          props.className,
           props.visible && !props.hidden
             ? "ToolIcon_type_button--show"
-            : "ToolIcon_type_button--hide"
-        }`}
+            : "ToolIcon_type_button--hide",
+          {
+            ToolIcon: !props.hidden,
+            "ToolIcon--selected": props.selected,
+          },
+        )}
         hidden={props.hidden}
         title={props.title}
         aria-label={props["aria-label"]}
@@ -78,7 +82,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
   }
 
   return (
-    <label className={`ToolIcon ${props.className ?? ""}`} title={props.title}>
+    <label className={clsx("ToolIcon", props.className)} title={props.title}>
       <input
         className={`ToolIcon_type_radio ${sizeCn}`}
         type="radio"

+ 6 - 11
src/components/UserList.tsx

@@ -1,6 +1,7 @@
 import "./UserList.scss";
 
 import React from "react";
+import clsx from "clsx";
 
 type UserListProps = {
   children: React.ReactNode;
@@ -9,15 +10,9 @@ type UserListProps = {
 };
 
 export const UserList = ({ children, className, mobile }: UserListProps) => {
-  let compClassName = "UserList";
-
-  if (className) {
-    compClassName += ` ${className}`;
-  }
-
-  if (mobile) {
-    compClassName += " UserList_mobile";
-  }
-
-  return <div className={compClassName}>{children}</div>;
+  return (
+    <div className={clsx("UserList", className, { UserList_mobile: mobile })}>
+      {children}
+    </div>
+  );
 };

+ 2 - 1
src/components/icons.tsx

@@ -6,6 +6,7 @@
 import React from "react";
 
 import oc from "open-color";
+import clsx from "clsx";
 
 const activeElementColor = (appearance: "light" | "dark") =>
   appearance === "light" ? oc.orange[4] : oc.orange[9];
@@ -27,7 +28,7 @@ const createIcon = (d: string | React.ReactNode, opts: number | Opts = 512) => {
       focusable="false"
       role="img"
       viewBox={`0 0 ${width} ${height}`}
-      className={mirror && "rtl-mirror"}
+      className={clsx({ "rtl-mirror": mirror })}
       style={style}
     >
       {typeof d === "string" ? <path fill="currentColor" d={d} /> : d}