Skip to content
This repository has been archived by the owner on Mar 4, 2020. It is now read-only.

Commit

Permalink
fix(SelectableList): Items in list should be selectable (#566)
Browse files Browse the repository at this point in the history
* Reflect which item is selected in list

* Make list derived from autocontrolled component

* small fix

* Update ListExampleSelection.tsx

* Update ListExampleSelection.shorthand.tsx

* Small improvement

* Rename *ItemIndex -> *Index

* Names refactoring

* Minor improvements

* update changelog

* Add onSelectedIndexChange

* Add some tests

* Small improvements afer CR

* Small improvements afer CR

* Small improvements afer CR

* create focus handler when List is constructed

* fix changelog

* changelog
  • Loading branch information
sophieH29 authored and manajdov committed Dec 19, 2018
1 parent 4895301 commit 3c6e75e
Show file tree
Hide file tree
Showing 25 changed files with 316 additions and 173 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ const items = [
},
]

const ListExample = () => <List items={items} selection />
const ListExample = () => <List items={items} selectable />

export default ListExample
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ const ListExample = () => (
<List.Item
content="Program the sensor to the SAS alarm through the haptic SQL card!"
endMedia={ellipsis}
selection
selectable
/>
<List.Item
content="Use the online FTP application to input the multi-byte application!"
endMedia={ellipsis}
selection
selectable
/>
<List.Item
content="The GB pixel is down, navigate the virtual interface!"
endMedia={ellipsis}
selection
selectable
/>
</List>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ const items = [
},
]

const ListExampleSelection = ({ knobs }) => <List debug={knobs.debug} items={items} />
const ListExampleSelectable = ({ knobs }) => <List debug={knobs.debug} items={items} />

export default ListExampleSelection
export default ListExampleSelectable
4 changes: 2 additions & 2 deletions docs/src/examples/components/List/Types/ListExample.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'
import { List, Image } from '@stardust-ui/react'

const ListExampleSelection = ({ knobs }) => (
const ListExampleSelectable = ({ knobs }) => (
<List debug={knobs.debug}>
<List.Item
media={<Image src="public/images/avatar/small/matt.jpg" avatar />}
Expand All @@ -24,4 +24,4 @@ const ListExampleSelection = ({ knobs }) => (
</List>
)

export default ListExampleSelection
export default ListExampleSelectable
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ const items = [
},
]

const selection = knobs => (knobs === undefined ? true : knobs.selection)
const ListExampleSelectable = () => <List selectable defaultSelectedIndex={0} items={items} />

const ListExampleSelection = ({ knobs }) => <List selection={selection(knobs)} items={items} />

export default ListExampleSelection
export default ListExampleSelectable
Original file line number Diff line number Diff line change
@@ -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 }) => (
<List selection={selection(knobs)}>
const ListExampleSelectable = () => (
<List selectable>
<List.Item
media={<Image src="public/images/avatar/small/matt.jpg" avatar />}
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
/>
<List.Item
media={<Image src="public/images/avatar/small/steve.jpg" avatar />}
header="Skyler Parks"
headerMedia="11:30:17 PM"
content="Use the online FTP application to input the multi-byte application!"
selection={selection(knobs)}
selectable
/>
<List.Item
media={<Image src="public/images/avatar/small/nom.jpg" avatar />}
header="Dante Schneider"
headerMedia="5:22:40 PM"
content="The GB pixel is down, navigate the virtual interface!"
selection={selection(knobs)}
selectable
/>
</List>
)

export default ListExampleSelection
export default ListExampleSelectable
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as React from 'react'
import { List, Image } from '@stardust-ui/react'

class SelectableListControlledExample extends React.Component<any, any> {
state = { selectedIndex: -1 }

items = [
{
key: 'irving',
media: <Image src="public/images/avatar/small/matt.jpg" avatar />,
header: 'Irving Kuhic',
headerMedia: '7:26:56 AM',
content: 'Program the sensor to the SAS alarm through the haptic SQL card!',
},
{
key: 'skyler',
media: <Image src="public/images/avatar/small/steve.jpg" avatar />,
header: 'Skyler Parks',
headerMedia: '11:30:17 PM',
content: 'Use the online FTP application to input the multi-byte application!',
},
{
key: 'dante',
media: <Image src="public/images/avatar/small/nom.jpg" avatar />,
header: 'Dante Schneider',
headerMedia: '5:22:40 PM',
content: 'The GB pixel is down, navigate the virtual interface!',
},
]

render() {
return (
<List
selectable
selectedIndex={this.state.selectedIndex}
onSelectedIndexChange={(e, newProps) => {
alert(
`List is requested to change its selectedIndex state to "${newProps.selectedIndex}"`,
)
this.setState({ selectedIndex: newProps.selectedIndex })
}}
items={this.items}
/>
)
}
}

export default SelectableListControlledExample

This file was deleted.

11 changes: 8 additions & 3 deletions docs/src/examples/components/List/Types/index.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -10,9 +10,14 @@ const Types = () => (
examplePath="components/List/Types/ListExample"
/>
<ComponentExample
title="Selection"
title="Selectable list"
description="A list can be formatted to indicate that its items can be selected."
examplePath="components/List/Types/ListExampleSelection"
examplePath="components/List/Types/ListExampleSelectable"
/>
<ComponentExample
title="Controlled selectable list"
description="List can handle selected index in controlled mode."
examplePath="components/List/Types/ListExampleSelectableControlled"
/>
</ExampleSection>
)
Expand Down
2 changes: 1 addition & 1 deletion docs/src/prototypes/SearchPage/SearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class SearchPage extends React.Component<SearchPageState, any> {
Results <strong>{results.length}</strong> of <strong>{DATA_RECORDS.length}</strong>
</small>
</p>
<List selection items={results.map(dataRecordToListItem)} />
<List selectable items={results.map(dataRecordToListItem)} />
</div>
)}
</Segment>
Expand Down
90 changes: 60 additions & 30 deletions src/components/List/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as PropTypes from 'prop-types'
import {
customPropTypes,
childrenExist,
UIComponent,
AutoControlledComponent,
UIComponentProps,
ChildrenComponentProps,
commonPropTypes,
Expand All @@ -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 {
/**
Expand All @@ -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<ListProps>

/** Truncates content */
truncateContent?: boolean
Expand All @@ -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<Extendable<ListProps>, ListState> {
class List extends AutoControlledComponent<Extendable<ListProps>, ListState> {
static displayName = 'List'

static className = 'ui-list'
Expand All @@ -59,24 +73,28 @@ class List extends UIComponent<Extendable<ListProps>, 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 = {
as: 'ul',
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 = []
Expand All @@ -100,6 +118,22 @@ class List extends UIComponent<Extendable<ListProps>, 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

Expand All @@ -115,36 +149,32 @@ class List extends UIComponent<Extendable<ListProps>, 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 = {
Expand Down
Loading

0 comments on commit 3c6e75e

Please sign in to comment.