From 3a75eb12265ac83b9d88549b1b7f593905e5912d Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 10 May 2021 14:39:10 +0100
Subject: [PATCH] Fix handling of enter/return in space creation menu

---
 .../views/spaces/SpaceBasicSettings.tsx       | 50 ++++++++-------
 .../views/spaces/SpaceCreateMenu.tsx          | 61 ++++++++++++++++---
 src/i18n/strings/en_EN.json                   |  1 +
 3 files changed, 83 insertions(+), 29 deletions(-)

diff --git a/src/components/views/spaces/SpaceBasicSettings.tsx b/src/components/views/spaces/SpaceBasicSettings.tsx
index bc378ab956..ec40f7bed8 100644
--- a/src/components/views/spaces/SpaceBasicSettings.tsx
+++ b/src/components/views/spaces/SpaceBasicSettings.tsx
@@ -32,17 +32,11 @@ interface IProps {
     setTopic(topic: string): void;
 }
 
-const SpaceBasicSettings = ({
+export const SpaceAvatar = ({
     avatarUrl,
     avatarDisabled = false,
     setAvatar,
-    name = "",
-    nameDisabled = false,
-    setName,
-    topic = "",
-    topicDisabled = false,
-    setTopic,
-}: IProps) => {
+}: Pick<IProps, "avatarUrl" | "avatarDisabled" | "setAvatar">) => {
     const avatarUploadRef = useRef<HTMLInputElement>();
     const [avatar, setAvatarDataUrl] = useState(avatarUrl); // avatar data url cache
 
@@ -81,20 +75,34 @@ const SpaceBasicSettings = ({
         }
     }
 
+    return <div className="mx_SpaceBasicSettings_avatarContainer">
+        { avatarSection }
+        <input type="file" ref={avatarUploadRef} onChange={(e) => {
+            if (!e.target.files?.length) return;
+            const file = e.target.files[0];
+            setAvatar(file);
+            const reader = new FileReader();
+            reader.onload = (ev) => {
+                setAvatarDataUrl(ev.target.result as string);
+            };
+            reader.readAsDataURL(file);
+        }} accept="image/*" />
+    </div>;
+};
+
+const SpaceBasicSettings = ({
+    avatarUrl,
+    avatarDisabled = false,
+    setAvatar,
+    name = "",
+    nameDisabled = false,
+    setName,
+    topic = "",
+    topicDisabled = false,
+    setTopic,
+}: IProps) => {
     return <div className="mx_SpaceBasicSettings">
-        <div className="mx_SpaceBasicSettings_avatarContainer">
-            { avatarSection }
-            <input type="file" ref={avatarUploadRef} onChange={(e) => {
-                if (!e.target.files?.length) return;
-                const file = e.target.files[0];
-                setAvatar(file);
-                const reader = new FileReader();
-                reader.onload = (ev) => {
-                    setAvatarDataUrl(ev.target.result as string);
-                };
-                reader.readAsDataURL(file);
-            }} accept="image/*" />
-        </div>
+        <SpaceAvatar avatarUrl={avatarUrl} avatarDisabled={avatarDisabled} setAvatar={setAvatar} />
 
         <Field
             name="spaceName"
diff --git a/src/components/views/spaces/SpaceCreateMenu.tsx b/src/components/views/spaces/SpaceCreateMenu.tsx
index 9c42b9c7c4..491a9bffee 100644
--- a/src/components/views/spaces/SpaceCreateMenu.tsx
+++ b/src/components/views/spaces/SpaceCreateMenu.tsx
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React, {useContext, useState} from "react";
+import React, {useContext, useRef, useState} from "react";
 import classNames from "classnames";
 import {EventType, RoomType, RoomCreateTypeField} from "matrix-js-sdk/src/@types/event";
 
@@ -23,9 +23,11 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
 import {ChevronFace, ContextMenu} from "../../structures/ContextMenu";
 import createRoom, {IStateEvent, Preset} from "../../../createRoom";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
-import SpaceBasicSettings from "./SpaceBasicSettings";
+import {SpaceAvatar} from "./SpaceBasicSettings";
 import AccessibleButton from "../elements/AccessibleButton";
 import FocusLock from "react-focus-lock";
+import Field from "../elements/Field";
+import withValidation from "../elements/Validation";
 
 const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
     return (
@@ -41,17 +43,39 @@ enum Visibility {
     Private,
 }
 
+const spaceNameValidator = withValidation({
+    rules: [
+        {
+            key: "required",
+            test: async ({ value }) => !!value,
+            invalid: () => _t("Please enter a name for the space"),
+        },
+    ],
+});
+
 const SpaceCreateMenu = ({ onFinished }) => {
     const cli = useContext(MatrixClientContext);
     const [visibility, setVisibility] = useState<Visibility>(null);
-    const [name, setName] = useState("");
-    const [avatar, setAvatar] = useState<File>(null);
-    const [topic, setTopic] = useState<string>("");
     const [busy, setBusy] = useState<boolean>(false);
 
-    const onSpaceCreateClick = async () => {
+    const [name, setName] = useState("");
+    const spaceNameField = useRef<Field>();
+    const [avatar, setAvatar] = useState<File>(null);
+    const [topic, setTopic] = useState<string>("");
+
+    const onSpaceCreateClick = async (e) => {
+        e.preventDefault();
         if (busy) return;
+
         setBusy(true);
+        // require & validate the space name field
+        if (!await spaceNameField.current.validate({ allowEmpty: false })) {
+            spaceNameField.current.focus();
+            spaceNameField.current.validate({ allowEmpty: false, focused: true });
+            setBusy(false);
+            return;
+        }
+
         const initialState: IStateEvent[] = [
             {
                 type: EventType.RoomHistoryVisibility,
@@ -146,9 +170,30 @@ const SpaceCreateMenu = ({ onFinished }) => {
                 }
             </p>
 
-            <SpaceBasicSettings setAvatar={setAvatar} name={name} setName={setName} topic={topic} setTopic={setTopic} />
+            <form className="mx_SpaceBasicSettings" onSubmit={onSpaceCreateClick}>
+                <SpaceAvatar setAvatar={setAvatar} />
 
-            <AccessibleButton kind="primary" onClick={onSpaceCreateClick} disabled={!name || busy}>
+                <Field
+                    name="spaceName"
+                    label={_t("Name")}
+                    autoFocus={true}
+                    value={name}
+                    onChange={ev => setName(ev.target.value)}
+                    ref={spaceNameField}
+                    onValidate={spaceNameValidator}
+                />
+
+                <Field
+                    name="spaceTopic"
+                    element="textarea"
+                    label={_t("Description")}
+                    value={topic}
+                    onChange={ev => setTopic(ev.target.value)}
+                    rows={3}
+                />
+            </form>
+
+            <AccessibleButton kind="primary" onClick={onSpaceCreateClick} disabled={busy}>
                 { busy ? _t("Creating...") : _t("Create") }
             </AccessibleButton>
         </React.Fragment>;
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index dcad970300..26275c4325 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -996,6 +996,7 @@
     "Upload": "Upload",
     "Name": "Name",
     "Description": "Description",
+    "Please enter a name for the space": "Please enter a name for the space",
     "Create a space": "Create a space",
     "Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.": "Spaces are new ways to group rooms and people. To join an existing space you'll need an invite.",
     "Public": "Public",