Repository: microsoft/fluent-ui-react (ts)

Issue:
Keyboard navigation key handlers exception in RTL
## Bug Report
In RTL, text input (Input, TextArea) crashes with uncaught exception:

TypeError
keyCombinations.map is not a function

This causes bad rendering behavior.

### Steps
A clear and concise description of steps to reproduce the problem.
Use this example:
https://codesandbox.io/s/stardust-ui-example-p3n6j

See that text input both in RTL and LTR work normally.

Download Chrome V69
https://www.slimjet.com/chrome/google-chrome-old-version.php

See that when RTL is false, text input works normally. Then set it to true. See that any text input now causes an uncaught exception to be thrown.

### Expected Result
Text input to work normally.

### Actual Result
Uncaught exception causes crash.

### Version
Chrome: Version 69.0.3497.92 (Official Build) (64-bit)
Stardust: Stardust UI @ 0.40.1

### Testcase
[Fork, update, and replace this sandbox to show the bug]:
https://codesandbox.io/s/stardust-ui-example-8ismt
N/A

Pull request description:
fix(Accessibility): filter out key actions with no key combinations
Fixes #2058.
Ignore keyActions with empty keyCombinations. Fixes exception in RTL.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ba5a7996d..07fa62df5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 
 ### Fixes
 - Show debug panel correctly for components with no owner @miroslavstastny ([#2055](https://github.com/stardust-ui/react/pull/2055))
+- Correctly handle empty key actions in RTL @miroslavstastny ([#2060](https://github.com/stardust-ui/react/pull/2060))
 
 ### Features
 - Add `menu` prop on `ToolbarMenuItem` component @mnajdova ([#1984](https://github.com/stardust-ui/react/pull/1984))
diff --git a/packages/react/src/lib/getKeyDownHandlers.ts b/packages/react/src/lib/getKeyDownHandlers.ts
index d7c94c3b0..eeaca6208 100644
--- a/packages/react/src/lib/getKeyDownHandlers.ts
+++ b/packages/react/src/lib/getKeyDownHandlers.ts
@@ -28,7 +28,10 @@ const getKeyDownHandlers = (
   if (!componentActionHandlers || !behaviorKeyActions) return keyHandlers
 
   for (const componentPart in behaviorKeyActions) {
-    const componentPartKeyAction = behaviorKeyActions[componentPart]
+    const componentPartKeyAction = _.pickBy(
+      behaviorKeyActions[componentPart],
+      actions => !_.isEmpty(actions.keyCombinations),
+    )
     const handledActions = _.intersection(
       _.keys(componentPartKeyAction),
       _.keys(componentActionHandlers),


Test patch:
diff --git a/packages/react/test/specs/lib/getKeyDownHandlers-test.ts b/packages/react/test/specs/lib/getKeyDownHandlers-test.ts
index e26ff4ff4..527dad98f 100644
--- a/packages/react/test/specs/lib/getKeyDownHandlers-test.ts
+++ b/packages/react/test/specs/lib/getKeyDownHandlers-test.ts
@@ -34,6 +34,7 @@ describe('getKeyDownHandlers', () => {
       expect(keyHandlers.hasOwnProperty(partElementName)).toBeTruthy()
       expect(keyHandlers[partElementName].hasOwnProperty('onKeyDown')).toBeTruthy()
     })
+
     test('for few component elements', () => {
       const actions = {
         testAction: () => {},
@@ -54,6 +55,7 @@ describe('getKeyDownHandlers', () => {
       expect(keyHandlers[partElementName].hasOwnProperty('onKeyDown')).toBeTruthy()
       expect(keyHandlers[anotherPartName].hasOwnProperty('onKeyDown')).toBeTruthy()
     })
+
     test('when there is 1 common action and few others that are not common', () => {
       const actions = {
         uncommonAction: () => {},
@@ -71,6 +73,7 @@ describe('getKeyDownHandlers', () => {
       expect(keyHandlers.hasOwnProperty(partElementName)).toBeTruthy()
       expect(keyHandlers[partElementName].hasOwnProperty('onKeyDown')).toBeTruthy()
     })
+
     test('and action should be invoked if keydown event has keycode mapped to that action', () => {
       const actions = {
         testAction: jest.fn(),
@@ -94,6 +97,34 @@ describe('getKeyDownHandlers', () => {
       expect(actions.anotherTestAction).not.toHaveBeenCalled()
     })
 
+    test('should ignore actions with no keyCombinations', () => {
+      const actions = {
+        testAction: jest.fn(),
+        actionFalse: jest.fn(),
+        actionNull: jest.fn(),
+        actionEmpty: jest.fn(),
+      }
+
+      actionsDefinition[partElementName].actionFalse = {
+        keyCombinations: false,
+      }
+      actionsDefinition[partElementName].actionNull = {
+        keyCombinations: null,
+      }
+      actionsDefinition[partElementName].actionEmpty = {
+        keyCombinations: [],
+      }
+
+      const keyHandlers = getKeyDownHandlers(actions, actionsDefinition)
+
+      keyHandlers[partElementName] &&
+        keyHandlers[partElementName]['onKeyDown'](eventArg(testKeyCode))
+      expect(actions.testAction).toHaveBeenCalled()
+      expect(actions.actionFalse).not.toHaveBeenCalled()
+      expect(actions.actionNull).not.toHaveBeenCalled()
+      expect(actions.actionEmpty).not.toHaveBeenCalled()
+    })
+
     describe('with respect of RTL', () => {
       test('swap Right key to Left key', () => {
         const actions = {
@@ -132,6 +163,38 @@ describe('getKeyDownHandlers', () => {
         expect(actions.actionOnLeftArrow).not.toHaveBeenCalled()
         expect(actions.actionOnRightArrow).toHaveBeenCalled()
       })
+
+      test('should ignore actions with no keyCombinations', () => {
+        const actions = {
+          actionOnRightArrow: jest.fn(),
+          actionFalse: jest.fn(),
+          actionNull: jest.fn(),
+          actionEmpty: jest.fn(),
+        }
+
+        actionsDefinition[partElementName].actionOnRightArrow = {
+          keyCombinations: [{ keyCode: keyboardKey.ArrowRight }],
+        }
+
+        actionsDefinition[partElementName].actionFalse = {
+          keyCombinations: false,
+        }
+        actionsDefinition[partElementName].actionNull = {
+          keyCombinations: null,
+        }
+        actionsDefinition[partElementName].actionEmpty = {
+          keyCombinations: [],
+        }
+
+        const keyHandlers = getKeyDownHandlers(actions, actionsDefinition, true)
+
+        keyHandlers[partElementName] &&
+          keyHandlers[partElementName]['onKeyDown'](eventArg(keyboardKey.ArrowLeft))
+        expect(actions.actionOnRightArrow).toHaveBeenCalled()
+        expect(actions.actionFalse).not.toHaveBeenCalled()
+        expect(actions.actionNull).not.toHaveBeenCalled()
+        expect(actions.actionEmpty).not.toHaveBeenCalled()
+      })
     })
   })
 
@@ -142,17 +205,43 @@ describe('getKeyDownHandlers', () => {
       const keyHandlers = getKeyDownHandlers(actions, actionsDefinition)
       expect(keyHandlers.hasOwnProperty(partElementName)).toBeFalsy()
     })
-    test("when acessibility's actionsDefinition is null", () => {
+
+    test("when accessibility's actionsDefinition is null", () => {
       const actions = { otherAction: () => {} }
       const keyHandlers = getKeyDownHandlers(actions, null)
 
       expect(keyHandlers.hasOwnProperty(partElementName)).toBeFalsy()
     })
+
     test('there are not common actions and actions definition', () => {
       const actions = { otherAction: () => {} }
       const keyHandlers = getKeyDownHandlers(actions, actionsDefinition)
 
       expect(keyHandlers.hasOwnProperty(partElementName)).toBeFalsy()
     })
+
+    test('when action definition has no keyCombinations', () => {
+      const actions = {
+        testAction: () => {},
+        actionFalse: () => {},
+        actionNull: () => {},
+        actionEmpty: () => {},
+      }
+
+      actionsDefinition.anotherPart = {
+        actionFalse: {
+          keyCombinations: false,
+        },
+        actionNull: {
+          keyCombinations: null,
+        },
+        actionEmpty: {
+          keyCombinations: [],
+        },
+      }
+
+      const keyHandlers = getKeyDownHandlers(actions, actionsDefinition)
+      expect(keyHandlers.hasOwnProperty('anotherPart')).toBeFalsy()
+    })
   })
 })