diff --git a/CHANGELOG.md b/CHANGELOG.md index af197727ac..7006d2dd84 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 - Ensure `Popup` properly flips values of `offset` prop in RTL @kuzhelov ([#612](https://github.com/stardust-ui/react/pull/612)) +- Fix `List` - items should be selectable @sophieH29 ([#566](https://github.com/stardust-ui/react/pull/566)) ### Features - Add `color` prop to `Text` component @Bugaa92 ([#597](https://github.com/stardust-ui/react/pull/597)) diff --git a/docs/src/examples/components/List/Content/ListExampleEndMedia.shorthand.tsx b/docs/src/examples/components/List/Content/ListExampleEndMedia.shorthand.tsx index 3955f2f76a..6915275332 100644 --- a/docs/src/examples/components/List/Content/ListExampleEndMedia.shorthand.tsx +++ b/docs/src/examples/components/List/Content/ListExampleEndMedia.shorthand.tsx @@ -21,6 +21,6 @@ const items = [ }, ] -const ListExample = () => +const ListExample = () => export default ListExample diff --git a/docs/src/examples/components/List/Content/ListExampleEndMedia.tsx b/docs/src/examples/components/List/Content/ListExampleEndMedia.tsx index 6b7a375791..3b99d18770 100644 --- a/docs/src/examples/components/List/Content/ListExampleEndMedia.tsx +++ b/docs/src/examples/components/List/Content/ListExampleEndMedia.tsx @@ -8,17 +8,17 @@ const ListExample = () => ( ) diff --git a/docs/src/examples/components/List/Types/ListExample.shorthand.tsx b/docs/src/examples/components/List/Types/ListExample.shorthand.tsx index 866bfe7a56..10f591fff3 100644 --- a/docs/src/examples/components/List/Types/ListExample.shorthand.tsx +++ b/docs/src/examples/components/List/Types/ListExample.shorthand.tsx @@ -25,6 +25,6 @@ const items = [ }, ] -const ListExampleSelection = ({ knobs }) => +const ListExampleSelectable = ({ knobs }) => -export default ListExampleSelection +export default ListExampleSelectable diff --git a/docs/src/examples/components/List/Types/ListExample.tsx b/docs/src/examples/components/List/Types/ListExample.tsx index da2e3c1f4a..250ab8ef2a 100644 --- a/docs/src/examples/components/List/Types/ListExample.tsx +++ b/docs/src/examples/components/List/Types/ListExample.tsx @@ -1,7 +1,7 @@ import React from 'react' import { List, Image } from '@stardust-ui/react' -const ListExampleSelection = ({ knobs }) => ( +const ListExampleSelectable = ({ knobs }) => ( } @@ -24,4 +24,4 @@ const ListExampleSelection = ({ knobs }) => ( ) -export default ListExampleSelection +export default ListExampleSelectable diff --git a/docs/src/examples/components/List/Types/ListExampleSelection.shorthand.tsx b/docs/src/examples/components/List/Types/ListExampleSelectable.shorthand.tsx similarity index 79% rename from docs/src/examples/components/List/Types/ListExampleSelection.shorthand.tsx rename to docs/src/examples/components/List/Types/ListExampleSelectable.shorthand.tsx index fe7842da57..49a526710e 100644 --- a/docs/src/examples/components/List/Types/ListExampleSelection.shorthand.tsx +++ b/docs/src/examples/components/List/Types/ListExampleSelectable.shorthand.tsx @@ -25,8 +25,6 @@ const items = [ }, ] -const selection = knobs => (knobs === undefined ? true : knobs.selection) +const ListExampleSelectable = () => -const ListExampleSelection = ({ knobs }) => - -export default ListExampleSelection +export default ListExampleSelectable diff --git a/docs/src/examples/components/List/Types/ListExampleSelection.tsx b/docs/src/examples/components/List/Types/ListExampleSelectable.tsx similarity index 72% rename from docs/src/examples/components/List/Types/ListExampleSelection.tsx rename to docs/src/examples/components/List/Types/ListExampleSelectable.tsx index ff00e6ef0c..55029d1256 100644 --- a/docs/src/examples/components/List/Types/ListExampleSelection.tsx +++ b/docs/src/examples/components/List/Types/ListExampleSelectable.tsx @@ -1,32 +1,30 @@ import React from 'react' import { List, Image } from '@stardust-ui/react' -const selection = knobs => (knobs === undefined ? true : knobs.selection) - -const ListExampleSelection = ({ knobs }) => ( - +const ListExampleSelectable = () => ( + } header="Irving Kuhic" headerMedia="7:26:56 AM" content="Program the sensor to the SAS alarm through the haptic SQL card!" - selection={selection(knobs)} + selectable /> } header="Skyler Parks" headerMedia="11:30:17 PM" content="Use the online FTP application to input the multi-byte application!" - selection={selection(knobs)} + selectable /> } header="Dante Schneider" headerMedia="5:22:40 PM" content="The GB pixel is down, navigate the virtual interface!" - selection={selection(knobs)} + selectable /> ) -export default ListExampleSelection +export default ListExampleSelectable diff --git a/docs/src/examples/components/List/Types/ListExampleSelectableControlled.shorthand.tsx b/docs/src/examples/components/List/Types/ListExampleSelectableControlled.shorthand.tsx new file mode 100644 index 0000000000..f493b03105 --- /dev/null +++ b/docs/src/examples/components/List/Types/ListExampleSelectableControlled.shorthand.tsx @@ -0,0 +1,48 @@ +import * as React from 'react' +import { List, Image } from '@stardust-ui/react' + +class SelectableListControlledExample extends React.Component { + state = { selectedIndex: -1 } + + items = [ + { + key: 'irving', + media: , + header: 'Irving Kuhic', + headerMedia: '7:26:56 AM', + content: 'Program the sensor to the SAS alarm through the haptic SQL card!', + }, + { + key: 'skyler', + media: , + header: 'Skyler Parks', + headerMedia: '11:30:17 PM', + content: 'Use the online FTP application to input the multi-byte application!', + }, + { + key: 'dante', + media: , + header: 'Dante Schneider', + headerMedia: '5:22:40 PM', + content: 'The GB pixel is down, navigate the virtual interface!', + }, + ] + + render() { + return ( + { + alert( + `List is requested to change its selectedIndex state to "${newProps.selectedIndex}"`, + ) + this.setState({ selectedIndex: newProps.selectedIndex }) + }} + items={this.items} + /> + ) + } +} + +export default SelectableListControlledExample diff --git a/docs/src/examples/components/List/Types/ListExampleSelection.knobs.tsx b/docs/src/examples/components/List/Types/ListExampleSelection.knobs.tsx deleted file mode 100644 index f8494fc867..0000000000 --- a/docs/src/examples/components/List/Types/ListExampleSelection.knobs.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import PropTypes from 'prop-types' -import React from 'react' -import Knobs from 'docs/src/components/Knobs/Knobs' - -const ListExampleSelectionKnobs: any = props => { - const { onKnobChange, selection } = props - - return ( - - - - ) -} - -ListExampleSelectionKnobs.propTypes = { - onKnobChange: PropTypes.func.isRequired, - selection: PropTypes.bool, -} - -ListExampleSelectionKnobs.defaultProps = { - selection: true, -} - -export default ListExampleSelectionKnobs diff --git a/docs/src/examples/components/List/Types/index.tsx b/docs/src/examples/components/List/Types/index.tsx index 0d78cbf48a..7483e6ff44 100644 --- a/docs/src/examples/components/List/Types/index.tsx +++ b/docs/src/examples/components/List/Types/index.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import * as React from 'react' import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample' import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection' @@ -10,9 +10,14 @@ const Types = () => ( examplePath="components/List/Types/ListExample" /> + ) diff --git a/docs/src/prototypes/SearchPage/SearchPage.tsx b/docs/src/prototypes/SearchPage/SearchPage.tsx index c655a20ad5..fa784abf6c 100644 --- a/docs/src/prototypes/SearchPage/SearchPage.tsx +++ b/docs/src/prototypes/SearchPage/SearchPage.tsx @@ -97,7 +97,7 @@ class SearchPage extends React.Component { Results {results.length} of {DATA_RECORDS.length}

- + )} diff --git a/src/components/List/List.tsx b/src/components/List/List.tsx index cee47f7d2e..009b69940e 100644 --- a/src/components/List/List.tsx +++ b/src/components/List/List.tsx @@ -6,7 +6,7 @@ import * as PropTypes from 'prop-types' import { customPropTypes, childrenExist, - UIComponent, + AutoControlledComponent, UIComponentProps, ChildrenComponentProps, commonPropTypes, @@ -15,7 +15,7 @@ import ListItem from './ListItem' import { listBehavior } from '../../lib/accessibility' import { Accessibility, AccessibilityActionHandlers } from '../../lib/accessibility/types' import { ContainerFocusHandler } from '../../lib/accessibility/FocusHandling/FocusContainer' -import { Extendable, ShorthandValue } from '../../../types/utils' +import { Extendable, ShorthandValue, ComponentEventHandler } from '../../../types/utils' export interface ListProps extends UIComponentProps, ChildrenComponentProps { /** @@ -30,8 +30,21 @@ export interface ListProps extends UIComponentProps, ChildrenComponentProps { /** Shorthand array of props for ListItem. */ items?: ShorthandValue[] - /** A selection list formats list items as possible choices. */ - selection?: boolean + /** A selectable list formats list items as possible choices. */ + selectable?: boolean + + /** Index of the currently selected item. */ + selectedIndex?: number + + /** Initial selectedIndex value. */ + defaultSelectedIndex?: number + + /** + * Event for request to change 'selectedIndex' value. + * @param {SyntheticEvent} event - React's original SyntheticEvent. + * @param {object} data - All props and proposed value. + */ + onSelectedIndexChange?: ComponentEventHandler /** Truncates content */ truncateContent?: boolean @@ -41,13 +54,14 @@ export interface ListProps extends UIComponentProps, ChildrenComponentProps { } export interface ListState { - selectedItemIndex: number + focusedIndex: number + selectedIndex?: number } /** * A list displays a group of related content. */ -class List extends UIComponent, ListState> { +class List extends AutoControlledComponent, ListState> { static displayName = 'List' static className = 'ui-list' @@ -59,9 +73,12 @@ class List extends UIComponent, ListState> { accessibility: PropTypes.func, debug: PropTypes.bool, items: customPropTypes.collectionShorthand, - selection: PropTypes.bool, + selectable: PropTypes.bool, truncateContent: PropTypes.bool, truncateHeader: PropTypes.bool, + selectedIndex: PropTypes.number, + defaultSelectedIndex: PropTypes.number, + onSelectedIndexChange: PropTypes.func, } static defaultProps = { @@ -69,14 +86,15 @@ class List extends UIComponent, ListState> { accessibility: listBehavior as Accessibility, } + static autoControlledProps = ['selectedIndex'] + getInitialAutoControlledState() { + return { selectedIndex: -1, focusedIndex: 0 } + } + static Item = ListItem // List props that are passed to each individual Item props - static itemProps = ['debug', 'selection', 'truncateContent', 'truncateHeader', 'variables'] - - public state = { - selectedItemIndex: 0, - } + static itemProps = ['debug', 'selectable', 'truncateContent', 'truncateHeader', 'variables'] private focusHandler: ContainerFocusHandler = null private itemRefs = [] @@ -100,6 +118,22 @@ class List extends UIComponent, ListState> { }, } + constructor(props, context) { + super(props, context) + + this.focusHandler = new ContainerFocusHandler( + () => this.props.items.length, + index => { + this.setState({ focusedIndex: index }, () => { + const targetComponent = this.itemRefs[index] && this.itemRefs[index].current + const targetDomNode = ReactDOM.findDOMNode(targetComponent) as any + + targetDomNode && targetDomNode.focus() + }) + }, + ) + } + renderComponent({ ElementType, classes, accessibility, rest }) { const { children } = this.props @@ -115,36 +149,32 @@ class List extends UIComponent, ListState> { ) } - componentDidMount() { - this.focusHandler = new ContainerFocusHandler( - () => this.props.items.length, - index => { - this.setState({ selectedItemIndex: index }, () => { - const targetComponent = this.itemRefs[index] && this.itemRefs[index].current - const targetDomNode = ReactDOM.findDOMNode(targetComponent) as any - - targetDomNode && targetDomNode.focus() - }) - }, - ) - } - renderItems() { const { items } = this.props - const { selectedItemIndex } = this.state + const { focusedIndex, selectedIndex } = this.state + + this.focusHandler.syncFocusedIndex(focusedIndex) this.itemRefs = [] return _.map(items, (item, idx) => { const maybeSelectableItemProps = {} as any - if (this.props.selection) { + if (this.props.selectable) { const ref = React.createRef() this.itemRefs[idx] = ref - maybeSelectableItemProps.tabIndex = idx === selectedItemIndex ? 0 : -1 maybeSelectableItemProps.ref = ref - maybeSelectableItemProps.onFocus = () => this.focusHandler.syncFocusedItemIndex(idx) + maybeSelectableItemProps.onFocus = () => this.setState({ focusedIndex: idx }) + maybeSelectableItemProps.onClick = e => { + this.trySetState({ selectedIndex: idx }) + _.invoke(this.props, 'onSelectedIndexChange', e, { + ...this.props, + ...{ selectedIndex: idx }, + }) + } + maybeSelectableItemProps.selected = idx === selectedIndex + maybeSelectableItemProps.tabIndex = idx === focusedIndex ? 0 : -1 } const itemProps = { diff --git a/src/components/List/ListItem.tsx b/src/components/List/ListItem.tsx index febdea30d7..b6af86fd62 100644 --- a/src/components/List/ListItem.tsx +++ b/src/components/List/ListItem.tsx @@ -1,5 +1,5 @@ import * as React from 'react' - +import * as _ from 'lodash' import * as PropTypes from 'prop-types' import { createShorthandFactory, @@ -10,8 +10,8 @@ import { } from '../../lib' import ItemLayout from '../ItemLayout/ItemLayout' import { listItemBehavior } from '../../lib/accessibility' -import { Accessibility } from '../../lib/accessibility/types' -import { Extendable } from '../../../types/utils' +import { Accessibility, AccessibilityActionHandlers } from '../../lib/accessibility/types' +import { Extendable, ComponentEventHandler } from '../../../types/utils' export interface ListItemProps extends UIComponentProps, ContentComponentProps { /** @@ -20,7 +20,7 @@ export interface ListItemProps extends UIComponentProps, ContentComponentProps } /** * A list item contains a single piece of content within a list. */ -class ListItem extends UIComponent, ListItemState> { +class ListItem extends UIComponent> { static create: Function static displayName = 'ListItem' @@ -67,11 +73,14 @@ class ListItem extends UIComponent, ListItemState> { important: PropTypes.bool, media: PropTypes.any, - selection: PropTypes.bool, + selectable: PropTypes.bool, + selected: PropTypes.bool, + truncateContent: PropTypes.bool, truncateHeader: PropTypes.bool, accessibility: PropTypes.func, + onClick: PropTypes.func, } static defaultProps = { @@ -79,17 +88,18 @@ class ListItem extends UIComponent, ListItemState> { accessibility: listItemBehavior as Accessibility, } - constructor(props: ListItemProps) { - super(props, null) - - this.state = { - isHovering: false, - } + protected actionHandlers: AccessibilityActionHandlers = { + performClick: event => { + this.handleClick(event) + event.preventDefault() + }, } - private itemRef = React.createRef() + handleClick = e => { + _.invoke(this.props, 'onClick', e, this.props) + } - renderComponent({ ElementType, classes, accessibility, rest, styles }) { + renderComponent({ classes, accessibility, rest, styles }) { const { as, debug, @@ -121,8 +131,9 @@ class ListItem extends UIComponent, ListItemState> { headerCSS={styles.header} headerMediaCSS={styles.headerMedia} contentCSS={styles.content} - ref={this.itemRef} + onClick={this.handleClick} {...accessibility.attributes.root} + {...accessibility.keyHandlers.root} {...rest} /> ) diff --git a/src/index.ts b/src/index.ts index af883aeb8b..2edc275517 100644 --- a/src/index.ts +++ b/src/index.ts @@ -72,7 +72,7 @@ export { default as Label, LabelProps } from './components/Label/Label' export { default as Layout, LayoutPropsWithDefaults, LayoutProps } from './components/Layout/Layout' export { default as List, ListProps } from './components/List/List' -export { default as ListItem, ListItemState, ListItemProps } from './components/List/ListItem' +export { default as ListItem, ListItemProps } from './components/List/ListItem' export { default as Menu, MenuProps } from './components/Menu/Menu' export { default as MenuItem, MenuItemState, MenuItemProps } from './components/Menu/MenuItem' diff --git a/src/lib/accessibility/Behaviors/List/listBehavior.ts b/src/lib/accessibility/Behaviors/List/listBehavior.ts index 360e89f59e..54f91bf632 100644 --- a/src/lib/accessibility/Behaviors/List/listBehavior.ts +++ b/src/lib/accessibility/Behaviors/List/listBehavior.ts @@ -4,10 +4,10 @@ import basicListBehavior from './basicListBehavior' /** * @description - * Defines a behavior 'BasicListBehavior' or 'SelectableListBehavior' based on property 'selection'. + * Defines a behavior 'BasicListBehavior' or 'SelectableListBehavior' based on property 'selectable'. */ const ListBehavior: Accessibility = (props: any) => - props.selection ? selectableListBehavior(props) : basicListBehavior(props) + props.selectable ? selectableListBehavior(props) : basicListBehavior(props) export default ListBehavior diff --git a/src/lib/accessibility/Behaviors/List/listItemBehavior.ts b/src/lib/accessibility/Behaviors/List/listItemBehavior.ts index 09292f93ff..714bf96c3f 100644 --- a/src/lib/accessibility/Behaviors/List/listItemBehavior.ts +++ b/src/lib/accessibility/Behaviors/List/listItemBehavior.ts @@ -4,10 +4,10 @@ import { Accessibility } from '../../types' /** * @description - * Defines a behavior "BasicListItemBehavior" or "SelectableListItemBehavior" based on "selection" property. + * Defines a behavior "BasicListItemBehavior" or "SelectableListItemBehavior" based on "selectable" property. */ const listItemBehavior: Accessibility = (props: any) => - props.selection ? selectableListItemBehavior(props) : basicListItemBehavior(props) + props.selectable ? selectableListItemBehavior(props) : basicListItemBehavior(props) export default listItemBehavior diff --git a/src/lib/accessibility/Behaviors/List/selectableListItemBehavior.ts b/src/lib/accessibility/Behaviors/List/selectableListItemBehavior.ts index 31f88d0b33..ae946d29a2 100644 --- a/src/lib/accessibility/Behaviors/List/selectableListItemBehavior.ts +++ b/src/lib/accessibility/Behaviors/List/selectableListItemBehavior.ts @@ -1,19 +1,24 @@ import { Accessibility } from '../../types' +import * as keyboardKey from 'keyboard-key' /** * @specification * 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. + * Adds attribute 'aria-selected=true' based on the property 'selected'. Based on this screen readers will recognize the selected state of the item. */ const selectableListItemBehavior: Accessibility = (props: any) => ({ attributes: { root: { role: 'option', - 'aria-selected': !!props['active'], - ...(props.focusableItemProps && { - tabIndex: props.focusableItemProps.isFocused ? '0' : '-1', - }), + 'aria-selected': !!props['selected'], + }, + }, + keyActions: { + root: { + performClick: { + keyCombinations: [{ keyCode: keyboardKey.Enter }, { keyCode: keyboardKey.Spacebar }], + }, }, }, }) diff --git a/src/lib/accessibility/FocusHandling/FocusContainer.ts b/src/lib/accessibility/FocusHandling/FocusContainer.ts index 14238fdb47..ef2aea39bd 100644 --- a/src/lib/accessibility/FocusHandling/FocusContainer.ts +++ b/src/lib/accessibility/FocusHandling/FocusContainer.ts @@ -1,29 +1,29 @@ import * as _ from 'lodash' export class ContainerFocusHandler { - private focusedItemIndex = 0 + private focusedIndex = 0 constructor(private getItemsCount: () => number, private readonly setFocusAt: (number) => void) {} private noItems = (): boolean => this.getItemsCount() === 0 - private constrainFocusedItemIndex(): void { - if (this.focusedItemIndex < 0) { - this.focusedItemIndex = 0 + private constrainFocusedIndex(): void { + if (this.focusedIndex < 0) { + this.focusedIndex = 0 } const itemsCount = this.getItemsCount() - if (this.focusedItemIndex >= itemsCount) { - this.focusedItemIndex = itemsCount - 1 + if (this.focusedIndex >= itemsCount) { + this.focusedIndex = itemsCount - 1 } } - public getFocusedItemIndex(): number { - return this.focusedItemIndex + public getFocusedIndex(): number { + return this.focusedIndex } - public syncFocusedItemIndex(withCurrentIndex: number) { - this.focusedItemIndex = withCurrentIndex + public syncFocusedIndex(withCurrentIndex: number) { + this.focusedIndex = withCurrentIndex } public movePrevious(): void { @@ -31,10 +31,10 @@ export class ContainerFocusHandler { return } - this.focusedItemIndex -= 1 - this.constrainFocusedItemIndex() + this.focusedIndex -= 1 + this.constrainFocusedIndex() - this.setFocusAt(this.focusedItemIndex) + this.setFocusAt(this.focusedIndex) } public moveNext(): void { @@ -42,10 +42,10 @@ export class ContainerFocusHandler { return } - this.focusedItemIndex += 1 - this.constrainFocusedItemIndex() + this.focusedIndex += 1 + this.constrainFocusedIndex() - this.setFocusAt(this.focusedItemIndex) + this.setFocusAt(this.focusedIndex) } public moveFirst(): void { @@ -53,8 +53,8 @@ export class ContainerFocusHandler { return } - this.focusedItemIndex = 0 - this.setFocusAt(this.focusedItemIndex) + this.focusedIndex = 0 + this.setFocusAt(this.focusedIndex) } public moveLast(): void { @@ -62,7 +62,7 @@ export class ContainerFocusHandler { return } - this.focusedItemIndex = this.getItemsCount() - 1 - this.setFocusAt(this.focusedItemIndex) + this.focusedIndex = this.getItemsCount() - 1 + this.setFocusAt(this.focusedIndex) } } diff --git a/src/themes/teams/components/List/listItemStyles.ts b/src/themes/teams/components/List/listItemStyles.ts index 4131c9e584..34721950c3 100644 --- a/src/themes/teams/components/List/listItemStyles.ts +++ b/src/themes/teams/components/List/listItemStyles.ts @@ -2,9 +2,9 @@ import { pxToRem } from '../../utils' import { ComponentSlotStylesInput, ICSSInJSStyle } from '../../../types' import { ListItemProps } from '../../../../components/List/ListItem' -const hoverStyle = variables => ({ - background: variables.selectionHoverBackgroundColor, - color: variables.selectionHoverColor, +const hoverFocusStyle = variables => ({ + background: variables.selectableFocusHoverBackgroundColor, + color: variables.selectableFocusHoverColor, cursor: 'pointer', '& .ui-item-layout__header': { color: 'inherit' }, @@ -18,16 +18,22 @@ const hoverStyle = variables => ({ '& .ui-item-layout__endMedia': { display: 'block', color: 'inherit' }, }) +const selectedStyle = variables => ({ + background: variables.selectedBackgroundColor, + color: variables.selectedColor, +}) + const listItemStyles: ComponentSlotStylesInput = { - root: ({ props: { selection, important }, variables }): ICSSInJSStyle => ({ - ...(selection && { + root: ({ props: { selectable, selected, important }, variables }): ICSSInJSStyle => ({ + ...(selectable && { position: 'relative', // hide the end media by default '& .ui-item-layout__endMedia': { display: 'none' }, - '&:hover': hoverStyle(variables), - '&:focus': hoverStyle(variables), + '&:hover': hoverFocusStyle(variables), + '&:focus': hoverFocusStyle(variables), + ...(selected && selectedStyle(variables)), }), ...(important && { fontWeight: 'bold', diff --git a/src/themes/teams/components/List/listItemVariables.ts b/src/themes/teams/components/List/listItemVariables.ts index 5edc096d03..e7a622765c 100644 --- a/src/themes/teams/components/List/listItemVariables.ts +++ b/src/themes/teams/components/List/listItemVariables.ts @@ -13,7 +13,9 @@ export default siteVariables => ({ contentFontSize: siteVariables.fontSizes.small, contentLineHeight: siteVariables.lineHeightSmall, - // Selection - selectionHoverColor: siteVariables.white, - selectionHoverBackgroundColor: siteVariables.brand08, + // Selectable + selectableFocusHoverColor: siteVariables.white, + selectableFocusHoverBackgroundColor: siteVariables.brand08, + selectedColor: siteVariables.black, + selectedBackgroundColor: siteVariables.gray10, }) diff --git a/test/specs/behaviors/listBehavior-test.tsx b/test/specs/behaviors/listBehavior-test.tsx index faed2115ef..eada8135d1 100644 --- a/test/specs/behaviors/listBehavior-test.tsx +++ b/test/specs/behaviors/listBehavior-test.tsx @@ -1,15 +1,15 @@ import { listBehavior } from 'src/lib/accessibility' describe('ListBehavior.ts', () => { - test('use SelectableListBehavior if selection prop is defined', () => { + test('use SelectableListBehavior if selectable prop is defined', () => { const property = { - selection: true, + selectable: true, } const expectedResult = listBehavior(property) expect(expectedResult.attributes.root.role).toEqual('listbox') }) - test('use BasicListItemBehavior if selection prop is NOT defined', () => { + test('use BasicListItemBehavior if selectable prop is NOT defined', () => { const property = {} const expectedResult = listBehavior(property) expect(expectedResult.attributes.root.role).toEqual('list') diff --git a/test/specs/behaviors/listItemBehavior-test.tsx b/test/specs/behaviors/listItemBehavior-test.tsx index 261ba29fe3..752000a625 100644 --- a/test/specs/behaviors/listItemBehavior-test.tsx +++ b/test/specs/behaviors/listItemBehavior-test.tsx @@ -1,15 +1,15 @@ import { listItemBehavior } from 'src/lib/accessibility' describe('ListItemBehavior.ts', () => { - test('use SelectableListItemBehavior if selection prop is defined', () => { + test('use SelectableListItemBehavior if selectable prop is defined', () => { const property = { - selection: true, + selectable: true, } const expectedResult = listItemBehavior(property) expect(expectedResult.attributes.root.role).toEqual('option') }) - test('use BasicListBehavior if selection prop is NOT defined', () => { + test('use BasicListBehavior if selectable prop is NOT defined', () => { const property = {} const expectedResult = listItemBehavior(property) expect(expectedResult.attributes.root.role).toEqual('listitem') diff --git a/test/specs/components/List/List-test.ts b/test/specs/components/List/List-test.ts deleted file mode 100644 index 60f4471d91..0000000000 --- a/test/specs/components/List/List-test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { isConformant, handlesAccessibility } from 'test/specs/commonTests' - -import List from 'src/components/List/List' -import implementsCollectionShorthandProp from '../../commonTests/implementsCollectionShorthandProp' -import ListItem from 'src/components/List/ListItem' - -const listImplementsCollectionShorthandProp = implementsCollectionShorthandProp(List) - -describe('List', () => { - isConformant(List) - handlesAccessibility(List, { defaultRootRole: 'list' }) - listImplementsCollectionShorthandProp('items', ListItem, { mapsValueToProp: 'content' }) -}) diff --git a/test/specs/components/List/List-test.tsx b/test/specs/components/List/List-test.tsx new file mode 100644 index 0000000000..ae7fe610bf --- /dev/null +++ b/test/specs/components/List/List-test.tsx @@ -0,0 +1,76 @@ +import * as React from 'react' + +import { isConformant, handlesAccessibility } from 'test/specs/commonTests' +import { mountWithProvider } from 'test/utils' + +import List from 'src/components/List/List' +import implementsCollectionShorthandProp from '../../commonTests/implementsCollectionShorthandProp' +import ListItem from 'src/components/List/ListItem' + +const listImplementsCollectionShorthandProp = implementsCollectionShorthandProp(List) + +describe('List', () => { + isConformant(List) + handlesAccessibility(List, { defaultRootRole: 'list' }) + listImplementsCollectionShorthandProp('items', ListItem, { mapsValueToProp: 'content' }) + + const getItems = () => [ + { key: 'irving', content: 'Irving', onClick: jest.fn() }, + { key: 'skyler', content: 'Skyler' }, + { key: 'dante', content: 'Dante' }, + ] + + describe('items', () => { + it('renders children', () => { + const listItems = mountWithProvider().find('ListItem') + expect(listItems.length).toBe(3) + expect(listItems.first().props().content).toBe('Irving') + expect(listItems.last().props().content).toBe('Dante') + }) + + it('calls onClick handler for item', () => { + const items = getItems() + const listItems = mountWithProvider().find('ListItem') + + listItems + .first() + .find('li') + .first() + .simulate('click') + expect(items[0].onClick).toHaveBeenCalled() + }) + }) + + describe('selectedIndex', () => { + it('should not be set by default', () => { + const listItems = mountWithProvider().find('ListItem') + expect(listItems.everyWhere(item => !item.props().selected)).toBe(true) + }) + + it('can be set a default value', () => { + const listItems = mountWithProvider( + , + ).find('ListItem') + expect(listItems.first().props().selected).toBe(true) + }) + + it('should be set when item is clicked', () => { + const wrapper = mountWithProvider( + , + ) + const listItems = wrapper.find('ListItem') + expect(listItems.at(0).props().selected).toBe(true) + + listItems + .at(1) + .find('li') + .first() + .simulate('click') + + const updatedItems = wrapper.find('ListItem') + + expect(updatedItems.at(0).props().selected).toBe(false) + expect(updatedItems.at(1).props().selected).toBe(true) + }) + }) +}) diff --git a/test/specs/lib/accessibility/FocusContainer-test.ts b/test/specs/lib/accessibility/FocusContainer-test.ts index bed2ab14da..58c98d18ff 100644 --- a/test/specs/lib/accessibility/FocusContainer-test.ts +++ b/test/specs/lib/accessibility/FocusContainer-test.ts @@ -11,22 +11,22 @@ describe('Focus Container', () => { const focusContainer = createFocusContainer() expect(focusContainer).toBeDefined() - expect(focusContainer.getFocusedItemIndex()).toBe(0) + expect(focusContainer.getFocusedIndex()).toBe(0) }) describe('sync item index', () => { test('should set focus item index', () => { const focusContainer = createFocusContainer({ itemsCount: 5 }) - focusContainer.syncFocusedItemIndex(4) + focusContainer.syncFocusedIndex(4) - expect(focusContainer.getFocusedItemIndex()).toBe(4) + expect(focusContainer.getFocusedIndex()).toBe(4) }) test('should not set focus index function', () => { const setFocusAt = jest.fn() const focusContainer = createFocusContainer({ itemsCount: 5, setFocusAtFn: setFocusAt }) - focusContainer.syncFocusedItemIndex(4) + focusContainer.syncFocusedIndex(4) expect(setFocusAt).not.toBeCalled() }) }) @@ -34,10 +34,10 @@ describe('Focus Container', () => { describe('move previous', () => { test('should decrement index of focused item', () => { const focusContainer = createFocusContainer({ itemsCount: 5 }) - focusContainer.syncFocusedItemIndex(4) + focusContainer.syncFocusedIndex(4) focusContainer.movePrevious() - expect(focusContainer.getFocusedItemIndex()).toBe(3) + expect(focusContainer.getFocusedIndex()).toBe(3) }) test('should call set focus index function if there are any items', () => { @@ -58,21 +58,21 @@ describe('Focus Container', () => { test('focused item index should not ever become less than 0', () => { const focusContainer = createFocusContainer({ itemsCount: 5 }) - focusContainer.syncFocusedItemIndex(0) + focusContainer.syncFocusedIndex(0) focusContainer.movePrevious() - expect(focusContainer.getFocusedItemIndex()).toBe(0) + expect(focusContainer.getFocusedIndex()).toBe(0) }) }) describe('move next', () => { test('should increment index of focused item', () => { const focusContainer = createFocusContainer({ itemsCount: 5 }) - focusContainer.syncFocusedItemIndex(3) + focusContainer.syncFocusedIndex(3) focusContainer.moveNext() - expect(focusContainer.getFocusedItemIndex()).toBe(4) + expect(focusContainer.getFocusedIndex()).toBe(4) }) test('should call set focus index function if there are any items', () => { @@ -93,21 +93,21 @@ describe('Focus Container', () => { test('focused item index should not exceed range of valid indexes', () => { const focusContainer = createFocusContainer({ itemsCount: 5 }) - focusContainer.syncFocusedItemIndex(4) + focusContainer.syncFocusedIndex(4) focusContainer.moveNext() - expect(focusContainer.getFocusedItemIndex()).toBe(4) + expect(focusContainer.getFocusedIndex()).toBe(4) }) }) describe('move first', () => { test('should set focused item index to 0', () => { const focusContainer = createFocusContainer({ itemsCount: 5 }) - focusContainer.syncFocusedItemIndex(3) + focusContainer.syncFocusedIndex(3) focusContainer.moveFirst() - expect(focusContainer.getFocusedItemIndex()).toBe(0) + expect(focusContainer.getFocusedIndex()).toBe(0) }) test('should call set focus index function if there are any items', () => { @@ -130,10 +130,10 @@ describe('Focus Container', () => { describe('move last', () => { test('should set focused item index to last index of valid range', () => { const focusContainer = createFocusContainer({ itemsCount: 5 }) - focusContainer.syncFocusedItemIndex(2) + focusContainer.syncFocusedIndex(2) focusContainer.moveLast() - expect(focusContainer.getFocusedItemIndex()).toBe(4) + expect(focusContainer.getFocusedIndex()).toBe(4) }) test('should call set focus index function if there are any items', () => {