From 6cb0ac6a5010ae88b9008a0445e357a5456975b7 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 6 Jul 2020 10:18:49 +0100
Subject: [PATCH] Fix checkboxes/radios in context menus should only close on
 ENTER, not SPACE
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 src/components/structures/ContextMenu.js    | 38 ++++++++++++++++++---
 src/components/views/rooms/RoomSublist2.tsx |  4 +++
 src/components/views/rooms/RoomTile2.tsx    | 16 +++++++--
 3 files changed, 51 insertions(+), 7 deletions(-)
diff --git a/src/components/structures/ContextMenu.js b/src/components/structures/ContextMenu.js
index c4825ca1da..76f31a1017 100644
--- a/src/components/structures/ContextMenu.js
+++ b/src/components/structures/ContextMenu.js
@@ -424,7 +424,18 @@ MenuItemCheckbox.propTypes = {
 };
 
 // Semantic component for representing a styled role=menuitemcheckbox
-export const StyledMenuItemCheckbox = ({children, label, checked=false, disabled=false, ...props}) => {
+export const StyledMenuItemCheckbox = ({children, label, onChange, onClose, checked=false, disabled=false, ...props}) => {
+    const onKeyDown = (ev) => {
+        // Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
+        if (ev.key === Key.ENTER || ev.key === Key.SPACE) {
+            ev.preventDefault();
+            ev.stopPropagation();
+            onChange(ev);
+            if (ev.key === Key.ENTER) {
+                onClose();
+            }
+        }
+    };
     return (
         
             { children }
         
     );
 };
 StyledMenuItemCheckbox.propTypes = {
-    ...AccessibleButton.propTypes,
+    ...StyledCheckbox.propTypes,
     label: PropTypes.string, // optional
     checked: PropTypes.bool.isRequired,
     disabled: PropTypes.bool, // optional
     className: PropTypes.string, // optional
-    onClick: PropTypes.func.isRequired,
+    onChange: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired, // gets called after onChange on Key.ENTER
 };
 
 // Semantic component for representing a role=menuitemradio
@@ -467,7 +481,18 @@ MenuItemRadio.propTypes = {
 };
 
 // Semantic component for representing a styled role=menuitemradio
-export const StyledMenuItemRadio = ({children, label, checked=false, disabled=false, ...props}) => {
+export const StyledMenuItemRadio = ({children, label, onChange, onClose, checked=false, disabled=false, ...props}) => {
+    const onKeyDown = (ev) => {
+        // Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
+        if (ev.key === Key.ENTER || ev.key === Key.SPACE) {
+            ev.preventDefault();
+            ev.stopPropagation();
+            onChange(ev);
+            if (ev.key === Key.ENTER) {
+                onClose();
+            }
+        }
+    };
     return (
         
             { children }
         
@@ -488,7 +515,8 @@ StyledMenuItemRadio.propTypes = {
     checked: PropTypes.bool.isRequired,
     disabled: PropTypes.bool, // optional
     className: PropTypes.string, // optional
-    onClick: PropTypes.func.isRequired,
+    onChange: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired, // gets called after onChange on Key.ENTER
 };
 
 // Placement method for  to position context menu to right of elementRect with chevronOffset
diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx
index bc42f2321c..9817302445 100644
--- a/src/components/views/rooms/RoomSublist2.tsx
+++ b/src/components/views/rooms/RoomSublist2.tsx
@@ -334,6 +334,7 @@ export default class RoomSublist2 extends React.Component {
                         
                             {_t("Sort by")}
                              this.onTagSortChanged(SortAlgorithm.Recent)}
                                 checked={!isAlphabetical}
                                 name={`mx_${this.props.tagId}_sortBy`}
@@ -341,6 +342,7 @@ export default class RoomSublist2 extends React.Component {
                                 {_t("Activity")}
                             
                              this.onTagSortChanged(SortAlgorithm.Alphabetic)}
                                 checked={isAlphabetical}
                                 name={`mx_${this.props.tagId}_sortBy`}
@@ -352,6 +354,7 @@ export default class RoomSublist2 extends React.Component {
                         
                             {_t("Unread rooms")}
                             
@@ -362,6 +365,7 @@ export default class RoomSublist2 extends React.Component {
                         
                             {_t("Show")}
                             
diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx
index f3b22c3a64..8ee838fbba 100644
--- a/src/components/views/rooms/RoomTile2.tsx
+++ b/src/components/views/rooms/RoomTile2.tsx
@@ -80,6 +80,8 @@ interface IState {
     generalMenuPosition: PartialDOMRect;
 }
 
+type Volume = ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE;
+
 const messagePreviewId = (roomId: string) => `mx_RoomTile2_messagePreview_${roomId}`;
 
 const contextMenuBelow = (elementRect: PartialDOMRect) => {
@@ -213,6 +215,11 @@ export default class RoomTile2 extends React.Component {
 
         // TODO: Support tagging: https://github.com/vector-im/riot-web/issues/14211
         // TODO: XOR favourites and low priority: https://github.com/vector-im/riot-web/issues/14210
+
+        if ((ev as React.KeyboardEvent).key === Key.ENTER) {
+            // Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
+            this.setState({generalMenuPosition: null}); // hide the menu
+        }
     };
 
     private onLeaveRoomClick = (ev: ButtonEvent) => {
@@ -237,11 +244,13 @@ export default class RoomTile2 extends React.Component {
         this.setState({generalMenuPosition: null}); // hide the menu
     };
 
-    private async saveNotifState(ev: ButtonEvent, newState: ALL_MESSAGES_LOUD | ALL_MESSAGES | MENTIONS_ONLY | MUTE) {
+    private async saveNotifState(ev: ButtonEvent, newState: Volume) {
         ev.preventDefault();
         ev.stopPropagation();
         if (MatrixClientPeg.get().isGuest()) return;
 
+        // get key before we go async and React discards the nativeEvent
+        const key = (ev as React.KeyboardEvent).key;
         try {
             // TODO add local echo - https://github.com/vector-im/riot-web/issues/14280
             await setRoomNotifsState(this.props.room.roomId, newState);
@@ -251,7 +260,10 @@ export default class RoomTile2 extends React.Component {
             console.error(error);
         }
 
-        this.setState({notificationsMenuPosition: null}); // Close the context menu
+        if (key === Key.ENTER) {
+            // Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12
+            this.setState({notificationsMenuPosition: null}); // hide the menu
+        }
     }
 
     private onClickAllNotifs = ev => this.saveNotifState(ev, ALL_MESSAGES);