From 08563e7d7bd30db16d09da47b40b8faae176d998 Mon Sep 17 00:00:00 2001 From: Are Date: Fri, 26 May 2023 16:16:55 +0200 Subject: [PATCH] feat: update design of ImageExportDialog (#6614) Co-authored-by: dwelle --- src/actions/actionExport.tsx | 3 +- src/actions/manager.tsx | 7 +- src/components/ConfirmDialog.tsx | 2 +- src/components/Dialog.scss | 29 ++ src/components/Dialog.tsx | 33 +- src/components/ErrorDialog.tsx | 2 +- src/components/ImageExportDialog.scss | 215 ++++++++++++ src/components/ImageExportDialog.tsx | 360 +++++++++++++------- src/components/LibraryMenuHeaderContent.tsx | 2 +- src/components/Modal.scss | 19 +- src/components/Modal.tsx | 2 +- src/components/PasteChartDialog.tsx | 2 +- src/components/ProjectName.tsx | 5 +- src/components/RadioGroup.scss | 100 ++++++ src/components/RadioGroup.tsx | 42 +++ src/components/Switch.scss | 116 +++++++ src/components/Switch.tsx | 38 +++ src/components/icons.tsx | 29 ++ src/excalidraw-app/collab/RoomDialog.tsx | 2 +- src/locales/en.json | 33 +- src/tests/packages/excalidraw.test.tsx | 11 +- 21 files changed, 881 insertions(+), 171 deletions(-) create mode 100644 src/components/ImageExportDialog.scss create mode 100644 src/components/RadioGroup.scss create mode 100644 src/components/RadioGroup.tsx create mode 100644 src/components/Switch.scss create mode 100644 src/components/Switch.tsx diff --git a/src/actions/actionExport.tsx b/src/actions/actionExport.tsx index f142eac8..d945ba95 100644 --- a/src/actions/actionExport.tsx +++ b/src/actions/actionExport.tsx @@ -26,7 +26,7 @@ export const actionChangeProjectName = register({ perform: (_elements, appState, value) => { return { appState: { ...appState, name: value }, commitToHistory: false }; }, - PanelComponent: ({ appState, updateData, appProps }) => ( + PanelComponent: ({ appState, updateData, appProps, data }) => ( ), }); diff --git a/src/actions/manager.tsx b/src/actions/manager.tsx index 60648e41..e52e91da 100644 --- a/src/actions/manager.tsx +++ b/src/actions/manager.tsx @@ -118,10 +118,13 @@ export class ActionManager { return true; } - executeAction(action: Action, source: ActionSource = "api") { + executeAction( + action: Action, + source: ActionSource = "api", + value: any = null, + ) { const elements = this.getElementsIncludingDeleted(); const appState = this.getAppState(); - const value = null; trackAction(action, source, appState, elements, this.app, value); diff --git a/src/components/ConfirmDialog.tsx b/src/components/ConfirmDialog.tsx index a0f257e3..9061fefa 100644 --- a/src/components/ConfirmDialog.tsx +++ b/src/components/ConfirmDialog.tsx @@ -31,7 +31,7 @@ const ConfirmDialog = (props: Props) => { return ( diff --git a/src/components/Dialog.scss b/src/components/Dialog.scss index 604b3c64..405f9235 100644 --- a/src/components/Dialog.scss +++ b/src/components/Dialog.scss @@ -14,4 +14,33 @@ padding: 0 0 0.75rem; margin-bottom: 1.5rem; } + + .Dialog__close { + color: var(--color-gray-40); + margin: 0; + position: absolute; + top: 0.75rem; + right: 0.5rem; + border: 0; + background-color: transparent; + line-height: 0; + cursor: pointer; + + &:hover { + color: var(--color-gray-60); + } + &:active { + color: var(--color-gray-40); + } + + @include isMobile { + top: 1.25rem; + right: 1.25rem; + } + + svg { + width: 1.5rem; + height: 1.5rem; + } + } } diff --git a/src/components/Dialog.tsx b/src/components/Dialog.tsx index 363bb849..a76f65e4 100644 --- a/src/components/Dialog.tsx +++ b/src/components/Dialog.tsx @@ -21,9 +21,9 @@ import { jotaiScope } from "../jotai"; export interface DialogProps { children: React.ReactNode; className?: string; - small?: boolean; + size?: "small" | "regular" | "wide"; onCloseRequest(): void; - title: React.ReactNode; + title: React.ReactNode | false; autofocus?: boolean; theme?: AppState["theme"]; closeOnClickOutside?: boolean; @@ -33,6 +33,7 @@ export const Dialog = (props: DialogProps) => { const [islandNode, setIslandNode] = useCallbackRefState(); const [lastActiveElement] = useState(document.activeElement); const { id } = useExcalidrawContainer(); + const device = useDevice(); useEffect(() => { if (!islandNode) { @@ -86,23 +87,27 @@ export const Dialog = (props: DialogProps) => { -

- {props.title} - -

+ {props.title && ( +

+ {props.title} +

+ )} +
{props.children}
diff --git a/src/components/ErrorDialog.tsx b/src/components/ErrorDialog.tsx index 56c303c1..74d265f7 100644 --- a/src/components/ErrorDialog.tsx +++ b/src/components/ErrorDialog.tsx @@ -28,7 +28,7 @@ export const ErrorDialog = ({ <> {modalIsShown && ( diff --git a/src/components/ImageExportDialog.scss b/src/components/ImageExportDialog.scss new file mode 100644 index 00000000..a74cfdc2 --- /dev/null +++ b/src/components/ImageExportDialog.scss @@ -0,0 +1,215 @@ +@import "../css/variables.module"; + +.excalidraw { + --ImageExportModal-preview-border: #d6d6d6; + + &.theme--dark { + --ImageExportModal-preview-border: #5c5c5c; + } + + .ImageExportModal { + display: flex; + flex-direction: row; + justify-content: space-between; + + & h3 { + font-family: "Assistant"; + font-style: normal; + font-weight: 700; + font-size: 1.313rem; + line-height: 130%; + padding: 0; + margin: 0; + + @include isMobile { + display: none; + } + } + + & > h3 { + display: none; + + @include isMobile { + display: block; + } + } + + @include isMobile { + flex-direction: column; + height: calc(100vh - 5rem); + } + + &__preview { + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + height: 360px; + width: 55%; + + margin-right: 1.5rem; + + @include isMobile { + max-width: unset; + margin-right: unset; + + width: 100%; + height: unset; + flex-grow: 1; + } + + &__filename { + & > input { + margin-top: 1rem; + } + } + + &__canvas { + box-sizing: border-box; + width: 100%; + height: 100%; + display: flex; + flex-grow: 1; + justify-content: center; + align-items: center; + + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==") + left center; + + border: 1px solid var(--ImageExportModal-preview-border); + border-radius: 12px; + + overflow: hidden; + padding: 1rem; + + & > canvas { + max-width: calc(100% - 2rem); + max-height: calc(100% - 2rem); + + filter: none !important; + + @include isMobile { + max-height: 100%; + } + } + + @include isMobile { + margin-top: 24px; + max-width: unset; + } + } + } + + &__settings { + display: flex; + flex-direction: column; + flex-wrap: wrap; + gap: 18px; + + @include isMobile { + margin-left: unset; + margin-top: 1rem; + flex-direction: row; + gap: 6px 34px; + + align-content: flex-start; + } + + &__setting { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + @include isMobile { + flex-direction: column; + align-items: start; + justify-content: unset; + height: 52px; + } + + &__label { + display: flex; + flex-direction: row; + align-items: center; + + font-family: "Assistant"; + font-weight: 600; + font-size: 1rem; + line-height: 150%; + + & svg { + width: 20px; + height: 20px; + margin-left: 10px; + } + } + + &__content { + display: flex; + height: 100%; + align-items: center; + } + } + + &__buttons { + flex-grow: 1; + flex-wrap: wrap; + display: flex; + flex-direction: row; + gap: 11px; + + align-items: flex-end; + align-content: flex-end; + + @include isMobile { + padding-top: 32px; + flex-basis: 100%; + justify-content: center; + } + + &__button { + box-sizing: border-box; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 8px 16px; + flex-shrink: 0; + width: fit-content; + gap: 8px; + + height: 40px; + border: 0; + border-radius: 8px; + + user-select: none; + font-family: "Assistant"; + font-style: normal; + font-weight: 600; + font-size: 0.75rem; + line-height: 100%; + transition: 150ms ease-out; + transition-property: background, color; + + background: var(--color-primary); + color: var(--color-icon-white); + + &:hover { + background: var(--color-primary-darker); + color: var(--color-icon-white); + } + + &:active { + background: var(--color-primary-darkest); + } + + & > svg { + width: 20px; + height: 20px; + } + } + } + } + } +} diff --git a/src/components/ImageExportDialog.tsx b/src/components/ImageExportDialog.tsx index 38467d6e..604f50b4 100644 --- a/src/components/ImageExportDialog.tsx +++ b/src/components/ImageExportDialog.tsx @@ -1,25 +1,39 @@ import React, { useEffect, useRef, useState } from "react"; + +import type { ActionManager } from "../actions/manager"; +import type { AppClassProperties, BinaryFiles, UIAppState } from "../types"; + +import { + actionExportWithDarkMode, + actionChangeExportBackground, + actionChangeExportEmbedScene, + actionChangeExportScale, + actionChangeProjectName, +} from "../actions/actionExport"; import { probablySupportsClipboardBlob } from "../clipboard"; -import { canvasToBlob } from "../data/blob"; -import { NonDeletedExcalidrawElement } from "../element/types"; -import { t } from "../i18n"; -import { getSelectedElements, isSomeElementSelected } from "../scene"; -import { AppClassProperties, BinaryFiles, UIAppState } from "../types"; -import { Dialog } from "./Dialog"; -import { clipboard } from "./icons"; -import Stack from "./Stack"; -import OpenColor from "open-color"; -import { CheckboxItem } from "./CheckboxItem"; import { DEFAULT_EXPORT_PADDING, EXPORT_IMAGE_TYPES, isFirefox, + EXPORT_SCALES, } from "../constants"; + +import { canvasToBlob } from "../data/blob"; import { nativeFileSystemSupported } from "../data/filesystem"; -import { ActionManager } from "../actions/manager"; +import { NonDeletedExcalidrawElement } from "../element/types"; +import { t } from "../i18n"; +import { getSelectedElements, isSomeElementSelected } from "../scene"; import { exportToCanvas } from "../packages/utils"; -import "./ExportDialog.scss"; +import { copyIcon, downloadIcon, helpIcon } from "./icons"; +import { Button } from "./Button"; +import { Dialog } from "./Dialog"; +import { RadioGroup } from "./RadioGroup"; +import { Switch } from "./Switch"; +import { Tooltip } from "./Tooltip"; + +import "./ImageExportDialog.scss"; +import { useAppProps } from "./App"; const supportsContextFilters = "filter" in document.createElement("canvas").getContext("2d")!; @@ -36,50 +50,36 @@ export const ErrorCanvasPreview = () => { ); }; -export type ExportCB = ( - elements: readonly NonDeletedExcalidrawElement[], - scale?: number, -) => void; - -const ExportButton: React.FC<{ - color: keyof OpenColor; - onClick: () => void; - title: string; - shade?: number; - children?: React.ReactNode; -}> = ({ children, title, onClick, color, shade = 6 }) => { - return ( - - ); -}; - -const ImageExportModal = ({ - elements, - appState, - files, - actionManager, - onExportImage, -}: { +type ImageExportModalProps = { appState: UIAppState; elements: readonly NonDeletedExcalidrawElement[]; files: BinaryFiles; actionManager: ActionManager; onExportImage: AppClassProperties["onExportImage"]; -}) => { +}; + +const ImageExportModal = ({ + appState, + elements, + files, + actionManager, + onExportImage, +}: ImageExportModalProps) => { + const appProps = useAppProps(); + const [projectName, setProjectName] = useState(appState.name); + const someElementIsSelected = isSomeElementSelected(elements, appState); + const [exportSelected, setExportSelected] = useState(someElementIsSelected); + const [exportWithBackground, setExportWithBackground] = useState( + appState.exportBackground, + ); + const [exportDarkMode, setExportDarkMode] = useState( + appState.exportWithDarkMode, + ); + const [embedScene, setEmbedScene] = useState(appState.exportEmbedScene); + const [exportScale, setExportScale] = useState(appState.exportScale); + const previewRef = useRef(null); const [renderError, setRenderError] = useState(null); @@ -93,6 +93,7 @@ const ImageExportModal = ({ return; } const maxWidth = previewNode.offsetWidth; + const maxHeight = previewNode.offsetHeight; if (!maxWidth) { return; } @@ -101,7 +102,7 @@ const ImageExportModal = ({ appState, files, exportPadding: DEFAULT_EXPORT_PADDING, - maxWidthOrHeight: maxWidth, + maxWidthOrHeight: Math.max(maxWidth, maxHeight), }) .then((canvas) => { setRenderError(null); @@ -118,89 +119,190 @@ const ImageExportModal = ({ }, [appState, files, exportedElements]); return ( -
-
- {renderError && } -
- {supportsContextFilters && - actionManager.renderAction("exportWithDarkMode")} -
-
- {actionManager.renderAction("changeExportBackground")} - {someElementIsSelected && ( - setExportSelected(checked)} - > - {t("labels.onlySelected")} - +
+

{t("imageExportDialog.header")}

+
+
+ {renderError && } +
+
+ {!nativeFileSystemSupported && ( + { + setProjectName(event.target.value); + actionManager.executeAction( + actionChangeProjectName, + "ui", + event.target.value, + ); + }} + /> )} - {actionManager.renderAction("changeExportEmbedScene")}
-
- - {actionManager.renderAction("changeExportScale")} - -

- {t("buttons.scale")} -

-
-
- {!nativeFileSystemSupported && - actionManager.renderAction("changeProjectName")} -
- - - onExportImage(EXPORT_IMAGE_TYPES.png, exportedElements) - } - > - PNG - - - onExportImage(EXPORT_IMAGE_TYPES.svg, exportedElements) - } - > - SVG - - {/* firefox supports clipboard API under a flag, - so let's throw and tell people what they can do */} - {(probablySupportsClipboardBlob || isFirefox) && ( - - onExportImage(EXPORT_IMAGE_TYPES.clipboard, exportedElements) - } - color="gray" - shade={7} +
+

{t("imageExportDialog.header")}

+ {someElementIsSelected && ( + - {clipboard} - + { + setExportSelected(checked); + }} + /> + )} - + + { + setExportWithBackground(checked); + actionManager.executeAction( + actionChangeExportBackground, + "ui", + checked, + ); + }} + /> + + {supportsContextFilters && ( + + { + setExportDarkMode(checked); + actionManager.executeAction( + actionExportWithDarkMode, + "ui", + checked, + ); + }} + /> + + )} + + { + setEmbedScene(checked); + actionManager.executeAction( + actionChangeExportEmbedScene, + "ui", + checked, + ); + }} + /> + + + { + setExportScale(scale); + actionManager.executeAction(actionChangeExportScale, "ui", scale); + }} + choices={EXPORT_SCALES.map((scale) => ({ + value: scale, + label: `${scale}\u00d7`, + }))} + /> + + +
+ + + {(probablySupportsClipboardBlob || isFirefox) && ( + + )} +
+
+
+ ); +}; + +type ExportSettingProps = { + label: string; + children: React.ReactNode; + tooltip?: string; + name?: string; +}; + +const ExportSetting = ({ + label, + children, + tooltip, + name, +}: ExportSettingProps) => { + return ( +
+ +
+ {children} +
); }; @@ -225,7 +327,7 @@ export const ImageExportDialog = ({ } return ( - + setPublishLibSuccess(null)} title={t("publishSuccessDialog.title")} className="publish-library-success" - small={true} + size="small" >

+ />
void; label: string; isNameEditable: boolean; + ignoreFocus?: boolean; }; export const ProjectName = (props: Props) => { @@ -19,7 +20,9 @@ export const ProjectName = (props: Props) => { const [fileName, setFileName] = useState(props.value); const handleBlur = (event: any) => { - focusNearestParent(event.target); + if (!props.ignoreFocus) { + focusNearestParent(event.target); + } const value = event.target.value; if (value !== props.value) { props.onChange(value); diff --git a/src/components/RadioGroup.scss b/src/components/RadioGroup.scss new file mode 100644 index 00000000..86064826 --- /dev/null +++ b/src/components/RadioGroup.scss @@ -0,0 +1,100 @@ +@import "../css/variables.module"; + +.excalidraw { + --RadioGroup-background: #ffffff; + --RadioGroup-border: var(--color-gray-30); + + --RadioGroup-choice-color-off: var(--color-primary); + --RadioGroup-choice-color-off-hover: var(--color-primary-darkest); + --RadioGroup-choice-background-off: white; + --RadioGroup-choice-background-off-active: var(--color-gray-20); + + --RadioGroup-choice-color-on: white; + --RadioGroup-choice-background-on: var(--color-primary); + --RadioGroup-choice-background-on-hover: var(--color-primary-darker); + --RadioGroup-choice-background-on-active: var(--color-primary-darkest); + + &.theme--dark { + --RadioGroup-background: var(--color-gray-85); + --RadioGroup-border: var(--color-gray-70); + + --RadioGroup-choice-background-off: var(--color-gray-85); + --RadioGroup-choice-background-off-active: var(--color-gray-70); + --RadioGroup-choice-color-on: var(--color-gray-85); + } + + .RadioGroup { + box-sizing: border-box; + display: flex; + flex-direction: row; + align-items: flex-start; + + padding: 3px; + border-radius: 10px; + + background: var(--RadioGroup-background); + border: 1px solid var(--RadioGroup-border); + + &__choice { + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 24px; + + color: var(--RadioGroup-choice-color-off); + background: var(--RadioGroup-choice-background-off); + + border-radius: 8px; + + font-family: "Assistant"; + font-style: normal; + font-weight: 600; + font-size: 0.75rem; + line-height: 100%; + user-select: none; + letter-spacing: 0.4px; + + transition: all 75ms ease-out; + + &:hover { + color: var(--RadioGroup-choice-color-off-hover); + } + + &:active { + background: var(--RadioGroup-choice-background-off-active); + } + + &.active { + color: var(--RadioGroup-choice-color-on); + background: var(--RadioGroup-choice-background-on); + + &:hover { + background: var(--RadioGroup-choice-background-on-hover); + } + + &:active { + background: var(--RadioGroup-choice-background-on-active); + } + } + + & input { + z-index: 1; + position: absolute; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + + border-radius: 8px; + + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + + cursor: pointer; + } + } + } +} diff --git a/src/components/RadioGroup.tsx b/src/components/RadioGroup.tsx new file mode 100644 index 00000000..40c6551f --- /dev/null +++ b/src/components/RadioGroup.tsx @@ -0,0 +1,42 @@ +import clsx from "clsx"; +import "./RadioGroup.scss"; + +export type RadioGroupChoice = { + value: T; + label: string; +}; + +export type RadioGroupProps = { + choices: RadioGroupChoice[]; + value: T; + onChange: (value: T) => void; + name: string; +}; + +export const RadioGroup = function ({ + onChange, + value, + choices, + name, +}: RadioGroupProps) { + return ( +
+ {choices.map((choice) => ( +
+ onChange(choice.value)} + /> + {choice.label} +
+ ))} +
+ ); +}; diff --git a/src/components/Switch.scss b/src/components/Switch.scss new file mode 100644 index 00000000..ab98bad6 --- /dev/null +++ b/src/components/Switch.scss @@ -0,0 +1,116 @@ +@import "../css/variables.module"; + +.excalidraw { + --Switch-disabled-color: #d6d6d6; + --Switch-track-background: white; + --Switch-thumb-background: #3d3d3d; + + &.theme--dark { + --Switch-disabled-color: #5c5c5c; + --Switch-track-background: #242424; + --Switch-thumb-background: #b8b8b8; + } + + .Switch { + position: relative; + box-sizing: border-box; + + width: 40px; + height: 20px; + border-radius: 12px; + + transition-property: background, border; + transition-duration: 150ms; + transition-timing-function: ease-out; + + background: var(--Switch-track-background); + border: 1px solid var(--Switch-disabled-color); + + &:hover { + background: var(--Switch-track-background); + border: 1px solid #999999; + } + + &.toggled { + background: var(--color-primary); + border: 1px solid var(--color-primary); + + &:hover { + background: var(--color-primary-darker); + border: 1px solid var(--color-primary-darker); + } + } + + &.disabled { + background: var(--Switch-track-background); + border: 1px solid var(--Switch-disabled-color); + + &.toggled { + background: var(--Switch-disabled-color); + border: 1px solid var(--Switch-disabled-color); + } + } + + &:before { + content: ""; + box-sizing: border-box; + display: block; + pointer-events: none; + position: absolute; + + border-radius: 100%; + transition: all 150ms ease-out; + + width: 10px; + height: 10px; + top: 4px; + left: 4px; + + background: var(--Switch-thumb-background); + } + + &:active:before { + width: 12px; + } + + &.toggled:before { + width: 14px; + height: 14px; + left: 22px; + top: 2px; + + background: var(--Switch-track-background); + } + + &.toggled:active:before { + width: 16px; + left: 20px; + } + + &.disabled:before { + background: var(--Switch-disabled-color); + } + + &.disabled.toggled:before { + background: var(--color-gray-50); + } + + & input { + width: 100%; + height: 100%; + margin: 0; + + border-radius: 12px; + + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + + cursor: pointer; + + &:disabled { + cursor: unset; + } + } + } +} diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx new file mode 100644 index 00000000..dfbf332f --- /dev/null +++ b/src/components/Switch.tsx @@ -0,0 +1,38 @@ +import clsx from "clsx"; + +import "./Switch.scss"; + +export type SwitchProps = { + name: string; + checked: boolean; + title?: string; + onChange: (value: boolean) => void; + disabled?: boolean; +}; + +export const Switch = ({ + title, + name, + checked, + onChange, + disabled = false, +}: SwitchProps) => { + return ( +
+ onChange(!checked)} + onKeyDown={(e) => { + if (e.key === " ") { + onChange(!checked); + } + }} + /> +
+ ); +}; diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 784e8102..3841d030 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -1550,3 +1550,32 @@ export const handIcon = createIcon( , tablerIconProps, ); + +export const downloadIcon = createIcon( + <> + + + + + , + tablerIconProps, +); + +export const copyIcon = createIcon( + <> + + + + , + tablerIconProps, +); + +export const helpIcon = createIcon( + <> + + + + + , + tablerIconProps, +); diff --git a/src/excalidraw-app/collab/RoomDialog.tsx b/src/excalidraw-app/collab/RoomDialog.tsx index 4810b5a5..05772774 100644 --- a/src/excalidraw-app/collab/RoomDialog.tsx +++ b/src/excalidraw-app/collab/RoomDialog.tsx @@ -180,7 +180,7 @@ const RoomDialog = ({ }; return ( ", () => { toggleMenu(container); fireEvent.click(queryByTestId(container, "image-export-button")!); const textInput: HTMLInputElement | null = document.querySelector( - ".ExportDialog .ProjectName .TextInput", + ".ImageExportModal .ImageExportModal__preview__filename .TextInput", ); expect(textInput?.value).toContain(`${t("labels.untitled")}`); expect(textInput?.nodeName).toBe("INPUT"); @@ -303,10 +303,11 @@ describe("", () => { toggleMenu(container); await fireEvent.click(queryByTestId(container, "image-export-button")!); const textInput = document.querySelector( - ".ExportDialog .ProjectName .TextInput--readonly", - ); - expect(textInput?.textContent).toEqual(name); - expect(textInput?.nodeName).toBe("SPAN"); + ".ImageExportModal .ImageExportModal__preview__filename .TextInput", + ) as HTMLInputElement; + expect(textInput?.value).toEqual(name); + expect(textInput?.nodeName).toBe("INPUT"); + expect(textInput?.disabled).toBe(true); }); });