Repository: inovex/scrumlr.io (ts)

Issue:
Line breaks in note text are not saved
Also, the text element is <blockquote /> which seems wrong?
Pressing Enter in the note details deletes the note as author/moderator/owner
Voting buttons stay active
The "Add Vote" and "Remove Vote" buttons stay active until they lose focus, e.g. until the user clicks somewhere else.
 
<img width="316" alt="image" src="https://user-images.githubusercontent.com/32651718/173255517-3899c461-9c21-4cdc-8eef-2fb2976afddb.png">

<img width="307" alt="image" src="https://user-images.githubusercontent.com/32651718/173255540-d6d458fc-0737-4c1b-b1b6-5f79405245fc.png">


Pull request description:
Enhance hover & focus styles for Voting-, NoteDialog- and MenuBar- buttons
Closes #1736, closes #1802, closes #1805

- Unify appearance of all NoteDialog buttons
- Make hover & focus styles visually distinguishable for all NoteDialog buttons
- Replace `<blockquote>` with `<textarea>`
- Add focus style to the action bar buttons
- Update tests & snapshots

![Screenshot 2022-06-22 at 14 04 28](https://user-images.githubusercontent.com/36969812/175025407-e9d24cc1-e0fd-49a4-a12c-bfc32b8c6d2a.png)



Patch:
diff --git a/src/components/DotButton/DotButton.scss b/src/components/DotButton/DotButton.scss
index 8c02fc25..69a08fc3 100644
--- a/src/components/DotButton/DotButton.scss
+++ b/src/components/DotButton/DotButton.scss
@@ -3,7 +3,6 @@
 .dot-button {
   border-radius: 100%;
   border: none;
-  transition: all 0.15s ease-in-out;
   padding: 0;
   font-size: $text-size--medium;
   line-height: 28px;
diff --git a/src/components/MenuBars/MenuBars.scss b/src/components/MenuBars/MenuBars.scss
index c38c5cc3..6c1cfccd 100644
--- a/src/components/MenuBars/MenuBars.scss
+++ b/src/components/MenuBars/MenuBars.scss
@@ -113,11 +113,13 @@
 
 .user-menu {
   --primaryColor: #0057ff;
+  --primaryColor-rgb: 0, 87, 255;
   --secondaryColor: #253f71;
 }
 
 .admin-menu {
   --primaryColor: #ff0069;
+  --primaryColor-rgb: 255, 0, 105;
   --secondaryColor: #7c123e;
 }
 
diff --git a/src/components/MenuBars/MenuItem/MenuItem.scss b/src/components/MenuBars/MenuItem/MenuItem.scss
index b22ccffc..efb07d31 100644
--- a/src/components/MenuBars/MenuItem/MenuItem.scss
+++ b/src/components/MenuBars/MenuItem/MenuItem.scss
@@ -11,10 +11,6 @@ $number-of-menu-items: 4;
   background: transparent;
 }
 
-.menu-item:focus {
-  outline: none;
-}
-
 .menu-item {
   background-color: transparent;
   border: none;
@@ -25,6 +21,12 @@ $number-of-menu-items: 4;
   width: 36px;
   cursor: pointer;
 }
+.menu-item:focus {
+  outline: 2px solid rgba(var(--primaryColor-rgb), 0.5);
+}
+[theme="dark"] .menu-item:focus {
+  outline: 2px solid $color-white;
+}
 
 @include collapsedColumns(".menu-bars") {
   $menu-item__first-position-left: 28px;
diff --git a/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogNoteContent.scss b/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogNoteContent.scss
index 886e70d9..94e7b7cf 100644
--- a/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogNoteContent.scss
+++ b/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogNoteContent.scss
@@ -17,15 +17,17 @@
   line-height: $line-height--medium;
   margin-bottom: $margin--small;
   padding: $padding--small;
-  border: 1px dashed transparent;
+  border: 2px dashed transparent;
   border-radius: 5px;
-  transition: border-color 0.5s ease-in-out;
+  transition: border-color 150ms ease-in-out;
   outline: none;
+  resize: none;
 }
-
-.note-dialog__note-content__text-hover:hover,
-.note-dialog__note-content__text-hover:focus {
-  border-color: $color-middle-gray;
+.note-dialog__note-content__text:enabled {
+  &:hover,
+  &:focus {
+    border-color: rgba(var(--accent-color-rgb), 0.3);
+  }
 }
 
 [theme="dark"] {
diff --git a/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogNoteContent.tsx b/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogNoteContent.tsx
index 80a498f8..f162f90f 100644
--- a/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogNoteContent.tsx
+++ b/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogNoteContent.tsx
@@ -1,9 +1,9 @@
 import classNames from "classnames";
-import React, {FC} from "react";
+import {FC} from "react";
 import {Actions} from "store/action";
 import "./NoteDialogNoteContent.scss";
 import {useDispatch} from "react-redux";
-import {Participant} from "../../../../types/participant";
+import {Participant} from "types/participant";
 
 type NoteDialogNoteContentProps = {
   noteId?: string;
@@ -16,7 +16,7 @@ export const NoteDialogNoteContent: FC<NoteDialogNoteContentProps> = ({noteId, a
   const dispatch = useDispatch();
   const editable = viewer.user.id === authorId || viewer.role === "OWNER" || viewer.role === "MODERATOR";
 
-  const onEdit = (id: string, editorId: string, newText: string) => {
+  const onEdit = (id: string, newText: string) => {
     if (editable && newText !== text) {
       dispatch(Actions.editNote(id, {text: newText}));
     }
@@ -24,16 +24,22 @@ export const NoteDialogNoteContent: FC<NoteDialogNoteContentProps> = ({noteId, a
 
   return (
     <div className="note-dialog__note-content">
-      <blockquote
+      <textarea
+        tabIndex={editable ? 0 : -1}
         className={classNames("note-dialog__note-content__text", {".note-dialog__note-content__text-hover": editable})}
-        contentEditable={editable}
+        disabled={!editable}
         suppressContentEditableWarning
-        onBlur={(e: React.FocusEvent<HTMLElement>) => {
-          onEdit(noteId!, authorId, e.target.textContent as string);
+        defaultValue={text}
+        onBlur={(e) => {
+          onEdit(noteId!, e.target.value ?? "");
         }}
-      >
-        {text}
-      </blockquote>
+        onKeyDown={(e) => {
+          if (e.key === "Enter") {
+            e.preventDefault();
+            (e.target as HTMLTextAreaElement).blur();
+          }
+        }}
+      />
     </div>
   );
 };
diff --git a/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogOptions/NoteDialogDeleteNoteButton.scss b/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogOptions/NoteDialogDeleteNoteButton.scss
index 2b33c84a..91eb4526 100644
--- a/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogOptions/NoteDialogDeleteNoteButton.scss
+++ b/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogOptions/NoteDialogDeleteNoteButton.scss
@@ -2,18 +2,25 @@
 @import "./NoteDialogNoteOptions.scss";
 
 .note-dialog__note-option__remove {
-  height: $option-height;
-  width: $option-width;
-  background-color: transparent;
-  color: rgba(var(--accent-color-rgb), 0.4);
+  height: $icon--large;
+  width: $icon--large;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background-color: rgba(var(--accent-color-rgb), 0.2);
+  color: var(--accent-color);
+  transition: background-color 150ms ease-in-out, color 150ms ease-in-out;
 }
-
-.note-dialog__note-option__remove-icon {
-  height: $option-height;
-  width: $option-width;
+.note-dialog__note-option__remove:hover {
+  background-color: var(--accent-color);
+  color: $color-white;
+}
+.note-dialog__note-option__remove:focus {
+  outline: 2px solid rgba(var(--accent-color-rgb), 0.5);
 }
 
-.note-dialog__note-option__remove:focus,
-.note-dialog__note-option__remove:hover {
-  color: var(--accent-color);
+.note-dialog__note-option__remove-icon {
+  color: inherit;
+  height: $icon--medium;
+  width: $icon--medium;
 }
diff --git a/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogOptions/NoteDialogNoteOptions.scss b/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogOptions/NoteDialogNoteOptions.scss
index 9adb4389..093b7ff4 100644
--- a/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogOptions/NoteDialogNoteOptions.scss
+++ b/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogOptions/NoteDialogNoteOptions.scss
@@ -1,19 +1,16 @@
 @import "src/constants/style";
 
-$option-height: 22px;
-$option-width: 22px;
-
 .note-dialog__note-options {
   list-style-type: none;
   position: relative;
   display: inline-flex;
-  height: 26px;
+  height: $icon--large;
   width: fit-content;
   padding: 0;
   margin: 0;
   justify-content: space-between;
   align-items: flex-end;
-  gap: $margin--default;
+  gap: 4px;
 }
 
 .note-dialog__note-option {
diff --git a/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogOptions/NoteDialogUnstackNoteButton.scss b/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogOptions/NoteDialogUnstackNoteButton.scss
index ade17376..34414ac1 100644
--- a/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogOptions/NoteDialogUnstackNoteButton.scss
+++ b/src/components/NoteDialog/NoteDialogComponents/NoteDialogNoteComponents/NoteDialogOptions/NoteDialogUnstackNoteButton.scss
@@ -2,18 +2,24 @@
 @import "./NoteDialogNoteOptions.scss";
 
 .note-dialog__note-option__unstack {
-  height: $option-height;
-  width: $option-width;
-  background-color: transparent;
-  color: rgba(var(--accent-color-rgb), 0.4);
+  height: $icon--large;
+  width: $icon--large;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background-color: rgba(var(--accent-color-rgb), 0.2);
+  color: var(--accent-color);
+  transition: background-color 150ms ease-in-out, color 150ms ease-in-out;
 }
-
-.note-dialog__note-option__unstack-icon {
-  height: $option-height;
-  width: $option-width;
+.note-dialog__note-option__unstack:hover {
+  background-color: var(--accent-color);
+  color: $color-white;
+}
+.note-dialog__note-option__unstack:focus {
+  outline: 2px solid rgba(var(--accent-color-rgb), 0.5);
 }
 
-.note-dialog__note-option__unstack:focus,
-.note-dialog__note-option__unstack:hover {
-  color: var(--accent-color);
+.note-dialog__note-option__unstack-icon {
+  height: $icon--medium;
+  width: $icon--medium;
 }
diff --git a/src/components/Portal/Portal.tsx b/src/components/Portal/Portal.tsx
index 19ac663f..1a1b1968 100644
--- a/src/components/Portal/Portal.tsx
+++ b/src/components/Portal/Portal.tsx
@@ -33,7 +33,7 @@ export const Portal: FC<PortalProps> = ({onClose, hiddenOverflow, centered, disa
 
   return ReactDOM.createPortal(
     <div className={classNames("portal", className)} onClick={() => onClose?.()} role="dialog" {...otherProps}>
-      <FocusLock>
+      <FocusLock autoFocus={false}>
         <div
           className={classNames(
             "portal__frame",
diff --git a/src/components/Votes/VoteButtons/AddVoteButton.scss b/src/components/Votes/VoteButtons/AddVoteButton.scss
index 2b087e3f..3cfe69d3 100644
--- a/src/components/Votes/VoteButtons/AddVoteButton.scss
+++ b/src/components/Votes/VoteButtons/AddVoteButton.scss
@@ -1,40 +1,37 @@
 @import "src/constants/style";
-@import "./VoteButton.scss";
 
 .vote-button-add {
   display: flex;
   justify-content: center;
   align-items: center;
 
-  height: $vote-button-height;
-  width: $vote-button-width;
+  height: $icon--large;
+  width: $icon--large;
 
-  background-color: rgba(var(--accent-color-rgb), 0.1);
+  background-color: rgba(var(--accent-color-rgb), 0.2);
   color: var(--accent-color);
+  transition: background-color 150ms ease-in-out, color 150ms ease-in-out;
+}
+.vote-button-add:disabled {
+  opacity: 0.5;
 }
 
 .vote-button-add__icon {
-  height: $vote-button-icon-height;
-  width: $vote-button-icon-width;
-  color: var(--accent-color);
+  height: $icon--medium;
+  width: $icon--medium;
+  color: inherit;
   pointer-events: none;
 }
-
-.vote-button-add:not([disabled]):focus,
-.vote-button-add:not([disabled]):hover {
+.vote-button-add:enabled:hover {
   background-color: var(--accent-color);
-
-  .vote-button-add__icon {
-    color: $color-white;
-  }
+  color: $color-white;
+}
+.vote-button-add:focus {
+  outline: 2px solid rgba(var(--accent-color-rgb), 0.5);
 }
 
 [theme="dark"] {
   .vote-button-add {
     background-color: rgba(var(--accent-color-rgb), 0.25);
   }
-
-  .vote-button-add__icon {
-    filter: brightness(1.7);
-  }
 }
diff --git a/src/components/Votes/VoteButtons/AddVoteButton.tsx b/src/components/Votes/VoteButtons/AddVoteButton.tsx
index 0d1b66b7..32538a3a 100644
--- a/src/components/Votes/VoteButtons/AddVoteButton.tsx
+++ b/src/components/Votes/VoteButtons/AddVoteButton.tsx
@@ -1,10 +1,9 @@
+import {FC} from "react";
+import {useDispatch} from "react-redux";
 import {Actions} from "store/action";
 import {DotButton} from "components/DotButton";
-import "./AddVoteButton.scss";
-import "./VoteButton.scss";
 import {ReactComponent as PlusIcon} from "assets/icon-add.svg";
-import {FC} from "react";
-import {useDispatch} from "react-redux";
+import "./AddVoteButton.scss";
 
 type AddVoteProps = {
   noteId: string;
diff --git a/src/components/Votes/VoteButtons/RemoveVoteButton.scss b/src/components/Votes/VoteButtons/RemoveVoteButton.scss
index 5e2ab40e..894a7988 100644
--- a/src/components/Votes/VoteButtons/RemoveVoteButton.scss
+++ b/src/components/Votes/VoteButtons/RemoveVoteButton.scss
@@ -1,28 +1,26 @@
 @import "src/constants/style";
-@import "./VoteButton.scss";
 
 .vote-button-remove {
   position: relative;
-  height: $vote-button-height;
-  width: $vote-button-width;
+  height: $icon--large;
+  width: $icon--large;
+  overflow: hidden;
   background-color: rgba(var(--accent-color-rgb), 0.2);
   color: var(--accent-color);
+  transition: background-color 150ms ease-in-out, color 150ms ease-in-out;
 }
-
-.vote-button-remove:not([disabled]) {
-  background-color: var(--accent-color);
-  color: $color-white;
-}
-
-.vote-button-remove:not([disabled]):focus > .vote-button-remove__folded-corner,
-.vote-button-remove:not([disabled]):hover > .vote-button-remove__folded-corner {
-  border-width: 12px 0 0 12px;
+.vote-button-remove:disabled {
+  cursor: default;
 }
-.vote-button-remove:not([disabled]):active {
+.vote-button-remove:active {
   transform: none;
 }
-.vote-button-remove:disabled {
-  cursor: default;
+.vote-button-remove:enabled:hover {
+  background-color: var(--accent-color);
+  color: $color-white;
+}
+.vote-button-remove:focus {
+  outline: 2px solid rgba(var(--accent-color-rgb), 0.5);
 }
 
 .vote-button-remove__folded-corner {
@@ -39,6 +37,10 @@
   border-color: $color-white $color-white transparent transparent;
   transition: border-width 0.15s ease-in-out;
 }
+.vote-button-remove:enabled:hover > .vote-button-remove__folded-corner {
+  border-width: 12px 0 0 12px;
+}
+
 [theme="dark"] {
   .vote-button-remove__folded-corner {
     box-shadow: 2px -2px 0px $color-dark-mode-note, 0 1px 1px rgba(0, 0, 0, 0.1), -1px 1px 1px rgba(0, 0, 0, 0.1);
diff --git a/src/components/Votes/VoteButtons/VoteButton.scss b/src/components/Votes/VoteButtons/VoteButton.scss
deleted file mode 100644
index 7c8c5db6..00000000
--- a/src/components/Votes/VoteButtons/VoteButton.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-$vote-button-height: 28px;
-$vote-button-width: 28px;
-
-$vote-button-icon-height: 18px;
-$vote-button-icon-width: 18px;
diff --git a/src/components/Votes/Votes.scss b/src/components/Votes/Votes.scss
index fa2c46c8..87699523 100644
--- a/src/components/Votes/Votes.scss
+++ b/src/components/Votes/Votes.scss
@@ -3,9 +3,9 @@
 .votes {
   display: inline-flex;
   position: relative;
-  width: 60px;
-  height: 28px;
+  height: $icon--large;
   justify-content: flex-end;
+  gap: 4px;
 }
 
 .votes > * + * {


Test patch:
diff --git a/src/components/Note/__tests__/__snapshots__/Note.test.tsx.snap b/src/components/Note/__tests__/__snapshots__/Note.test.tsx.snap
index 1ab8343c..609065c0 100644
--- a/src/components/Note/__tests__/__snapshots__/Note.test.tsx.snap
+++ b/src/components/Note/__tests__/__snapshots__/Note.test.tsx.snap
@@ -540,12 +540,13 @@ exports[`Note Test NoteDialog created/not created NoteDialog is present: snapsho
                   <div
                     class="note-dialog__note-content"
                   >
-                    <blockquote
+                    <textarea
                       class="note-dialog__note-content__text"
-                      contenteditable="false"
+                      disabled=""
+                      tabindex="-1"
                     >
                       Lorem Ipsum
-                    </blockquote>
+                    </textarea>
                   </div>
                   <div
                     class="note-dialog__note-footer"
diff --git a/src/components/NoteDialog/__tests__/NoteDialog.test.tsx b/src/components/NoteDialog/__tests__/NoteDialog.test.tsx
index b7d9d098..d8cea4f3 100644
--- a/src/components/NoteDialog/__tests__/NoteDialog.test.tsx
+++ b/src/components/NoteDialog/__tests__/NoteDialog.test.tsx
@@ -126,7 +126,7 @@ describe("<NoteDialog/>", () => {
         useDispatchSpy.mockReturnValue(mockDispatchFn);
         const {container} = render(createNoteDialog({authorId: "test-participant-id"}), {container: global.document.querySelector("#portal")!});
         const noteContentText = container.querySelector(".note-dialog__note-content__text")!;
-        fireEvent.blur(noteContentText, {target: {textContent: "Changed Text"}});
+        fireEvent.blur(noteContentText, {target: {value: "Changed Text"}});
 
         await waitFor(() => {
           expect(mockDispatchFn).toHaveBeenCalledWith(Actions.editNote("0", {text: "Changed Text"}));
@@ -139,7 +139,7 @@ describe("<NoteDialog/>", () => {
         useDispatchSpy.mockReturnValue(mockDispatchFn);
         const {container} = render(createNoteDialog({currentUserIsModerator: true}), {container: global.document.querySelector("#portal")!});
         const noteContentText = container.querySelector(".note-dialog__note-content__text")!;
-        fireEvent.blur(noteContentText, {target: {textContent: "Changed Text"}});
+        fireEvent.blur(noteContentText, {target: {value: "Changed Text"}});
 
         await waitFor(() => {
           expect(mockDispatchFn).toHaveBeenCalledWith(Actions.editNote("0", {text: "Changed Text"}));
diff --git a/src/components/NoteDialog/__tests__/__snapshots__/NoteDialog.test.tsx.snap b/src/components/NoteDialog/__tests__/__snapshots__/NoteDialog.test.tsx.snap
index 2073109a..44ad266d 100644
--- a/src/components/NoteDialog/__tests__/__snapshots__/NoteDialog.test.tsx.snap
+++ b/src/components/NoteDialog/__tests__/__snapshots__/NoteDialog.test.tsx.snap
@@ -19,12 +19,13 @@ exports[`<NoteDialog/> should render correctly note-dialog match snapshot 1`] =
         <div
           class="note-dialog__note-content"
         >
-          <blockquote
+          <textarea
             class="note-dialog__note-content__text"
-            contenteditable="false"
+            disabled=""
+            tabindex="-1"
           >
             Test Text
-          </blockquote>
+          </textarea>
         </div>
         <div
           class="note-dialog__note-footer"
@@ -524,9 +525,10 @@ exports[`<NoteDialog/> should render correctly note-dialog match snapshot 1`] =
         <div
           class="note-dialog__note-content"
         >
-          <blockquote
+          <textarea
             class="note-dialog__note-content__text"
-            contenteditable="false"
+            disabled=""
+            tabindex="-1"
           />
         </div>
         <div
@@ -968,9 +970,10 @@ exports[`<NoteDialog/> should render correctly note-dialog match snapshot 1`] =
         <div
           class="note-dialog__note-content"
         >
-          <blockquote
+          <textarea
             class="note-dialog__note-content__text"
-            contenteditable="false"
+            disabled=""
+            tabindex="-1"
           />
         </div>
         <div