Repository: inovex/scrumlr.io
Pull request #1431 (2022-02-07 10:53:09)

Dialogs
## Description

This pull request is going to replace the dropdown buttons for the timer- and vote-configuration with routable dialogs. 

## Changelog

- Add Dialog component
- Add VotingDialog component
- Add TimerDialog component
- Delete Vote- and TimerConfiguration from the MenuBars variants
- Add and update tests

## Checklist

- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] The light- and dark-theme are both supported and tested
- [x] The design was implemented and is responsive for all devices and screen sizes
- [x] The application was tested in the most commonly used browsers (e.g. Chrome, Firefox, Safari)

## (Optional) Visual Changes

[//]: <> (If available, please provide a before and after screenshot of your UI related changes.)



Patch:
diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json
index 228e44dd..88efe372 100644
--- a/public/locales/de/translation.json
+++ b/public/locales/de/translation.json
@@ -150,9 +150,9 @@
     "showVotesOfOthers": "Anonyme Abstimmung",
     "allowMultipleVotesPerNote": "Erlaube Stimmhäufung",
     "numberOfVotes": "Anzahl Stimmen",
-    "startTimer": "Abstimmung starten",
-    "cancelTimer": "Abstimmung abbrechen",
-    "stopTimer": "Abstimmung beenden"
+    "startVoting": "Abstimmung starten",
+    "cancelVoting": "Abstimmung abbrechen",
+    "stopVoting": "Abstimmung beenden"
   },
   "VoteDisplay": {
     "tooltip": "{{remaining}} von {{total}} Stimmen verbleiben"
diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
index 6ffa03d2..fb25d403 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -150,9 +150,9 @@
     "showVotesOfOthers": "Anonymous voting",
     "allowMultipleVotesPerNote": "Allow cumulative voting",
     "numberOfVotes": "Number of votes",
-    "startTimer": "Start voting phase",
-    "cancelTimer": "Cancel voting phase",
-    "stopTimer": "End voting phase"
+    "startVoting": "Start voting phase",
+    "cancelVoting": "Cancel voting phase",
+    "stopVoting": "End voting phase"
   },
   "VoteDisplay": {
     "tooltip": "{{remaining}} of {{total}} votes remaining"
diff --git a/src/assets/icon-five.svg b/src/assets/icon-five.svg
new file mode 100644
index 00000000..265f9373
--- /dev/null
+++ b/src/assets/icon-five.svg
@@ -0,0 +1,8 @@
+<svg id="ic_five" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
+  <g id="Ellipse_32" data-name="Ellipse 32" transform="translate(4 4)" fill="none" stroke="#ff0069" stroke-width="2">
+    <circle cx="12" cy="12" r="12" stroke="none"/>
+    <circle cx="12" cy="12" r="11" fill="none"/>
+  </g>
+  <path id="Pfad_3998" data-name="Pfad 3998" d="M3.31.971A3.429,3.429,0,0,1,1.592.541,2.92,2.92,0,0,1,.434-.623l.822-.784a2.63,2.63,0,0,0,.9.853,2.3,2.3,0,0,0,1.17.318,1.863,1.863,0,0,0,.909-.218,1.615,1.615,0,0,0,.629-.61,1.749,1.749,0,0,0,.23-.9,1.7,1.7,0,0,0-.224-.878,1.556,1.556,0,0,0-.6-.585,1.754,1.754,0,0,0-.859-.205,1.9,1.9,0,0,0-.884.212,1.791,1.791,0,0,0-.672.6H.646L1.654-7.994H5.975v1.258H2.65L2.2-4.321a1.569,1.569,0,0,1,.573-.367A2.215,2.215,0,0,1,3.6-4.831a2.985,2.985,0,0,1,1.488.367,2.755,2.755,0,0,1,1.033,1A2.807,2.807,0,0,1,6.5-1.992a2.828,2.828,0,0,1-.423,1.55A2.918,2.918,0,0,1,4.929.6,3.471,3.471,0,0,1,3.31.971Z" transform="translate(12.543 19.551)" fill="#ff0069"/>
+  <rect id="BG" width="32" height="32" fill="rgba(255,255,255,0)"/>
+</svg>
diff --git a/src/assets/icon-minus.svg b/src/assets/icon-minus.svg
new file mode 100644
index 00000000..5befe4ac
--- /dev/null
+++ b/src/assets/icon-minus.svg
@@ -0,0 +1,11 @@
+<svg id="ic_minus" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
+  <rect id="BG" width="32" height="32" fill="rgba(255,255,255,0)"/>
+  <g id="Ellipse_32" data-name="Ellipse 32" transform="translate(4 4)" fill="none" stroke="#ff0069" stroke-width="2">
+    <circle cx="12" cy="12" r="12" stroke="none"/>
+    <circle cx="12" cy="12" r="11" fill="none"/>
+  </g>
+  <g id="Rechteck_573" data-name="Rechteck 573" transform="translate(12 15)" fill="#fff" stroke="#ff0069" stroke-width="1">
+    <rect width="8" height="2" rx="1" stroke="none"/>
+    <rect x="0.5" y="0.5" width="7" height="1" rx="0.5" fill="none"/>
+  </g>
+</svg>
diff --git a/src/assets/icon-one.svg b/src/assets/icon-one.svg
new file mode 100644
index 00000000..ee7f6bc7
--- /dev/null
+++ b/src/assets/icon-one.svg
@@ -0,0 +1,8 @@
+<svg id="ic_one" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
+  <rect id="BG" width="32" height="32" fill="rgba(255,255,255,0)"/>
+  <g id="Ellipse_32" data-name="Ellipse 32" transform="translate(4 4)" fill="none" stroke="#ff0069" stroke-width="2">
+    <circle cx="12" cy="12" r="12" stroke="none"/>
+    <circle cx="12" cy="12" r="11" fill="none"/>
+  </g>
+  <path id="Pfad_3996" data-name="Pfad 3996" d="M6.966-.554v1.56H.945V-.554H3.183v-5.5a1.867,1.867,0,0,1-.441.394,5.348,5.348,0,0,1-.67.386,4.689,4.689,0,0,1-.749.292,2.494,2.494,0,0,1-.694.11V-6.5a1.769,1.769,0,0,0,.725-.189,6.54,6.54,0,0,0,.812-.449,6.3,6.3,0,0,0,.686-.5,1.354,1.354,0,0,0,.331-.355H4.965v7.44Z" transform="translate(12.27 19.494)" fill="#ff0069"/>
+</svg>
diff --git a/src/assets/icon-plus.svg b/src/assets/icon-plus.svg
new file mode 100644
index 00000000..4eefe185
--- /dev/null
+++ b/src/assets/icon-plus.svg
@@ -0,0 +1,15 @@
+<svg id="ic_plus" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
+  <rect id="BG" width="32" height="32" fill="rgba(255,255,255,0)"/>
+  <g id="Ellipse_32" data-name="Ellipse 32" transform="translate(4 4)" fill="none" stroke="#ff0069" stroke-width="2">
+    <circle cx="12" cy="12" r="12" stroke="none"/>
+    <circle cx="12" cy="12" r="11" fill="none"/>
+  </g>
+  <g id="Rechteck_573" data-name="Rechteck 573" transform="translate(12 15)" fill="#fff" stroke="#ff0069" stroke-width="1">
+    <rect width="8" height="2" rx="1" stroke="none"/>
+    <rect x="0.5" y="0.5" width="7" height="1" rx="0.5" fill="none"/>
+  </g>
+  <g id="Rechteck_574" data-name="Rechteck 574" transform="translate(17 12) rotate(90)" fill="#ff0069" stroke="#ff0069" stroke-width="1">
+    <rect width="8" height="2" rx="1" stroke="none"/>
+    <rect x="0.5" y="0.5" width="7" height="1" rx="0.5" fill="none"/>
+  </g>
+</svg>
diff --git a/src/assets/icon-three.svg b/src/assets/icon-three.svg
new file mode 100644
index 00000000..a9a82867
--- /dev/null
+++ b/src/assets/icon-three.svg
@@ -0,0 +1,8 @@
+<svg id="ic_three" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
+  <g id="Ellipse_32" data-name="Ellipse 32" transform="translate(4 4)" fill="none" stroke="#ff0069" stroke-width="2">
+    <circle cx="12" cy="12" r="12" stroke="none"/>
+    <circle cx="12" cy="12" r="11" fill="none"/>
+  </g>
+  <path id="Pfad_3997" data-name="Pfad 3997" d="M4.652-3.823a2,2,0,0,1,.917.393,2.019,2.019,0,0,1,.6.773A2.525,2.525,0,0,1,6.386-1.6a2.381,2.381,0,0,1-.38,1.347A2.5,2.5,0,0,1,4.926.637,3.943,3.943,0,0,1,3.28.955,3.984,3.984,0,0,1,1.608.624,2.811,2.811,0,0,1,.448-.33l.836-.948a2.244,2.244,0,0,0,.773.717,2.408,2.408,0,0,0,1.2.268,2.009,2.009,0,0,0,1.26-.349A1.245,1.245,0,0,0,4.964-1.69a1.4,1.4,0,0,0-.493-1.135,2.263,2.263,0,0,0-1.478-.412H2.656V-4.309H3.03A1.892,1.892,0,0,0,4.315-4.7a1.261,1.261,0,0,0,.449-.992,1.108,1.108,0,0,0-.181-.655,1.071,1.071,0,0,0-.505-.38,2.076,2.076,0,0,0-.748-.125,2.246,2.246,0,0,0-1.123.274,2.009,2.009,0,0,0-.773.786L.573-6.729a2.38,2.38,0,0,1,.686-.7,3.544,3.544,0,0,1,1-.468,4.211,4.211,0,0,1,1.2-.168,3.565,3.565,0,0,1,1.447.274,2.227,2.227,0,0,1,.967.773,2.019,2.019,0,0,1,.343,1.173,2.274,2.274,0,0,1-.187.917,1.885,1.885,0,0,1-.549.723A1.832,1.832,0,0,1,4.652-3.823Z" transform="translate(12.569 19.552)" fill="#ff0069"/>
+  <rect id="BG" width="32" height="32" fill="rgba(255,255,255,0)"/>
+</svg>
diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx
index 5d65d0d3..5fe87ed6 100644
--- a/src/components/Board/Board.tsx
+++ b/src/components/Board/Board.tsx
@@ -6,6 +6,7 @@ import {ReactComponent as LeftArrowIcon} from "assets/icon-arrow-previous.svg";
 import {MenuBars} from "components/MenuBars";
 import {BoardHeader} from "components/BoardHeader";
 import "./Board.scss";
+import {Outlet} from "react-router-dom";
 
 export interface BoardProps {
   children: React.ReactElement<ColumnProps> | React.ReactElement<ColumnProps>[];
@@ -152,6 +153,7 @@ export const BoardComponent = ({children, currentUserIsModerator}: BoardProps) =
 
       <BoardHeader currentUserIsModerator={currentUserIsModerator} />
       <MenuBars />
+      <Outlet />
 
       {state.showPreviousButton && (
         <button className={`board__navigation board__navigation-prev ${getColorClassName(columnColors[previousColumnIndex])}`} onClick={handlePreviousClick} aria-hidden>
diff --git a/src/components/Dialog/Dialog.scss b/src/components/Dialog/Dialog.scss
new file mode 100644
index 00000000..69f96f6c
--- /dev/null
+++ b/src/components/Dialog/Dialog.scss
@@ -0,0 +1,102 @@
+@import "constants/style";
+
+.dialog {
+  height: 100%;
+  width: 100%;
+  top: 0;
+  left: 0;
+  box-sizing: border-box;
+  position: fixed;
+  border: 3px solid $color-planning-pink;
+  background: $color-white-one;
+  box-shadow: 2.8px 2.8px 2.2px rgba(0, 0, 0, 0.02), 6.7px 6.7px 5.3px rgba(0, 0, 0, 0.028), 12.5px 12.5px 10px rgba(0, 0, 0, 0.035), 22.3px 22.3px 17.9px rgba(0, 0, 0, 0.042),
+    41.8px 41.8px 33.4px rgba(0, 0, 0, 0.05), 100px 100px 80px rgba(0, 0, 0, 0.07);
+}
+[theme="dark"] .dialog {
+  background: $color-dark-one;
+}
+@media screen and (min-width: 600px) {
+  .dialog {
+    width: 400px;
+    height: auto;
+    border-radius: 8px;
+  }
+}
+
+.dialog__content {
+  margin: 40px 0;
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 16px;
+}
+
+.dialog__header-text {
+  border-bottom: solid 3px $color-planning-pink;
+  padding-bottom: 8px;
+  margin: 0;
+}
+[theme="dark"] {
+  .dialog__header-text {
+    color: $color-white;
+  }
+}
+
+.dialog__close-button {
+  position: absolute;
+  top: 0;
+  right: 0;
+  color: $color-planning-pink;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 44px;
+  height: 44px;
+  cursor: pointer;
+  border: none;
+  padding: 0;
+  background-color: transparent;
+}
+@media screen and (min-width: 600px) {
+  .dialog__close-button {
+    display: none;
+  }
+}
+.close-button__icon {
+  width: 44px;
+  height: 44px;
+}
+
+.dialog__button {
+  position: relative;
+  border: none;
+  border-radius: 8px;
+  height: 48px;
+  width: 80%;
+  background: $color-white;
+  cursor: pointer;
+}
+.dialog__button:focus {
+  outline: none;
+}
+.dialog__button > label:first-of-type {
+  font-size: 14px;
+  color: $color-black;
+  letter-spacing: 0.49px;
+  position: absolute;
+  left: 24px;
+  top: 16px;
+  cursor: pointer;
+}
+[theme="dark"] {
+  .dialog__button {
+    background-color: $color-dark-mode-note;
+  }
+  .dialog__button:hover {
+    background-color: $color-dark-mode-note--hover;
+  }
+  .dialog__button > label {
+    color: white;
+  }
+}
diff --git a/src/components/Dialog/Dialog.tsx b/src/components/Dialog/Dialog.tsx
new file mode 100644
index 00000000..4b82dd06
--- /dev/null
+++ b/src/components/Dialog/Dialog.tsx
@@ -0,0 +1,24 @@
+import {FC, HTMLAttributes} from "react";
+import classNames from "classnames";
+import {Portal} from "components/Portal";
+import {ReactComponent as CloseIcon} from "assets/icon-close.svg";
+import "./Dialog.scss";
+
+type DialogProps = {
+  title: string;
+  onClose?: () => void;
+} & HTMLAttributes<HTMLDivElement>;
+
+export const Dialog: FC<DialogProps> = ({title, className, onClose, children, ...other}) => (
+  <Portal onClose={() => onClose?.()}>
+    <aside className={classNames("dialog", className)} {...other}>
+      <article className="dialog__content">
+        <h2 className="dialog__header-text">{title}</h2>
+        {children}
+      </article>
+      <button onClick={() => onClose?.()} className="dialog__close-button">
+        <CloseIcon className="close-button__icon" />
+      </button>
+    </aside>
+  </Portal>
+);
diff --git a/src/components/Dialog/index.ts b/src/components/Dialog/index.ts
new file mode 100644
index 00000000..24cc75a7
--- /dev/null
+++ b/src/components/Dialog/index.ts
@@ -0,0 +1,1 @@
+export * from "./Dialog";
diff --git a/src/components/MenuBars/MenuBars.tsx b/src/components/MenuBars/MenuBars.tsx
index 7afce1b6..b4d9918c 100644
--- a/src/components/MenuBars/MenuBars.tsx
+++ b/src/components/MenuBars/MenuBars.tsx
@@ -1,10 +1,13 @@
 import React, {useState} from "react";
+import {useNavigate} from "react-router";
 import {ActionFactory} from "store/action";
 import store, {useAppSelector} from "store";
 import Parse from "parse";
 import _ from "underscore";
 import classNames from "classnames";
-import {MenuToggle} from "components/MenuBars/MenuItem";
+import {MenuToggle, MenuButton} from "components/MenuBars/MenuItem";
+import {ReactComponent as VoteIcon} from "assets/icon-vote.svg";
+import {ReactComponent as TimerIcon} from "assets/icon-timer.svg";
 import {ReactComponent as RaiseHand} from "assets/icon-hand.svg";
 import {ReactComponent as CheckIcon} from "assets/icon-check.svg";
 import {ReactComponent as FocusIcon} from "assets/icon-focus.svg";
@@ -12,14 +15,13 @@ import {ReactComponent as ToggleSettingsMenuIcon} from "assets/icon-toggle-setti
 import {ReactComponent as ToggleAddMenuIcon} from "assets/icon-toggle-add-menu.svg";
 import {TabIndex} from "constants/tabIndex";
 import {useTranslation} from "react-i18next";
-import {TimerToggleButton} from "./MenuItem/variants/TimerToggleButton";
-import {VoteConfigurationButton} from "./MenuItem/variants/VoteConfigurationButton";
 import {ThemeToggleButton} from "./MenuItem/variants/ThemeToggleButton";
 
 import "./MenuBars.scss";
 
 export const MenuBars = () => {
   const {t} = useTranslation();
+  const navigate = useNavigate();
 
   const [showAdminMenu, toggleMenus] = useState(false);
   const [animate, setAnimate] = useState(false);
@@ -87,8 +89,8 @@ export const MenuBars = () => {
       {isAdmin && (
         <section className={classNames("menu", "admin-menu", {"menu-animation": animate})} onTransitionEnd={(event) => handleAnimate(event)}>
           <div className="menu__items">
-            <TimerToggleButton tabIndex={TabIndex.AdminMenu} />
-            <VoteConfigurationButton tabIndex={TabIndex.AdminMenu + 8} />
+            <MenuButton direction="left" label="Timer" onClick={() => navigate("timer")} icon={TimerIcon} />
+            <MenuButton direction="left" label="Voting" onClick={() => navigate("voting")} icon={VoteIcon} />
             <MenuToggle
               value={state.moderation === "active"}
               direction="left"
diff --git a/src/components/MenuBars/MenuItem/DropdownToggleButton.scss b/src/components/MenuBars/MenuItem/DropdownToggleButton.scss
deleted file mode 100644
index e4bda22d..00000000
--- a/src/components/MenuBars/MenuItem/DropdownToggleButton.scss
+++ /dev/null
@@ -1,77 +0,0 @@
-@import "constants/style.scss";
-
-$dropdown__box-shadow: var(--primaryColor) 3 d;
-
-.dropdown-item--open {
-  .menu-item__icon {
-    color: $color-white;
-    background-color: var(--secondaryColor);
-  }
-}
-
-.dropdown-item--open .dropdown {
-  transition-delay: 0.25s;
-}
-
-@include collapsedColumns(".menu-bars") {
-  .dropdown-item--open,
-  .menu-item--touch-hover.dropdown-item--open,
-  .menu-item.dropdown-item--open:enabled:hover {
-    .menu-item__tooltip {
-      transition: all 0.25s 0s ease-out;
-      width: 245px;
-      min-width: 245px;
-      opacity: 1;
-      visibility: visible;
-      border-top-left-radius: unset;
-      border-top-right-radius: unset;
-    }
-  }
-
-  .dropdown {
-    bottom: 94px;
-    left: 18px;
-    transform: translateX(-50%);
-    border-radius: 16px 16px 0 0;
-    box-shadow: 0 8px 24px $dropdown__box-shadow;
-  }
-
-  @media screen and (max-width: 450px) {
-    .menu-item:nth-child(1) {
-      .dropdown {
-        transform: translateX(-25%);
-      }
-    }
-    .menu-item:nth-child(4) {
-      .dropdown {
-        transform: translateX(-75%);
-      }
-    }
-  }
-}
-
-@include visibleColumns(".menu-bars") {
-  .dropdown-item--open .menu-item__tooltip,
-  .menu-item.dropdown-item--open:focus .menu-item__tooltip,
-  .menu-item.dropdown-item--open:hover .menu-item__tooltip {
-    width: 300px;
-    max-width: 384px;
-  }
-
-  .dropdown {
-    border-bottom-left-radius: 16px;
-    border-bottom-right-radius: 16px;
-  }
-
-  .admin-menu .dropdown {
-    top: 36px;
-    left: -265px;
-    box-shadow: 0 8px 24px #ff00693d;
-  }
-
-  .user-menu .dropdown {
-    top: 36px;
-    right: -265px;
-    box-shadow: 0 8px 24px #0057ff3d;
-  }
-}
diff --git a/src/components/MenuBars/MenuItem/DropdownToggleButton.tsx b/src/components/MenuBars/MenuItem/DropdownToggleButton.tsx
deleted file mode 100644
index c0976051..00000000
--- a/src/components/MenuBars/MenuItem/DropdownToggleButton.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import React, {useEffect, useState} from "react";
-import classNames from "classnames";
-import "./MenuItem.scss";
-import "./DropdownToggleButton.scss";
-import {TabIndex} from "constants/tabIndex";
-
-type DropdownButtonProps = {
-  direction: "left" | "right";
-  label: string;
-  icon: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
-  disabled?: boolean;
-  setTabable: React.Dispatch<React.SetStateAction<boolean>>;
-  tabIndex?: number;
-  active?: boolean;
-};
-
-export const DropdownToggleButton: React.FC<DropdownButtonProps> = (props) => {
-  const [touchHover, setTouchHover] = useState(false);
-  const [showDropdown, setShowDropdown] = useState(false);
-  const Icon = props.icon;
-
-  useEffect(() => {
-    if (showDropdown) {
-      window.addEventListener("click", () => setShowDropdown(false), {once: true});
-    }
-    props.setTabable(showDropdown);
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [showDropdown]);
-
-  return (
-    <button
-      className={classNames("menu-item", `menu-item--${props.direction}`, {"dropdown-item--open": showDropdown}, {"menu-item--touch-hover": touchHover})}
-      onClick={() => {
-        setShowDropdown((prev) => !prev);
-      }}
-      onTouchEnd={(e) => {
-        if (!touchHover && document.getElementsByClassName("menu-item--touch-hover").length === 0) {
-          e.preventDefault();
-          window.addEventListener("click", () => setTouchHover(false), {once: true});
-          setTouchHover(true);
-        }
-        if (touchHover) {
-          e.preventDefault();
-          setTouchHover(false);
-          setShowDropdown(true);
-        }
-      }}
-      tabIndex={props.tabIndex ?? TabIndex.default}
-      disabled={props.disabled}
-    >
-      <div className="menu-item__tooltip">
-        <span className="tooltip__text">{props.label}</span>
-      </div>
-      <Icon className={classNames("menu-item__icon", "menu-item__icon--start", {"menu-item-active": props.active})} />
-      {props.children}
-    </button>
-  );
-};
diff --git a/src/components/MenuBars/MenuItem/variants/TimerToggleButton.scss b/src/components/MenuBars/MenuItem/variants/TimerToggleButton.scss
deleted file mode 100644
index 0d3c694f..00000000
--- a/src/components/MenuBars/MenuItem/variants/TimerToggleButton.scss
+++ /dev/null
@@ -1,80 +0,0 @@
-@import "constants/style.scss";
-
-$timer-dropdown__color-light: $color-black;
-$timer-dropdown__color-dark: $color-white;
-
-.timer-dropdown__item-button label {
-  text-align: left;
-  letter-spacing: 0.49px;
-  font-size: 14px;
-  color: $timer-dropdown__color-light;
-  opacity: 1;
-  position: absolute;
-  left: 19px;
-  top: 16px;
-  outline: 0;
-}
-[theme="dark"] {
-  .timer-dropdown__item-button label {
-    color: $timer-dropdown__color-dark;
-  }
-}
-
-.timer-dropdown__item-button div,
-.timer-dropdown__item-button > button > button {
-  display: inline-block;
-  height: 24px;
-  width: 24px;
-
-  border: 2px solid var(--primaryColor);
-  border-radius: 12px;
-  background-color: transparent;
-
-  position: absolute;
-  top: 12px;
-  right: 16px;
-
-  color: var(--primaryColor);
-  font-size: 14px;
-  line-height: 20px;
-  font-weight: 600;
-}
-.timer-dropdown__item-button > button > button {
-  cursor: pointer;
-  font-size: 20px;
-}
-.timer-dropdown__item-button > button > button:last-child {
-  right: 72px;
-}
-
-.timer-dropdown__item-button > * > input {
-  width: 24px;
-  height: 24px;
-  background-color: transparent;
-  border: none;
-  color: $timer-dropdown__color-light;
-  border-bottom: 2px solid $timer-dropdown__color-light;
-  border-radius: 2px;
-  text-align: center;
-  position: absolute;
-  top: 12px;
-  right: 44px;
-}
-[theme="dark"] {
-  .timer-dropdown__item-button > * > input {
-    color: $timer-dropdown__color-dark;
-    border-bottom: 2px solid $timer-dropdown__color-dark;
-  }
-}
-.timer-dropdown__item-button > * > input:focus {
-  outline: none;
-}
-
-.timer-dropdown__time-button {
-  outline: none;
-}
-
-.timer-dropdown__time-button:focus,
-.timer-dropdown__time-button:hover {
-  transform: scale(1.2);
-}
diff --git a/src/components/MenuBars/MenuItem/variants/TimerToggleButton.tsx b/src/components/MenuBars/MenuItem/variants/TimerToggleButton.tsx
deleted file mode 100644
index a0569857..00000000
--- a/src/components/MenuBars/MenuItem/variants/TimerToggleButton.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import {useState} from "react";
-import store, {useAppSelector} from "store";
-import {ActionFactory} from "store/action";
-import Dropdown from "components/Dropdown";
-import {DropdownToggleButton} from "components/MenuBars/MenuItem/DropdownToggleButton";
-import {ReactComponent as TimerIcon} from "assets/icon-timer.svg";
-import "./TimerToggleButton.scss";
-import {useTranslation} from "react-i18next";
-import {TabIndex} from "constants/tabIndex";
-
-type TimerToggleButtonProps = {
-  tabIndex?: number;
-};
-
-export const TimerToggleButton = (props: TimerToggleButtonProps) => {
-  const {t} = useTranslation();
-
-  const timer = useAppSelector((state) => state.board.data?.timerUTCEndTime);
-  const [customTime, setCustomTime] = useState(10);
-  const [tabable, setTabable] = useState(false);
-
-  const onClick = (minutes: number) => {
-    store.dispatch(ActionFactory.setTimer(new Date(new Date().getTime() + minutes * 60 * 1000)));
-  };
-
-  const focusOnTab = (tabIndex: number) => {
-    if (tabable) {
-      return props.tabIndex ? props.tabIndex + tabIndex : TabIndex.default;
-    }
-    return TabIndex.disabled;
-  };
-
-  return (
-    <DropdownToggleButton
-      active={timer !== undefined}
-      tabIndex={props.tabIndex ?? TabIndex.default}
-      setTabable={setTabable}
-      direction="left"
-      label={t("TimerToggleButton.label")}
-      icon={TimerIcon}
-    >
-      <Dropdown className="timer__dropdown">
-        <Dropdown.Main>
-          <Dropdown.ItemButton tabIndex={focusOnTab(1)} className="timer-dropdown__item-button" onClick={() => onClick(1)} onTouchEnd={() => onClick(1)}>
-            <label>{t("TimerToggleButton.1min")}</label>
-            <div>1</div>
-          </Dropdown.ItemButton>
-          <Dropdown.ItemButton tabIndex={focusOnTab(2)} className="timer-dropdown__item-button" onClick={() => onClick(3)} onTouchEnd={() => onClick(3)}>
-            <label>{t("TimerToggleButton.3min")}</label>
-            <div>3</div>
-          </Dropdown.ItemButton>
-          <Dropdown.ItemButton tabIndex={focusOnTab(3)} className="timer-dropdown__item-button" onClick={() => onClick(5)} onTouchEnd={() => onClick(5)}>
-            <label>{t("TimerToggleButton.5min")}</label>
-            <div>5</div>
-          </Dropdown.ItemButton>
-          <Dropdown.ItemButton tabIndex={focusOnTab(4)} className="timer-dropdown__item-button" onClick={() => onClick(customTime)} onTouchEnd={() => onClick(customTime)}>
-            <label>{t("TimerToggleButton.customTime")}</label>
-            <button
-              onClick={(e) => {
-                e.stopPropagation();
-                setCustomTime((prev) => Math.min(++prev, 59));
-              }}
-              onTouchEnd={(e) => {
-                e.preventDefault();
-                e.stopPropagation();
-                setCustomTime((prev) => Math.min(++prev, 59));
-              }}
-              tabIndex={focusOnTab(7)}
-              className="timer-dropdown__time-button"
-            >
-              +
-            </button>
-            <input tabIndex={focusOnTab(6)} value={customTime} onClick={(e) => e.stopPropagation()} onChange={(e) => setCustomTime(Math.min(parseInt(e.target.value, 10), 59))} />
-            <button
-              onClick={(e) => {
-                e.stopPropagation();
-                setCustomTime((prev) => Math.max(--prev, 0));
-              }}
-              onTouchEnd={(e) => {
-                e.preventDefault();
-                e.stopPropagation();
-                setCustomTime((prev) => Math.max(--prev, 0));
-              }}
-              tabIndex={focusOnTab(5)}
-              className="timer-dropdown__time-button"
-            >
-              -
-            </button>
-          </Dropdown.ItemButton>
-        </Dropdown.Main>
-        {timer != null && (
-          <Dropdown.Footer>
-            <Dropdown.ItemButton
-              tabIndex={focusOnTab(8)}
-              className="timer-dropdown__item-button"
-              onClick={() => store.dispatch(ActionFactory.cancelTimer())}
-              onTouchEnd={() => store.dispatch(ActionFactory.cancelTimer())}
-            >
-              <label>{t("TimerToggleButton.cancelTimer")}</label>
-              <div>x</div>
-            </Dropdown.ItemButton>
-          </Dropdown.Footer>
-        )}
-      </Dropdown>
-    </DropdownToggleButton>
-  );
-};
diff --git a/src/components/MenuBars/MenuItem/variants/VoteConfigurationButton.scss b/src/components/MenuBars/MenuItem/variants/VoteConfigurationButton.scss
deleted file mode 100644
index e5a3a31d..00000000
--- a/src/components/MenuBars/MenuItem/variants/VoteConfigurationButton.scss
+++ /dev/null
@@ -1,89 +0,0 @@
-@import "constants/style.scss";
-
-$vote-dropdown__color-light: $color-black;
-$vote-dropdown__color-dark: $color-white;
-
-.vote-dropdown__item-button label {
-  text-align: left;
-  letter-spacing: 0.49px;
-  font-size: 14px;
-  color: $vote-dropdown__color-light;
-  opacity: 1;
-  position: absolute;
-  left: 19px;
-  top: 16px;
-  outline: 0;
-}
-[theme="dark"] {
-  .vote-dropdown__item-button label {
-    color: $vote-dropdown__color-dark;
-  }
-}
-.vote-dropdown__item-button div {
-  position: absolute;
-  top: 14px;
-  right: 16px;
-
-  color: var(--primaryColor);
-  font-size: 14px;
-  line-height: 20px;
-  font-weight: 600;
-}
-
-.vote-dropdown__item-button > button > button {
-  display: inline-block;
-  height: 24px;
-  width: 24px;
-
-  border: 2px solid var(--primaryColor);
-  border-radius: 12px;
-  background-color: transparent;
-
-  position: absolute;
-  top: 12px;
-  right: 16px;
-
-  color: var(--primaryColor);
-  font-size: 14px;
-  line-height: 20px;
-  font-weight: 600;
-}
-.vote-dropdown__item-button > button > button {
-  cursor: pointer;
-  font-size: 20px;
-}
-.vote-dropdown__item-button > button > button:last-child {
-  right: 72px;
-}
-
-.vote-dropdown__item-button > * > input {
-  width: 24px;
-  height: 24px;
-  background-color: transparent;
-  border: none;
-  color: $vote-dropdown__color-light;
-  border-bottom: 2px solid $vote-dropdown__color-light;
-  border-radius: 2px;
-  text-align: center;
-  position: absolute;
-  top: 12px;
-  right: 44px;
-}
-[theme="dark"] {
-  .vote-dropdown__item-button > * > input {
-    color: $vote-dropdown__color-dark;
-    border-bottom: 2px solid $vote-dropdown__color-dark;
-  }
-}
-.vote-dropdown__item-button > * > input:focus {
-  outline: none;
-}
-
-.vote-dropdown__vote-button {
-  outline: none;
-}
-
-.vote-dropdown__vote-button:focus,
-.vote-dropdown__vote-button:hover {
-  transform: scale(1.2);
-}
diff --git a/src/components/MenuBars/MenuItem/variants/VoteConfigurationButton.tsx b/src/components/MenuBars/MenuItem/variants/VoteConfigurationButton.tsx
deleted file mode 100644
index 713a3f63..00000000
--- a/src/components/MenuBars/MenuItem/variants/VoteConfigurationButton.tsx
+++ /dev/null
@@ -1,175 +0,0 @@
-import {useState, VFC} from "react";
-import {DropdownToggleButton} from "components/MenuBars/MenuItem/DropdownToggleButton";
-import {TabIndex} from "constants/tabIndex";
-import {ReactComponent as VoteIcon} from "assets/icon-vote.svg";
-import store, {useAppSelector} from "store";
-import Dropdown from "components/Dropdown";
-import "./VoteConfigurationButton.scss";
-import {ActionFactory} from "store/action";
-import {useTranslation} from "react-i18next";
-import {Toggle} from "../../../Toggle";
-
-type VoteConfigurationButtonProps = {
-  tabIndex?: number;
-};
-
-export const VoteConfigurationButton: VFC<VoteConfigurationButtonProps> = (props) => {
-  const {t} = useTranslation();
-  const [tabable, setTabable] = useState(false);
-  const [numberOfVotes, setNumberOfVotes] = useState(5);
-  const [allowMultipleVotesPerNote, setAllowMultipleVotesPerNote] = useState(true);
-  const [showVotesOfOthers, setShowVotesOfOthers] = useState(false);
-  const state = useAppSelector((rootState) => ({activeVoting: rootState.board.data?.voting === "active", boardId: rootState.board.data?.id}));
-
-  const focusOnTab = (tabIndex: number) => {
-    if (tabable) {
-      return props.tabIndex ? props.tabIndex + tabIndex : TabIndex.default;
-    }
-    return TabIndex.disabled;
-  };
-
-  const startVoting = () => {
-    store.dispatch(
-      ActionFactory.addVoteConfiguration({
-        boardId: state.boardId!,
-        voteLimit: numberOfVotes,
-        showVotesOfOtherUsers: showVotesOfOthers,
-        allowMultipleVotesPerNote,
-      })
-    );
-    store.dispatch(ActionFactory.editBoard({id: state.boardId!, voting: "active"}));
-  };
-  const stopVoting = () => {
-    store.dispatch(ActionFactory.editBoard({id: state.boardId!, voting: "disabled"}));
-  };
-  const cancelVoting = () => {
-    store.dispatch(ActionFactory.cancelVoting(state.boardId!));
-  };
-
-  return (
-    <DropdownToggleButton
-      active={state.activeVoting}
-      tabIndex={props.tabIndex ?? TabIndex.default}
-      setTabable={setTabable}
-      direction="left"
-      label={t("VoteConfigurationButton.label")}
-      icon={VoteIcon}
-    >
-      {state.activeVoting && (
-        <Dropdown className="vote-dropdown">
-          <Dropdown.Main>
-            <Dropdown.ItemButton
-              className="vote-dropdown__item-button"
-              onClick={() => {
-                stopVoting();
-              }}
-              onTouchEnd={() => {
-                stopVoting();
-              }}
-              tabIndex={focusOnTab(1)}
-            >
-              <label>{t("VoteConfigurationButton.stopTimer")}</label>
-            </Dropdown.ItemButton>
-            <Dropdown.ItemButton
-              className="vote-dropdown__item-button"
-              onClick={() => {
-                cancelVoting();
-              }}
-              onTouchEnd={() => {
-                cancelVoting();
-              }}
-              tabIndex={focusOnTab(2)}
-            >
-              <label>{t("VoteConfigurationButton.cancelTimer")}</label>
-            </Dropdown.ItemButton>
-          </Dropdown.Main>
-        </Dropdown>
-      )}
-      {!state.activeVoting && (
-        <Dropdown className="vote-dropdown">
-          <Dropdown.Main>
-            <Dropdown.ItemButton
-              className="vote-dropdown__item-button"
-              onClick={(e) => {
-                e.stopPropagation();
-                setAllowMultipleVotesPerNote((prev) => !prev);
-              }}
-              onTouchEnd={() => {
-                setAllowMultipleVotesPerNote((prev) => !prev);
-              }}
-              tabIndex={focusOnTab(1)}
-            >
-              <label>{t("VoteConfigurationButton.allowMultipleVotesPerNote")}</label>
-              <Toggle active={allowMultipleVotesPerNote} />
-            </Dropdown.ItemButton>
-            <Dropdown.ItemButton
-              className="vote-dropdown__item-button"
-              onClick={(e) => {
-                e.stopPropagation();
-                setShowVotesOfOthers((prev) => !prev);
-              }}
-              onTouchEnd={() => {
-                setShowVotesOfOthers((prev) => !prev);
-              }}
-              tabIndex={focusOnTab(2)}
-            >
-              <label>{t("VoteConfigurationButton.showVotesOfOthers")}</label>
-              <Toggle active={!showVotesOfOthers} />
-            </Dropdown.ItemButton>
-
-            <Dropdown.ItemButton
-              tabIndex={focusOnTab(3)}
-              className="vote-dropdown__item-button"
-              onClick={(e) => {
-                e.stopPropagation();
-              }}
-            >
-              <label>{t("VoteConfigurationButton.numberOfVotes")}</label>
-              <button
-                onClick={(e) => {
-                  e.stopPropagation();
-                  setNumberOfVotes((prev) => Math.min(++prev, 99));
-                }}
-                onTouchEnd={() => {
-                  setNumberOfVotes((prev) => Math.min(++prev, 99));
-                }}
-                tabIndex={focusOnTab(6)}
-                className="vote-dropdown__vote-button"
-              >
-                +
-              </button>
-              <input tabIndex={focusOnTab(5)} value={numberOfVotes} onClick={(e) => e.stopPropagation()} onChange={(e) => setNumberOfVotes(parseInt(e.target.value, 10))} />
-              <button
-                onClick={(e) => {
-                  e.stopPropagation();
-                  setNumberOfVotes((prev) => Math.max(--prev, 0));
-                }}
-                onTouchEnd={() => {
-                  setNumberOfVotes((prev) => Math.max(--prev, 0));
-                }}
-                tabIndex={focusOnTab(4)}
-                className="vote-dropdown__vote-button"
-              >
-                -
-              </button>
-            </Dropdown.ItemButton>
-          </Dropdown.Main>
-          <Dropdown.Footer>
-            <Dropdown.ItemButton
-              className="vote-dropdown__item-button"
-              onClick={() => {
-                startVoting();
-              }}
-              onTouchEnd={() => {
-                startVoting();
-              }}
-              tabIndex={focusOnTab(7)}
-            >
-              <label>{t("VoteConfigurationButton.startTimer")}</label>
-            </Dropdown.ItemButton>
-          </Dropdown.Footer>
-        </Dropdown>
-      )}
-    </DropdownToggleButton>
-  );
-};
diff --git a/src/components/TimerDialog/TimerDialog.scss b/src/components/TimerDialog/TimerDialog.scss
new file mode 100644
index 00000000..80817ac0
--- /dev/null
+++ b/src/components/TimerDialog/TimerDialog.scss
@@ -0,0 +1,108 @@
+@import "constants/style";
+
+@media screen and (min-width: 600px) {
+  #root[column-visibility="visible"] + #portal {
+    .timer-dialog {
+      top: calc(50% - 105px - 25px + 46px);
+      right: 100px;
+      left: auto;
+      bottom: auto;
+      height: auto;
+    }
+    .timer-dialog::before {
+      content: " ";
+      position: absolute;
+      left: 100%;
+      top: 10px;
+      border-width: 15px;
+      border-style: solid;
+      border-color: transparent transparent transparent $color-planning-pink;
+    }
+    .timer-dialog::after {
+      content: " ";
+      position: absolute;
+      left: 100%;
+      top: 15px;
+      border-width: 10px;
+      border-style: solid;
+      border-color: transparent transparent transparent $color-white-one;
+    }
+  }
+
+  [theme="dark"] #root[column-visibility="visible"] + #portal {
+    .timer-dialog::after {
+      border-color: transparent transparent transparent $color-dark-mode;
+    }
+  }
+
+  #root[column-visibility="collapsed"] + #portal {
+    .timer-dialog {
+      left: calc(50% - 139px + 28px + 18px - 200px);
+      bottom: 100px;
+      top: auto;
+      right: auto;
+      height: auto;
+    }
+    .timer-dialog::before {
+      content: " ";
+      position: absolute;
+      left: calc(50% - 15px);
+      top: 100%;
+      border-width: 15px;
+      border-style: solid;
+      border-color: $color-planning-pink transparent transparent transparent;
+    }
+    .timer-dialog::after {
+      content: " ";
+      position: absolute;
+      left: calc(50% - 11px);
+      top: 100%;
+      border-width: 11px;
+      border-style: solid;
+      border-color: $color-white-one transparent transparent transparent;
+    }
+  }
+  [theme="dark"] #root[column-visibility="collapsed"] + #portal {
+    .timer-dialog::after {
+      border-color: $color-dark-mode transparent transparent transparent;
+    }
+  }
+}
+
+.timer-dialog__time-button {
+  height: 32px;
+  width: 32px;
+  border: none;
+  border-radius: 16px;
+  background-color: transparent;
+  cursor: pointer;
+  position: absolute;
+  top: 8px;
+  padding: 0;
+}
+.timer-dialog__time-button:first-of-type {
+  right: 80px;
+}
+.timer-dialog__time-button:last-of-type {
+  right: 24px;
+}
+.timer-dialog__time-label {
+  position: absolute;
+  top: 8px;
+  right: 56px;
+  height: 32px;
+  width: 24px;
+  text-align: center;
+  line-height: 32px;
+  cursor: col-resize;
+  font-size: 14px;
+  user-select: none;
+  -webkit-touch-callout: none;
+  -webit-user-select: none;
+}
+
+.timer-dialog__button-icon {
+  position: absolute;
+  top: 8px;
+  right: 24px;
+}
diff --git a/src/components/TimerDialog/TimerDialog.tsx b/src/components/TimerDialog/TimerDialog.tsx
new file mode 100644
index 00000000..f32ce606
--- /dev/null
+++ b/src/components/TimerDialog/TimerDialog.tsx
@@ -0,0 +1,96 @@
+import {VFC, useState, useEffect} from "react";
+import {useTranslation} from "react-i18next";
+import {Dialog} from "components/Dialog";
+import {useNavigate} from "react-router-dom";
+import store, {useAppSelector} from "store";
+import {ActionFactory} from "store/action";
+import "./TimerDialog.scss";
+import {ReactComponent as PlusIcon} from "assets/icon-plus.svg";
+import {ReactComponent as MinusIcon} from "assets/icon-minus.svg";
+import {ReactComponent as OneIcon} from "assets/icon-one.svg";
+import {ReactComponent as ThreeIcon} from "assets/icon-three.svg";
+import {ReactComponent as FiveIcon} from "assets/icon-five.svg";
+import Parse from "parse";
+
+export const TimerDialog: VFC = () => {
+  const {t} = useTranslation();
+  const navigate = useNavigate();
+  const adminUsers = useAppSelector((state) => state.users.admins);
+  const [customTime, setCustomTime] = useState(10);
+
+  const startTimer = (minutes: number) => {
+    store.dispatch(ActionFactory.setTimer(new Date(new Date().getTime() + minutes * 60 * 1000)));
+    navigate("..");
+  };
+
+  const [startPositionX, setStartPositionX] = useState(0);
+
+  useEffect(() => {
+    const onUpdate = (e: MouseEvent) => {
+      if (startPositionX) {
+        setCustomTime(Math.max(1, Math.min(59, Math.abs(Math.floor((e.clientX - startPositionX) / 10)))));
+      }
+    };
+
+    const onEnd = () => {
+      setStartPositionX(0);
+    };
+
+    document.addEventListener("mousemove", onUpdate);
+    document.addEventListener("mouseup", onEnd);
+    return () => {
+      document.removeEventListener("mousemove", onUpdate);
+      document.removeEventListener("mouseup", onEnd);
+    };
+  }, [startPositionX]);
+
+  if (adminUsers == null || adminUsers.length === 0) {
+    return null;
+  }
+  if (adminUsers.find((user) => user.id === Parse.User.current()!.id) == null) {
+    navigate("..");
+  }
+
+  return (
+    <Dialog className="timer-dialog" title={t("TimerToggleButton.label")} onClose={() => navigate("..")}>
+      <button className="dialog__button" onClick={() => startTimer(1)} data-testid="timer-dialog__1-minute-button">
+        <label>{t("TimerToggleButton.1min")}</label>
+        <OneIcon className="timer-dialog__button-icon" />
+      </button>
+      <button className="dialog__button" onClick={() => startTimer(3)} data-testid="timer-dialog__3-minute-button">
+        <label>{t("TimerToggleButton.3min")}</label>
+        <ThreeIcon className="timer-dialog__button-icon" />
+      </button>
+      <button className="dialog__button" onClick={() => startTimer(5)} data-testid="timer-dialog__5-minute-button">
+        <label>{t("TimerToggleButton.5min")}</label>
+        <FiveIcon className="timer-dialog__button-icon" />
+      </button>
+      <button className="dialog__button" onClick={() => startTimer(customTime)} data-testid="timer-dialog__custom-minute-button">
+        <label>{t("TimerToggleButton.customTime")}</label>
+        <button
+          onClick={(e) => {
+            e.stopPropagation();
+            setCustomTime((prev) => Math.max(--prev, 1));
+          }}
+          className="timer-dialog__time-button"
+          data-testid="timer-dialog__minus-button"
+        >
+          <MinusIcon />
+        </button>
+        <label className="timer-dialog__time-label" onMouseDown={(e) => setStartPositionX(e.clientX)}>
+          {customTime}
+        </label>
+        <button
+          onClick={(e) => {
+            e.stopPropagation();
+            setCustomTime((prev) => Math.min(++prev, 59));
+          }}
+          className="timer-dialog__time-button"
+          data-testid="timer-dialog__plus-button"
+        >
+          <PlusIcon />
+        </button>
+      </button>
+    </Dialog>
+  );
+};
diff --git a/src/components/TimerDialog/index.ts b/src/components/TimerDialog/index.ts
new file mode 100644
index 00000000..6bc81c83
--- /dev/null
+++ b/src/components/TimerDialog/index.ts
@@ -0,0 +1,1 @@
+export * from "./TimerDialog";
diff --git a/src/components/Toggle/Toggle.scss b/src/components/Toggle/Toggle.scss
index b196c93d..f990bebe 100644
--- a/src/components/Toggle/Toggle.scss
+++ b/src/components/Toggle/Toggle.scss
@@ -32,7 +32,7 @@
 
 .toggle--active::after {
   left: 16px;
-  background-color: #0057ff;
+  background-color: $color-planning-pink;
   box-shadow: 0 4px 8px #0057ff29;
 }
 
diff --git a/src/components/VotingDialog/VotingDialog.scss b/src/components/VotingDialog/VotingDialog.scss
new file mode 100644
index 00000000..2ffafc32
--- /dev/null
+++ b/src/components/VotingDialog/VotingDialog.scss
@@ -0,0 +1,124 @@
+@import "constants/style";
+
+@media screen and (min-width: 600px) {
+  #root[column-visibility="visible"] + #portal {
+    .voting-dialog {
+      top: calc(50% - 25px);
+      right: 100px;
+      bottom: auto;
+      left: auto;
+      height: auto;
+    }
+    .voting-dialog::before {
+      content: " ";
+      position: absolute;
+      left: 100%;
+      top: 10px;
+      border-width: 15px;
+      border-style: solid;
+      border-color: transparent transparent transparent $color-planning-pink;
+    }
+    .voting-dialog::after {
+      content: " ";
+      position: absolute;
+      left: 100%;
+      top: 15px;
+      border-width: 10px;
+      border-style: solid;
+      border-color: transparent transparent transparent $color-white-one;
+    }
+  }
+
+  [theme="dark"] #root[column-visibility="visible"] + #portal {
+    .voting-dialog::after {
+      border-color: transparent transparent transparent $color-dark-mode;
+    }
+  }
+
+  #root[column-visibility="collapsed"] + #portal {
+    .voting-dialog {
+      left: calc(50% - 139px + 86px + 18px - 200px);
+      bottom: 100px;
+      right: auto;
+      top: auto;
+      height: auto;
+    }
+    .voting-dialog::before {
+      content: " ";
+      position: absolute;
+      left: calc(50% - 15px);
+      top: 100%;
+      border-width: 15px;
+      border-style: solid;
+      border-color: $color-planning-pink transparent transparent transparent;
+    }
+    .voting-dialog::after {
+      content: " ";
+      position: absolute;
+      left: calc(50% - 11px);
+      top: 100%;
+      border-width: 11px;
+      border-style: solid;
+      border-color: $color-white-one transparent transparent transparent;
+    }
+  }
+  [theme="dark"] #root[column-visibility="collapsed"] + #portal {
+    .voting-dialog::after {
+      border-color: $color-dark-mode transparent transparent transparent;
+    }
+  }
+}
+
+.voting-dialog__start-button {
+  border: none;
+  border-radius: 8px;
+  height: 48px;
+  width: 80%;
+  background: $color-planning-pink;
+  cursor: pointer;
+}
+.voting-dialog__start-button > label {
+  font-weight: bold;
+  font-size: 14px;
+  color: $color-white;
+  letter-spacing: 0.49px;
+  cursor: pointer;
+}
+
+.voting-dialog__toggle {
+  position: absolute;
+  top: 12px;
+  right: 24px;
+}
+
+.voting-dialog__vote-button {
+  height: 32px;
+  width: 32px;
+  border: none;
+  border-radius: 16px;
+  background-color: transparent;
+  cursor: pointer;
+  position: absolute;
+  top: 8px;
+  padding: 0;
+}
+.voting-dialog__vote-button:first-of-type {
+  right: 80px;
+}
+.voting-dialog__vote-button:last-of-type {
+  right: 24px;
+}
+.voting-dialog__vote-label {
+  position: absolute;
+  top: 8px;
+  right: 56px;
+  height: 32px;
+  width: 24px;
+  text-align: center;
+  line-height: 32px;
+  font-size: 14px;
+  cursor: col-resize;
+  user-select: none;
+  -webkit-touch-callout: none;
+  -webit-user-select: none;
+}
diff --git a/src/components/VotingDialog/VotingDialog.tsx b/src/components/VotingDialog/VotingDialog.tsx
new file mode 100644
index 00000000..7f8fc4c3
--- /dev/null
+++ b/src/components/VotingDialog/VotingDialog.tsx
@@ -0,0 +1,112 @@
+import {VFC, useState, useEffect} from "react";
+import {useNavigate} from "react-router-dom";
+import {Toggle} from "components/Toggle";
+import store, {useAppSelector} from "store";
+import {ActionFactory} from "store/action";
+import {useTranslation} from "react-i18next";
+import "./VotingDialog.scss";
+import {Dialog} from "components/Dialog";
+import {ReactComponent as PlusIcon} from "assets/icon-plus.svg";
+import {ReactComponent as MinusIcon} from "assets/icon-minus.svg";
+import Parse from "parse";
+
+export const VotingDialog: VFC = () => {
+  const {t} = useTranslation();
+  const navigate = useNavigate();
+  const boardId = useAppSelector((state) => state.board.data!.id);
+  const adminUsers = useAppSelector((state) => state.users.admins);
+  const activeVoting = useAppSelector((state) => state.board.data!.voting === "active");
+  const [allowCumulativeVoting, setAllowCumulativeVoting] = useState(false);
+  const [anonymousVoting, setAnonymousVoting] = useState(false);
+  const [numberOfVotes, setNumberOfVotes] = useState(5);
+  const [startPositionX, setStartPositionX] = useState(0);
+
+  useEffect(() => {
+    const onUpdate = (e: MouseEvent) => {
+      if (startPositionX) {
+        setNumberOfVotes(Math.min(99, Math.max(1, Math.abs(Math.floor((e.clientX - startPositionX) / 10)))));
+      }
+    };
+
+    const onEnd = () => {
+      setStartPositionX(0);
+    };
+
+    document.addEventListener("mousemove", onUpdate);
+    document.addEventListener("mouseup", onEnd);
+    return () => {
+      document.removeEventListener("mousemove", onUpdate);
+      document.removeEventListener("mouseup", onEnd);
+    };
+  }, [startPositionX]);
+
+  if (adminUsers == null || adminUsers.length === 0) {
+    return null;
+  }
+  if (adminUsers.find((user) => user.id === Parse.User.current()!.id) == null) {
+    navigate("..");
+  }
+
+  const startVoting = () => {
+    store.dispatch(
+      ActionFactory.addVoteConfiguration({
+        boardId,
+        voteLimit: numberOfVotes,
+        showVotesOfOtherUsers: !anonymousVoting,
+        allowMultipleVotesPerNote: allowCumulativeVoting,
+      })
+    );
+    store.dispatch(ActionFactory.editBoard({id: boardId, voting: "active"}));
+    navigate("..");
+  };
+
+  const stopVoting = () => {
+    store.dispatch(ActionFactory.editBoard({id: boardId, voting: "disabled"}));
+    navigate("..");
+  };
+  const cancelVoting = () => {
+    store.dispatch(ActionFactory.cancelVoting(boardId));
+    navigate("..");
+  };
+
+  return (
+    <Dialog className="voting-dialog" title={t("VoteConfigurationButton.label")} onClose={() => navigate("..")}>
+      {activeVoting ? (
+        <>
+          <button className="voting-dialog__start-button" data-testid="voting-dialog__cancel-button" onClick={() => cancelVoting()}>
+            <label>{t("VoteConfigurationButton.cancelVoting")}</label>
+          </button>
+          <button className="voting-dialog__start-button" data-testid="voting-dialog__stop-button" onClick={() => stopVoting()}>
+            <label>{t("VoteConfigurationButton.stopVoting")}</label>
+          </button>
+        </>
+      ) : (
+        <>
+          <button className="dialog__button" data-testid="voting-dialog__cumulative-voting-button" onClick={() => setAllowCumulativeVoting((state) => !state)}>
+            <label>{t("VoteConfigurationButton.allowMultipleVotesPerNote")}</label>
+            <Toggle active={allowCumulativeVoting} className="voting-dialog__toggle" />
+          </button>
+          <button className="dialog__button" data-testid="voting-dialog__anonymous-voting-button" onClick={() => setAnonymousVoting((state) => !state)}>
+            <label>{t("VoteConfigurationButton.showVotesOfOthers")}</label>
+            <Toggle active={anonymousVoting} className="voting-dialog__toggle" />
+          </button>
+          <div className="dialog__button">
+            <label>{t("VoteConfigurationButton.numberOfVotes")}</label>
+            <button onClick={() => setNumberOfVotes((prev) => Math.max(--prev, 0))} className="voting-dialog__vote-button" data-testid="voting-dialog__minus-button">
+              <MinusIcon />
+            </button>
+            <label className="voting-dialog__vote-label" onMouseDown={(e) => setStartPositionX(e.clientX)}>
+              {numberOfVotes}
+            </label>
+            <button onClick={() => setNumberOfVotes((prev) => Math.min(++prev, 99))} className="voting-dialog__vote-button" data-testid="voting-dialog__plus-button">
+              <PlusIcon />
+            </button>
+          </div>
+          <button className="voting-dialog__start-button" data-testid="voting-dialog__start-button" onClick={() => startVoting()}>
+            <label>{t("VoteConfigurationButton.startVoting")}</label>
+          </button>
+        </>
+      )}
+    </Dialog>
+  );
+};
diff --git a/src/components/VotingDialog/index.ts b/src/components/VotingDialog/index.ts
new file mode 100644
index 00000000..dcab6d8b
--- /dev/null
+++ b/src/components/VotingDialog/index.ts
@@ -0,0 +1,1 @@
+export * from "./VotingDialog";
diff --git a/src/routes/Router.tsx b/src/routes/Router.tsx
index 232d9f42..f97b8bd9 100644
--- a/src/routes/Router.tsx
+++ b/src/routes/Router.tsx
@@ -6,6 +6,8 @@ import {NotFound} from "routes/NotFound";
 import {RequireAuthentication} from "routes/RequireAuthentication";
 import {AuthRedirect} from "routes/AuthRedirect";
 import {Route} from "react-router";
+import {VotingDialog} from "components/VotingDialog";
+import {TimerDialog} from "components/TimerDialog";
 import {Homepage} from "./Homepage";
 import {Legal} from "./Legal";
 import ScrollToTop from "./ScrollToTop";
@@ -35,7 +37,10 @@ const Router = () => (
             <BoardGuard />
           </RequireAuthentication>
         }
-      />
+      >
+        <Route path="voting" element={<VotingDialog />} />
+        <Route path="timer" element={<TimerDialog />} />
+      </Route>
       <Route path="*" element={<NotFound />} />
     </Routes>
   </BrowserRouter>


Test patch:
diff --git a/src/components/Dialog/__tests__/Dialog.test.tsx b/src/components/Dialog/__tests__/Dialog.test.tsx
new file mode 100644
index 00000000..7ee68ce6
--- /dev/null
+++ b/src/components/Dialog/__tests__/Dialog.test.tsx
@@ -0,0 +1,37 @@
+import {fireEvent, render} from "@testing-library/react";
+import {Dialog} from "..";
+
+describe("Dialog", () => {
+  beforeEach(() => {
+    const portal = global.document.createElement("div");
+    portal.setAttribute("id", "portal");
+    global.document.querySelector("body")!.appendChild(portal);
+  });
+
+  it("should match snapshot", () => {
+    const {container} = render(<Dialog title="Test-Dialog" />, {container: global.document.querySelector("#portal")!});
+    expect(container.firstChild).toMatchSnapshot();
+  });
+
+  it("should contain the title prop as heading", () => {
+    const {container} = render(<Dialog title="Test-Dialog" />, {container: global.document.querySelector("#portal")!});
+    expect(container.getElementsByClassName("dialog__header-text")[0].innerHTML).toBe("Test-Dialog");
+  });
+
+  it("should contain the children", () => {
+    const {container} = render(
+      <Dialog title="Test-Dialog">
+        <div className="test-child" />
+      </Dialog>,
+      {container: global.document.querySelector("#portal")!}
+    );
+    expect(container.getElementsByClassName("test-child")).toHaveLength(1);
+  });
+
+  it("should run the onClose function on ESC key press", () => {
+    const mockOnClose = jest.fn();
+    const {container} = render(<Dialog title="Test-Dialog" onClose={mockOnClose} />, {container: global.document.querySelector("#portal")!});
+    fireEvent.keyDown(container, {key: "Escape", code: "Escape", charCode: 27});
+    expect(mockOnClose).toHaveBeenCalled();
+  });
+});
diff --git a/src/components/Dialog/__tests__/__snapshots__/Dialog.test.tsx.snap b/src/components/Dialog/__tests__/__snapshots__/Dialog.test.tsx.snap
new file mode 100644
index 00000000..e07b339d
--- /dev/null
+++ b/src/components/Dialog/__tests__/__snapshots__/Dialog.test.tsx.snap
@@ -0,0 +1,54 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Dialog should match snapshot 1`] = `
+<div
+  class="portal"
+  role="dialog"
+>
+  <div
+    data-focus-guard="true"
+    style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
+    tabindex="0"
+  />
+  <div
+    data-focus-lock-disabled="false"
+  >
+    <div
+      class="portal__frame"
+    >
+      <div
+        class="portal__content"
+        role="dialog"
+      >
+        <aside
+          class="dialog"
+        >
+          <article
+            class="dialog__content"
+          >
+            <h2
+              class="dialog__header-text"
+            >
+              Test-Dialog
+            </h2>
+          </article>
+          <button
+            class="dialog__close-button"
+          >
+            <svg
+              class="close-button__icon"
+            >
+              icon-close.svg
+            </svg>
+          </button>
+        </aside>
+      </div>
+    </div>
+  </div>
+  <div
+    data-focus-guard="true"
+    style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
+    tabindex="0"
+  />
+</div>
+`;
diff --git a/src/components/MenuBars/__tests__/__snapshots__/MenuBars.test.tsx.snap b/src/components/MenuBars/__tests__/__snapshots__/MenuBars.test.tsx.snap
index 33132191..d666fb82 100644
--- a/src/components/MenuBars/__tests__/__snapshots__/MenuBars.test.tsx.snap
+++ b/src/components/MenuBars/__tests__/__snapshots__/MenuBars.test.tsx.snap
@@ -161,7 +161,7 @@ exports[`Menu should render both add- and settings-menu for moderators 1`] = `
     >
       <button
         class="menu-item menu-item--left"
-        tabindex="400"
+        tabindex="0"
       >
         <div
           class="menu-item__tooltip"
@@ -177,89 +177,10 @@ exports[`Menu should render both add- and settings-menu for moderators 1`] = `
         >
           icon-timer.svg
         </svg>
-        <menu
-          class="dropdown timer__dropdown"
-        >
-          <div
-            class="dropdown__main"
-          >
-            <ul>
-              <li
-                class="dropdown__item-button timer-dropdown__item-button"
-              >
-                <button
-                  tabindex="-1"
-                >
-                  <label>
-                    1 minute
-                  </label>
-                  <div>
-                    1
-                  </div>
-                </button>
-              </li>
-              <li
-                class="dropdown__item-button timer-dropdown__item-button"
-              >
-                <button
-                  tabindex="-1"
-                >
-                  <label>
-                    3 minute
-                  </label>
-                  <div>
-                    3
-                  </div>
-                </button>
-              </li>
-              <li
-                class="dropdown__item-button timer-dropdown__item-button"
-              >
-                <button
-                  tabindex="-1"
-                >
-                  <label>
-                    5 minute
-                  </label>
-                  <div>
-                    5
-                  </div>
-                </button>
-              </li>
-              <li
-                class="dropdown__item-button timer-dropdown__item-button"
-              >
-                <button
-                  tabindex="-1"
-                >
-                  <label>
-                    Custom time
-                  </label>
-                  <button
-                    class="timer-dropdown__time-button"
-                    tabindex="-1"
-                  >
-                    +
-                  </button>
-                  <input
-                    tabindex="-1"
-                    value="10"
-                  />
-                  <button
-                    class="timer-dropdown__time-button"
-                    tabindex="-1"
-                  >
-                    -
-                  </button>
-                </button>
-              </li>
-            </ul>
-          </div>
-        </menu>
       </button>
       <button
         class="menu-item menu-item--left"
-        tabindex="408"
+        tabindex="0"
       >
         <div
           class="menu-item__tooltip"
@@ -275,88 +196,6 @@ exports[`Menu should render both add- and settings-menu for moderators 1`] = `
         >
           icon-vote.svg
         </svg>
-        <menu
-          class="dropdown vote-dropdown"
-        >
-          <div
-            class="dropdown__main"
-          >
-            <ul>
-              <li
-                class="dropdown__item-button vote-dropdown__item-button"
-              >
-                <button
-                  tabindex="-1"
-                >
-                  <label>
-                    Allow cumulative voting
-                  </label>
-                  <div
-                    class="toggle toggle--active"
-                  />
-                </button>
-              </li>
-              <li
-                class="dropdown__item-button vote-dropdown__item-button"
-              >
-                <button
-                  tabindex="-1"
-                >
-                  <label>
-                    Anonymous voting
-                  </label>
-                  <div
-                    class="toggle toggle--active"
-                  />
-                </button>
-              </li>
-              <li
-                class="dropdown__item-button vote-dropdown__item-button"
-              >
-                <button
-                  tabindex="-1"
-                >
-                  <label>
-                    Number of votes
-                  </label>
-                  <button
-                    class="vote-dropdown__vote-button"
-                    tabindex="-1"
-                  >
-                    +
-                  </button>
-                  <input
-                    tabindex="-1"
-                    value="5"
-                  />
-                  <button
-                    class="vote-dropdown__vote-button"
-                    tabindex="-1"
-                  >
-                    -
-                  </button>
-                </button>
-              </li>
-            </ul>
-          </div>
-          <div
-            class="dropdown__footer"
-          >
-            <ul>
-              <li
-                class="dropdown__item-button vote-dropdown__item-button"
-              >
-                <button
-                  tabindex="-1"
-                >
-                  <label>
-                    Start voting phase
-                  </label>
-                </button>
-              </li>
-            </ul>
-          </div>
-        </menu>
       </button>
       <button
         class="menu-item menu-item--disabled menu-item--left"
diff --git a/src/components/TimerDialog/__tests__/TimerDialog.test.tsx b/src/components/TimerDialog/__tests__/TimerDialog.test.tsx
new file mode 100644
index 00000000..f0c3f86c
--- /dev/null
+++ b/src/components/TimerDialog/__tests__/TimerDialog.test.tsx
@@ -0,0 +1,87 @@
+import {fireEvent, render, screen} from "@testing-library/react";
+import configureStore from "redux-mock-store";
+import {mocked} from "ts-jest/utils";
+import {User} from "parse";
+import {Provider} from "react-redux";
+import {BrowserRouter} from "react-router-dom";
+import store from "store";
+import {ActionFactory} from "store/action";
+import {TimerDialog} from "..";
+
+const mockedUsedNavigate = jest.fn();
+const storeDispatchSpy = jest.spyOn(store, "dispatch");
+
+jest.mock("react-router-dom", () => ({
+  ...jest.requireActual("react-router-dom"),
+  useNavigate: () => mockedUsedNavigate,
+}));
+
+const mockStore = configureStore();
+const mockedUser = mocked(User, true);
+
+jest.useFakeTimers("modern").setSystemTime(new Date(123456789).getTime());
+
+const createTimerDialog = () => {
+  const initialState = {
+    users: {
+      admins: [{id: "test-id"}],
+    },
+  };
+  const mockedStore = mockStore(initialState);
+  return (
+    <BrowserRouter>
+      <Provider store={mockedStore}>
+        <TimerDialog />
+      </Provider>
+    </BrowserRouter>
+  );
+};
+
+describe("TimerDialog", () => {
+  beforeEach(() => {
+    mockedUser.current = jest.fn(() => ({id: "test"} as never));
+    const portal = global.document.createElement("div");
+    portal.setAttribute("id", "portal");
+    global.document.querySelector("body")!.appendChild(portal);
+  });
+
+  it("should match the snapshot", () => {
+    const {container} = render(createTimerDialog(), {container: global.document.querySelector("#portal")!});
+    expect(container.firstChild).toMatchSnapshot();
+  });
+
+  it("should redirect if the current user isn't a moderator", () => {
+    mockedUser.current = jest.fn(() => ({id: "something-else"} as never));
+    render(createTimerDialog(), {container: global.document.querySelector("#portal")!});
+    expect(mockedUsedNavigate).toHaveBeenCalledWith("..");
+  });
+
+  it("should dispatch to store correctly on one minute button click", () => {
+    render(createTimerDialog(), {container: global.document.querySelector("#portal")!});
+    fireEvent.click(screen.getByTestId("timer-dialog__1-minute-button"));
+    expect(storeDispatchSpy).toHaveBeenCalledWith(ActionFactory.setTimer(new Date(new Date(123456789).getTime() + 1 * 60 * 1000)));
+  });
+
+  it("should dispatch to store correctly on three minute button click", () => {
+    render(createTimerDialog(), {container: global.document.querySelector("#portal")!});
+    fireEvent.click(screen.getByTestId("timer-dialog__3-minute-button"));
+    expect(storeDispatchSpy).toHaveBeenCalledWith(ActionFactory.setTimer(new Date(new Date(123456789).getTime() + 3 * 60 * 1000)));
+  });
+
+  it("should dispatch to store correctly on five minute button click", () => {
+    render(createTimerDialog(), {container: global.document.querySelector("#portal")!});
+    fireEvent.click(screen.getByTestId("timer-dialog__5-minute-button"));
+    expect(storeDispatchSpy).toHaveBeenCalledWith(ActionFactory.setTimer(new Date(new Date(123456789).getTime() + 5 * 60 * 1000)));
+  });
+
+  it("should dispatch to store correctly after custom time change", () => {
+    render(createTimerDialog(), {container: global.document.querySelector("#portal")!});
+    fireEvent.click(screen.getByTestId("timer-dialog__minus-button"));
+    fireEvent.click(screen.getByTestId("timer-dialog__custom-minute-button"));
+    expect(storeDispatchSpy).toHaveBeenCalledWith(ActionFactory.setTimer(new Date(new Date(123456789).getTime() + 9 * 60 * 1000)));
+    fireEvent.click(screen.getByTestId("timer-dialog__plus-button"));
+    fireEvent.click(screen.getByTestId("timer-dialog__plus-button"));
+    fireEvent.click(screen.getByTestId("timer-dialog__custom-minute-button"));
+    expect(storeDispatchSpy).toHaveBeenCalledWith(ActionFactory.setTimer(new Date(new Date(123456789).getTime() + 11 * 60 * 1000)));
+  });
+});
diff --git a/src/components/TimerDialog/__tests__/__snapshots__/TimerDialog.test.tsx.snap b/src/components/TimerDialog/__tests__/__snapshots__/TimerDialog.test.tsx.snap
new file mode 100644
index 00000000..6daa5712
--- /dev/null
+++ b/src/components/TimerDialog/__tests__/__snapshots__/TimerDialog.test.tsx.snap
@@ -0,0 +1,122 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TimerDialog should match the snapshot 1`] = `
+<div
+  class="portal"
+  role="dialog"
+>
+  <div
+    data-focus-guard="true"
+    style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
+    tabindex="0"
+  />
+  <div
+    data-focus-lock-disabled="false"
+  >
+    <div
+      class="portal__frame"
+    >
+      <div
+        class="portal__content"
+        role="dialog"
+      >
+        <aside
+          class="dialog timer-dialog"
+        >
+          <article
+            class="dialog__content"
+          >
+            <h2
+              class="dialog__header-text"
+            >
+              TimerToggleButton.label
+            </h2>
+            <button
+              class="dialog__button"
+              data-testid="timer-dialog__1-minute-button"
+            >
+              <label>
+                TimerToggleButton.1min
+              </label>
+              <svg
+                class="timer-dialog__button-icon"
+              >
+                icon-one.svg
+              </svg>
+            </button>
+            <button
+              class="dialog__button"
+              data-testid="timer-dialog__3-minute-button"
+            >
+              <label>
+                TimerToggleButton.3min
+              </label>
+              <svg
+                class="timer-dialog__button-icon"
+              >
+                icon-three.svg
+              </svg>
+            </button>
+            <button
+              class="dialog__button"
+              data-testid="timer-dialog__5-minute-button"
+            >
+              <label>
+                TimerToggleButton.5min
+              </label>
+              <svg
+                class="timer-dialog__button-icon"
+              >
+                icon-five.svg
+              </svg>
+            </button>
+            <button
+              class="dialog__button"
+              data-testid="timer-dialog__custom-minute-button"
+            >
+              <label>
+                TimerToggleButton.customTime
+              </label>
+              <button
+                class="timer-dialog__time-button"
+                data-testid="timer-dialog__minus-button"
+              >
+                <svg>
+                  icon-minus.svg
+                </svg>
+              </button>
+              <label
+                class="timer-dialog__time-label"
+              >
+                10
+              </label>
+              <button
+                class="timer-dialog__time-button"
+                data-testid="timer-dialog__plus-button"
+              >
+                <svg>
+                  icon-plus.svg
+                </svg>
+              </button>
+            </button>
+          </article>
+          <button
+            class="dialog__close-button"
+          >
+            <svg
+              class="close-button__icon"
+            >
+              icon-close.svg
+            </svg>
+          </button>
+        </aside>
+      </div>
+    </div>
+  </div>
+  <div
+    data-focus-guard="true"
+    style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
+    tabindex="0"
+  />
+</div>
+`;
diff --git a/src/components/VotingDialog/__tests__/VotingDialog.test.tsx b/src/components/VotingDialog/__tests__/VotingDialog.test.tsx
new file mode 100644
index 00000000..f44364f4
--- /dev/null
+++ b/src/components/VotingDialog/__tests__/VotingDialog.test.tsx
@@ -0,0 +1,100 @@
+import {fireEvent, render, screen} from "@testing-library/react";
+import configureStore from "redux-mock-store";
+import {mocked} from "ts-jest/utils";
+import {User} from "parse";
+import {Provider} from "react-redux";
+import {BrowserRouter} from "react-router-dom";
+import {ActionFactory} from "store/action";
+import store from "store";
+import {VotingDialog} from "..";
+
+const mockedUsedNavigate = jest.fn();
+
+jest.mock("react-router-dom", () => ({
+  ...jest.requireActual("react-router-dom"),
+  useNavigate: () => mockedUsedNavigate,
+}));
+
+const storeDispatchSpy = jest.spyOn(store, "dispatch");
+
+describe("VotingDialog", () => {
+  const mockStore = configureStore();
+  const mockedUser = mocked(User, true);
+
+  const createVotingDialog = (activeVoting: boolean) => {
+    const initialState = {
+      board: {
+        data: {
+          voting: activeVoting ? "active" : "disabled",
+          id: "test-board-id",
+        },
+      },
+      users: {
+        admins: [{id: "test-id"}],
+      },
+    };
+    const mockedStore = mockStore(initialState);
+    return (
+      <BrowserRouter>
+        <Provider store={mockedStore}>
+          <VotingDialog />
+        </Provider>
+      </BrowserRouter>
+    );
+  };
+
+  beforeEach(() => {
+    mockedUser.current = jest.fn(() => ({id: "test"} as never));
+    const portal = global.document.createElement("div");
+    portal.setAttribute("id", "portal");
+    global.document.querySelector("body")!.appendChild(portal);
+  });
+
+  it("should match the snapshot with no active voting", () => {
+    const {container} = render(createVotingDialog(false), {container: global.document.querySelector("#portal")!});
+    expect(container.firstChild).toMatchSnapshot();
+  });
+
+  it("should match the snapshot with active voting", () => {
+    const {container} = render(createVotingDialog(true), {container: global.document.querySelector("#portal")!});
+    expect(container.firstChild).toMatchSnapshot();
+  });
+
+  it("should redirect if the current user isn't a moderator", () => {
+    mockedUser.current = jest.fn(() => ({id: "something-else"} as never));
+    render(createVotingDialog(true), {container: global.document.querySelector("#portal")!});
+    expect(mockedUsedNavigate).toHaveBeenCalledWith("..");
+  });
+
+  it("should dispatch to store correctly on start voting button click", () => {
+    render(createVotingDialog(false), {container: global.document.querySelector("#portal")!});
+    fireEvent.click(screen.getByTestId("voting-dialog__start-button"));
+    expect(storeDispatchSpy).toHaveBeenCalledWith(
+      ActionFactory.addVoteConfiguration({boardId: "test-board-id", voteLimit: 5, showVotesOfOtherUsers: true, allowMultipleVotesPerNote: false})
+    );
+    expect(storeDispatchSpy).toHaveBeenCalledWith(ActionFactory.editBoard({id: "test-board-id", voting: "active"}));
+  });
+
+  it("should dispatch to store correctly on start voting button click with custom vote configuration", () => {
+    render(createVotingDialog(false), {container: global.document.querySelector("#portal")!});
+    fireEvent.click(screen.getByTestId("voting-dialog__cumulative-voting-button"));
+    fireEvent.click(screen.getByTestId("voting-dialog__anonymous-voting-button"));
+    fireEvent.click(screen.getByTestId("voting-dialog__plus-button"));
+    fireEvent.click(screen.getByTestId("voting-dialog__start-button"));
+    expect(storeDispatchSpy).toHaveBeenCalledWith(
+      ActionFactory.addVoteConfiguration({boardId: "test-board-id", voteLimit: 6, showVotesOfOtherUsers: false, allowMultipleVotesPerNote: true})
+    );
+  });
+
+  it("should dispatch to store correctly on cancel voting button click", () => {
+    render(createVotingDialog(true), {container: global.document.querySelector("#portal")!});
+    fireEvent.click(screen.getByTestId("voting-dialog__cancel-button"));
+    expect(storeDispatchSpy).toHaveBeenCalledWith(ActionFactory.cancelVoting("test-board-id"));
+  });
+
+  it("should dispatch to store correctly on stop voting button click", () => {
+    render(createVotingDialog(true), {container: global.document.querySelector("#portal")!});
+    fireEvent.click(screen.getByTestId("voting-dialog__stop-button"));
+    expect(storeDispatchSpy).toHaveBeenCalledWith(ActionFactory.editBoard({id: "test-board-id", voting: "disabled"}));
+  });
+});
diff --git a/src/components/VotingDialog/__tests__/__snapshots__/VotingDialog.test.tsx.snap b/src/components/VotingDialog/__tests__/__snapshots__/VotingDialog.test.tsx.snap
new file mode 100644
index 00000000..f31b6d9f
--- /dev/null
+++ b/src/components/VotingDialog/__tests__/__snapshots__/VotingDialog.test.tsx.snap
@@ -0,0 +1,181 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`VotingDialog should match the snapshot with active voting 1`] = `
+<div
+  class="portal"
+  role="dialog"
+>
+  <div
+    data-focus-guard="true"
+    style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
+    tabindex="0"
+  />
+  <div
+    data-focus-lock-disabled="false"
+  >
+    <div
+      class="portal__frame"
+    >
+      <div
+        class="portal__content"
+        role="dialog"
+      >
+        <aside
+          class="dialog voting-dialog"
+        >
+          <article
+            class="dialog__content"
+          >
+            <h2
+              class="dialog__header-text"
+            >
+              VoteConfigurationButton.label
+            </h2>
+            <button
+              class="voting-dialog__start-button"
+              data-testid="voting-dialog__cancel-button"
+            >
+              <label>
+                VoteConfigurationButton.cancelVoting
+              </label>
+            </button>
+            <button
+              class="voting-dialog__start-button"
+              data-testid="voting-dialog__stop-button"
+            >
+              <label>
+                VoteConfigurationButton.stopVoting
+              </label>
+            </button>
+          </article>
+          <button
+            class="dialog__close-button"
+          >
+            <svg
+              class="close-button__icon"
+            >
+              icon-close.svg
+            </svg>
+          </button>
+        </aside>
+      </div>
+    </div>
+  </div>
+  <div
+    data-focus-guard="true"
+    style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
+    tabindex="0"
+  />
+</div>
+`;
+
+exports[`VotingDialog should match the snapshot with no active voting 1`] = `
+<div
+  class="portal"
+  role="dialog"
+>
+  <div
+    data-focus-guard="true"
+    style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
+    tabindex="0"
+  />
+  <div
+    data-focus-lock-disabled="false"
+  >
+    <div
+      class="portal__frame"
+    >
+      <div
+        class="portal__content"
+        role="dialog"
+      >
+        <aside
+          class="dialog voting-dialog"
+        >
+          <article
+            class="dialog__content"
+          >
+            <h2
+              class="dialog__header-text"
+            >
+              VoteConfigurationButton.label
+            </h2>
+            <button
+              class="dialog__button"
+              data-testid="voting-dialog__cumulative-voting-button"
+            >
+              <label>
+                VoteConfigurationButton.allowMultipleVotesPerNote
+              </label>
+              <div
+                class="toggle voting-dialog__toggle"
+              />
+            </button>
+            <button
+              class="dialog__button"
+              data-testid="voting-dialog__anonymous-voting-button"
+            >
+              <label>
+                VoteConfigurationButton.showVotesOfOthers
+              </label>
+              <div
+                class="toggle voting-dialog__toggle"
+              />
+            </button>
+            <div
+              class="dialog__button"
+            >
+              <label>
+                VoteConfigurationButton.numberOfVotes
+              </label>
+              <button
+                class="voting-dialog__vote-button"
+                data-testid="voting-dialog__minus-button"
+              >
+                <svg>
+                  icon-minus.svg
+                </svg>
+              </button>
+              <label
+                class="voting-dialog__vote-label"
+              >
+                5
+              </label>
+              <button
+                class="voting-dialog__vote-button"
+                data-testid="voting-dialog__plus-button"
+              >
+                <svg>
+                  icon-plus.svg
+                </svg>
+              </button>
+            </div>
+            <button
+              class="voting-dialog__start-button"
+              data-testid="voting-dialog__start-button"
+            >
+              <label>
+                VoteConfigurationButton.startVoting
+              </label>
+            </button>
+          </article>
+          <button
+            class="dialog__close-button"
+          >
+            <svg
+              class="close-button__icon"
+            >
+              icon-close.svg
+            </svg>
+          </button>
+        </aside>
+      </div>
+    </div>
+  </div>
+  <div
+    data-focus-guard="true"
+    style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
+    tabindex="0"
+  />
+</div>
+`;