From 0c517fcba4a40f1ac011506513ac486ce723b076 Mon Sep 17 00:00:00 2001 From: Juraj Kapsiar Date: Wed, 26 Sep 2018 12:11:28 +0200 Subject: [PATCH 1/6] roles and keyboard handlers for list and listitem --- src/components/List/ListItem.tsx | 45 +++++++++++++++++-- .../Behaviors/List/SelectableListBehavior.ts | 11 ++++- .../List/SelectableListItemBehavior.ts | 10 +++++ .../teams/components/List/listItemStyles.ts | 9 ++++ 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/components/List/ListItem.tsx b/src/components/List/ListItem.tsx index c8dc20e7a9..6c8c268a90 100644 --- a/src/components/List/ListItem.tsx +++ b/src/components/List/ListItem.tsx @@ -1,11 +1,14 @@ import * as React from 'react' import * as PropTypes from 'prop-types' +import * as _ from 'lodash' import { createShorthandFactory, customPropTypes, UIComponent } from '../../lib' import ItemLayout from '../ItemLayout' import { ListItemBehavior } from '../../lib/accessibility' -import { Accessibility } from '../../lib/accessibility/interfaces' +import { Accessibility, AccessibilityActionHandlers } from '../../lib/accessibility/interfaces' import { ComponentVariablesInput, ComponentPartStyle } from '../../../types/theme' -import { Extendable } from '../../../types/utils' +import { Extendable, ComponentEventHandler } from '../../../types/utils' +import { ListItemProps } from '../../../node_modules/semantic-ui-react' +import isFromKeyboard from '../../lib/isFromKeyboard' export interface IListItemProps { accessibility?: Accessibility @@ -19,6 +22,8 @@ export interface IListItemProps { headerMedia?: any important?: boolean media?: any + onClick?: ComponentEventHandler + onFocus?: ComponentEventHandler selection?: boolean truncateContent?: boolean truncateHeader?: boolean @@ -55,6 +60,21 @@ class ListItem extends UIComponent, any> { important: PropTypes.bool, media: PropTypes.any, + /** + * Called on click. + * + * @param {SyntheticEvent} event - React's original SyntheticEvent. + * @param {object} data - All props. + */ + onClick: PropTypes.func, + + /** + * Called after user's focus. + * @param {SyntheticEvent} event - React's original SyntheticEvent. + * @param {object} data - All props. + */ + onFocus: PropTypes.func, + /** A list item can indicate that it can be selected. */ selection: PropTypes.bool, truncateContent: PropTypes.bool, @@ -82,6 +102,8 @@ class ListItem extends UIComponent, any> { 'headerMedia', 'important', 'media', + 'onClick', + 'onFocus', 'selection', 'styles', 'truncateContent', @@ -94,7 +116,21 @@ class ListItem extends UIComponent, any> { accessibility: ListItemBehavior as Accessibility, } - state: any = {} + state: any = isFromKeyboard.initial + + protected actionHandlers: AccessibilityActionHandlers = { + performClick: event => this.handleClick(event), + } + + handleClick = e => { + _.invoke(this.props, 'onClick', e, this.props) + } + + private handleFocus = (e: React.SyntheticEvent) => { + this.setState(isFromKeyboard.state()) + + _.invoke(this.props, 'onFocus', e, this.props) + } handleMouseEnter = () => { this.setState({ isHovering: true }) @@ -153,12 +189,15 @@ class ListItem extends UIComponent, any> { selection={selection} truncateContent={truncateContent} truncateHeader={truncateHeader} + onClick={this.handleClick} + onFocus={this.handleFocus} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} headerCSS={headerCSS} headerMediaCSS={headerMediaCSS} contentCSS={contentCSS} {...accessibility.attributes.root} + {...accessibility.keyHandlers.root} {...rest} /> ) diff --git a/src/lib/accessibility/Behaviors/List/SelectableListBehavior.ts b/src/lib/accessibility/Behaviors/List/SelectableListBehavior.ts index 77245f1752..03a0311b92 100644 --- a/src/lib/accessibility/Behaviors/List/SelectableListBehavior.ts +++ b/src/lib/accessibility/Behaviors/List/SelectableListBehavior.ts @@ -1,4 +1,5 @@ -import { IAccessibilityDefinition } from '../../interfaces' +import { IAccessibilityDefinition, FocusZoneMode } from '../../interfaces' +import { FocusZoneDirection } from '../../FocusZone' /** * @description @@ -12,6 +13,14 @@ const SelectableListBehavior: IAccessibilityDefinition = { role: 'listbox', }, }, + focusZone: { + mode: FocusZoneMode.Wrap, + props: { + isCircularNavigation: false, + direction: FocusZoneDirection.vertical, + preventDefaultWhenHandled: true, + }, + }, } export default SelectableListBehavior diff --git a/src/lib/accessibility/Behaviors/List/SelectableListItemBehavior.ts b/src/lib/accessibility/Behaviors/List/SelectableListItemBehavior.ts index 057d95a5e9..252bea661c 100644 --- a/src/lib/accessibility/Behaviors/List/SelectableListItemBehavior.ts +++ b/src/lib/accessibility/Behaviors/List/SelectableListItemBehavior.ts @@ -1,4 +1,6 @@ import { IAccessibilityDefinition } from '../../interfaces' +import { IS_FOCUSABLE_ATTRIBUTE } from '../../FocusZone/focusUtilities' +import * as keyboardKey from 'keyboard-key' /** * @description @@ -11,6 +13,14 @@ const SelectableListItemBehavior: (props: any) => IAccessibilityDefinition = (pr root: { role: 'option', 'aria-selected': !!props['active'], + [IS_FOCUSABLE_ATTRIBUTE]: true, + }, + }, + keyActions: { + root: { + performClick: { + keyCombinations: [{ keyCode: keyboardKey.Enter }, { keyCode: keyboardKey.Spacebar }], + }, }, }, }) diff --git a/src/themes/teams/components/List/listItemStyles.ts b/src/themes/teams/components/List/listItemStyles.ts index 4b90ef5407..a739748bc5 100644 --- a/src/themes/teams/components/List/listItemStyles.ts +++ b/src/themes/teams/components/List/listItemStyles.ts @@ -1,12 +1,21 @@ import { pxToRem } from '../../../../lib' import { IComponentPartStylesInput, ICSSInJSStyle } from '../../../../../types/theme' import { IListItemProps } from '../../../../components/List/ListItem' +import isFromKeyboard from '../../../../lib/isFromKeyboard' const listItemStyles: IComponentPartStylesInput = { root: ({ props: { selection, important } }): ICSSInJSStyle => ({ ...(selection && { position: 'relative', + ':focus': { + ...(isFromKeyboard && { + background: 'rgba(98, 100, 167, .8)', + color: '#fff', + }), + outline: 0, + }, + ':hover': { background: 'rgba(98, 100, 167, .8)', color: '#fff', From e66223f7948e302b276a9577964fafa42151ec74 Mon Sep 17 00:00:00 2001 From: Juraj Kapsiar Date: Thu, 8 Nov 2018 17:21:21 +0100 Subject: [PATCH 2/6] fix wrong merge, add spec entry --- .github/add-a-feature.md | 1 - src/components/List/ListItem.tsx | 11 +------ .../Behaviors/List/selectableListBehavior.ts | 33 +++++++++++++++++++ .../List/selectableListItemBehavior.ts | 2 +- 4 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 src/lib/accessibility/Behaviors/List/selectableListBehavior.ts diff --git a/.github/add-a-feature.md b/.github/add-a-feature.md index e7eec8c397..146e599bbb 100644 --- a/.github/add-a-feature.md +++ b/.github/add-a-feature.md @@ -8,7 +8,6 @@ Add a feature - [Propose feature](#propose-feature) - [Prototype](#prototype) - [Spec out the API](#spec-out-the-api) -- [Component anatomy](#component-anatomy) - [Create a component](#create-a-component) - [How to create a component](#how-to-create-a-component) - [Good practice](#good-practice) diff --git a/src/components/List/ListItem.tsx b/src/components/List/ListItem.tsx index 8b5d45fee7..f9edad00ba 100644 --- a/src/components/List/ListItem.tsx +++ b/src/components/List/ListItem.tsx @@ -1,5 +1,4 @@ import * as React from 'react' - import * as PropTypes from 'prop-types' import * as _ from 'lodash' import { createShorthandFactory, customPropTypes, UIComponent } from '../../lib' @@ -7,7 +6,7 @@ import ItemLayout from '../ItemLayout/ItemLayout' import { listItemBehavior } from '../../lib/accessibility' import { Accessibility, AccessibilityActionHandlers } from '../../lib/accessibility/types' import { ComponentVariablesInput, ComponentSlotStyle } from '../../themes/types' -import { Extendable } from '../../../types/utils' +import { Extendable, ComponentEventHandler } from '../../../types/utils' export interface ListItemProps { accessibility?: Accessibility @@ -22,7 +21,6 @@ export interface ListItemProps { important?: boolean media?: any onClick?: ComponentEventHandler - onFocus?: ComponentEventHandler selection?: boolean truncateContent?: boolean truncateHeader?: boolean @@ -74,13 +72,6 @@ class ListItem extends UIComponent, ListItemState> { */ onClick: PropTypes.func, - /** - * Called after user's focus. - * @param {SyntheticEvent} event - React's original SyntheticEvent. - * @param {object} data - All props. - */ - onFocus: PropTypes.func, - /** A list item can indicate that it can be selected. */ selection: PropTypes.bool, truncateContent: PropTypes.bool, diff --git a/src/lib/accessibility/Behaviors/List/selectableListBehavior.ts b/src/lib/accessibility/Behaviors/List/selectableListBehavior.ts new file mode 100644 index 0000000000..561f86a1a1 --- /dev/null +++ b/src/lib/accessibility/Behaviors/List/selectableListBehavior.ts @@ -0,0 +1,33 @@ +import * as keyboardKey from 'keyboard-key' +import { Accessibility } from '../../types' + +/** + * @description + * Adds role='listbox'. + * The listbox role is used to identify an element that creates a list from which a user may select one or more items. + */ +const selectableListBehavior: Accessibility = (props: any) => ({ + attributes: { + root: { + role: 'listbox', + }, + }, + keyActions: { + root: { + moveNext: { + keyCombinations: [{ keyCode: keyboardKey.ArrowDown }], + }, + movePrevious: { + keyCombinations: [{ keyCode: keyboardKey.ArrowUp }], + }, + moveFirst: { + keyCombinations: [{ keyCode: keyboardKey.Home }], + }, + moveLast: { + keyCombinations: [{ keyCode: keyboardKey.End }], + }, + }, + }, +}) + +export default selectableListBehavior diff --git a/src/lib/accessibility/Behaviors/List/selectableListItemBehavior.ts b/src/lib/accessibility/Behaviors/List/selectableListItemBehavior.ts index f334b49f8f..92bdce83d7 100644 --- a/src/lib/accessibility/Behaviors/List/selectableListItemBehavior.ts +++ b/src/lib/accessibility/Behaviors/List/selectableListItemBehavior.ts @@ -5,7 +5,7 @@ import { Accessibility } from '../../types' * @description * Adds role='option'. This role is used for a selectable item in a list. * Adds attribute 'aria-selected=true' based on the property 'active'. Based on this screen readers will recognize the selected state of the item. - * Performs click action with 'Enter' and 'Spacebar' on 'anchor'. + * Performs click action with 'Enter' and 'Spacebar' on 'root'. */ const selectableListItemBehavior: Accessibility = (props: any) => ({ From 05b6f31b37fae14ea592d48ca71c2e9af8c13acb Mon Sep 17 00:00:00 2001 From: Juraj Kapsiar Date: Fri, 9 Nov 2018 07:06:42 +0100 Subject: [PATCH 3/6] fix keyboardKey import --- .../accessibility/Behaviors/List/selectableListItemBehavior.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/accessibility/Behaviors/List/selectableListItemBehavior.ts b/src/lib/accessibility/Behaviors/List/selectableListItemBehavior.ts index 92bdce83d7..69340e5566 100644 --- a/src/lib/accessibility/Behaviors/List/selectableListItemBehavior.ts +++ b/src/lib/accessibility/Behaviors/List/selectableListItemBehavior.ts @@ -1,4 +1,4 @@ -import keyboardKey from 'keyboard-key' +import * as keyboardKey from 'keyboard-key' import { Accessibility } from '../../types' /** From 8225b6124206006c11c6defe98d1936200b64b8c Mon Sep 17 00:00:00 2001 From: Juraj Kapsiar Date: Fri, 9 Nov 2018 11:07:20 +0100 Subject: [PATCH 4/6] handleClick UT --- test/specs/components/List/ListItem-test.ts | 8 ------ test/specs/components/List/ListItem-test.tsx | 26 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 8 deletions(-) delete mode 100644 test/specs/components/List/ListItem-test.ts create mode 100644 test/specs/components/List/ListItem-test.tsx diff --git a/test/specs/components/List/ListItem-test.ts b/test/specs/components/List/ListItem-test.ts deleted file mode 100644 index 19e5922790..0000000000 --- a/test/specs/components/List/ListItem-test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { isConformant, handlesAccessibility } from 'test/specs/commonTests' - -import ListItem from 'src/components/List/ListItem' - -describe('ListItem', () => { - isConformant(ListItem) - handlesAccessibility(ListItem, { defaultRootRole: 'listitem' }) -}) diff --git a/test/specs/components/List/ListItem-test.tsx b/test/specs/components/List/ListItem-test.tsx new file mode 100644 index 0000000000..154bfa1815 --- /dev/null +++ b/test/specs/components/List/ListItem-test.tsx @@ -0,0 +1,26 @@ +import * as React from 'react' +import { isConformant, handlesAccessibility } from 'test/specs/commonTests' +import { mountWithProvider } from 'test/utils' + +import ListItem from 'src/components/List/ListItem' + +describe('ListItem', () => { + isConformant(ListItem) + handlesAccessibility(ListItem, { defaultRootRole: 'listitem' }) + + describe('handleClick', () => { + test('is executed when Enter is pressed', () => { + const onClick = jest.fn() + const listItem = mountWithProvider().find('ListItem') + listItem.simulate('keydown', { keyCode: 13 }) + expect(onClick).not.toHaveBeenCalled() + }) + + test('is executed when Spacebar is pressed', () => { + const onClick = jest.fn() + const listItem = mountWithProvider().find('ListItem') + listItem.simulate('keydown', { keyCode: 32 }) + expect(onClick).not.toHaveBeenCalled() + }) + }) +}) From 8d2a4a2071ea2e57b252887f640543ae5eed6213 Mon Sep 17 00:00:00 2001 From: Juraj Kapsiar Date: Fri, 9 Nov 2018 11:31:23 +0100 Subject: [PATCH 5/6] UT for selectable list item handleClick --- test/specs/components/List/ListItem-test.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/test/specs/components/List/ListItem-test.tsx b/test/specs/components/List/ListItem-test.tsx index 154bfa1815..e3bf3d581a 100644 --- a/test/specs/components/List/ListItem-test.tsx +++ b/test/specs/components/List/ListItem-test.tsx @@ -3,24 +3,29 @@ import { isConformant, handlesAccessibility } from 'test/specs/commonTests' import { mountWithProvider } from 'test/utils' import ListItem from 'src/components/List/ListItem' +import { selectableListItemBehavior } from 'src/lib/accessibility' describe('ListItem', () => { isConformant(ListItem) handlesAccessibility(ListItem, { defaultRootRole: 'listitem' }) - describe('handleClick', () => { + describe('selectable list handleClick', () => { test('is executed when Enter is pressed', () => { const onClick = jest.fn() - const listItem = mountWithProvider().find('ListItem') + const listItem = mountWithProvider( + , + ).find('ListItem') listItem.simulate('keydown', { keyCode: 13 }) - expect(onClick).not.toHaveBeenCalled() + expect(onClick).toHaveBeenCalled() }) test('is executed when Spacebar is pressed', () => { const onClick = jest.fn() - const listItem = mountWithProvider().find('ListItem') + const listItem = mountWithProvider( + , + ).find('ListItem') listItem.simulate('keydown', { keyCode: 32 }) - expect(onClick).not.toHaveBeenCalled() + expect(onClick).toHaveBeenCalled() }) }) }) From b37969694874956c17366ade757983ff59c5b0a3 Mon Sep 17 00:00:00 2001 From: Juraj Kapsiar Date: Fri, 9 Nov 2018 11:44:08 +0100 Subject: [PATCH 6/6] add changelog message --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f4525d439..40c3821fa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Close `Popup` on outside click @kuzhelov ([#410](https://github.com/stardust-ui/react/pull/410)) - Set default `chatBehavior` which uses Enter/Esc keys @sophieH29 ([#443](https://github.com/stardust-ui/react/pull/443)) - Add `iconPosition` property to `Input` component @mnajdova ([#442](https://github.com/stardust-ui/react/pull/442)) +- Handle `Enter` and `Spacebar` keys for selectable `ListItem` @jurokapsiar ([#279](https://github.com/stardust-ui/react/pull/279)) ### Documentation - Add all missing component descriptions and improve those existing @levithomason ([#400](https://github.com/stardust-ui/react/pull/400))