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

Commit d1a9105

Browse files
authored
fix(SelectableList): Items in list should be selectable (#566)
* 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
1 parent 98af14c commit d1a9105

25 files changed

+316
-173
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1919

2020
### Fixes
2121
- Ensure `Popup` properly flips values of `offset` prop in RTL @kuzhelov ([#612](https://github.com/stardust-ui/react/pull/612))
22+
- Fix `List` - items should be selectable @sophieH29 ([#566](https://github.com/stardust-ui/react/pull/566))
2223

2324
### Features
2425
- Add `color` prop to `Text` component @Bugaa92 ([#597](https://github.com/stardust-ui/react/pull/597))

docs/src/examples/components/List/Content/ListExampleEndMedia.shorthand.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@ const items = [
2121
},
2222
]
2323

24-
const ListExample = () => <List items={items} selection />
24+
const ListExample = () => <List items={items} selectable />
2525

2626
export default ListExample

docs/src/examples/components/List/Content/ListExampleEndMedia.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ const ListExample = () => (
88
<List.Item
99
content="Program the sensor to the SAS alarm through the haptic SQL card!"
1010
endMedia={ellipsis}
11-
selection
11+
selectable
1212
/>
1313
<List.Item
1414
content="Use the online FTP application to input the multi-byte application!"
1515
endMedia={ellipsis}
16-
selection
16+
selectable
1717
/>
1818
<List.Item
1919
content="The GB pixel is down, navigate the virtual interface!"
2020
endMedia={ellipsis}
21-
selection
21+
selectable
2222
/>
2323
</List>
2424
)

docs/src/examples/components/List/Types/ListExample.shorthand.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ const items = [
2525
},
2626
]
2727

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

30-
export default ListExampleSelection
30+
export default ListExampleSelectable

docs/src/examples/components/List/Types/ListExample.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react'
22
import { List, Image } from '@stardust-ui/react'
33

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

27-
export default ListExampleSelection
27+
export default ListExampleSelectable

docs/src/examples/components/List/Types/ListExampleSelection.shorthand.tsx renamed to docs/src/examples/components/List/Types/ListExampleSelectable.shorthand.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ const items = [
2525
},
2626
]
2727

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

30-
const ListExampleSelection = ({ knobs }) => <List selection={selection(knobs)} items={items} />
31-
32-
export default ListExampleSelection
30+
export default ListExampleSelectable
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,30 @@
11
import React from 'react'
22
import { List, Image } from '@stardust-ui/react'
33

4-
const selection = knobs => (knobs === undefined ? true : knobs.selection)
5-
6-
const ListExampleSelection = ({ knobs }) => (
7-
<List selection={selection(knobs)}>
4+
const ListExampleSelectable = () => (
5+
<List selectable>
86
<List.Item
97
media={<Image src="public/images/avatar/small/matt.jpg" avatar />}
108
header="Irving Kuhic"
119
headerMedia="7:26:56 AM"
1210
content="Program the sensor to the SAS alarm through the haptic SQL card!"
13-
selection={selection(knobs)}
11+
selectable
1412
/>
1513
<List.Item
1614
media={<Image src="public/images/avatar/small/steve.jpg" avatar />}
1715
header="Skyler Parks"
1816
headerMedia="11:30:17 PM"
1917
content="Use the online FTP application to input the multi-byte application!"
20-
selection={selection(knobs)}
18+
selectable
2119
/>
2220
<List.Item
2321
media={<Image src="public/images/avatar/small/nom.jpg" avatar />}
2422
header="Dante Schneider"
2523
headerMedia="5:22:40 PM"
2624
content="The GB pixel is down, navigate the virtual interface!"
27-
selection={selection(knobs)}
25+
selectable
2826
/>
2927
</List>
3028
)
3129

32-
export default ListExampleSelection
30+
export default ListExampleSelectable
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as React from 'react'
2+
import { List, Image } from '@stardust-ui/react'
3+
4+
class SelectableListControlledExample extends React.Component<any, any> {
5+
state = { selectedIndex: -1 }
6+
7+
items = [
8+
{
9+
key: 'irving',
10+
media: <Image src="public/images/avatar/small/matt.jpg" avatar />,
11+
header: 'Irving Kuhic',
12+
headerMedia: '7:26:56 AM',
13+
content: 'Program the sensor to the SAS alarm through the haptic SQL card!',
14+
},
15+
{
16+
key: 'skyler',
17+
media: <Image src="public/images/avatar/small/steve.jpg" avatar />,
18+
header: 'Skyler Parks',
19+
headerMedia: '11:30:17 PM',
20+
content: 'Use the online FTP application to input the multi-byte application!',
21+
},
22+
{
23+
key: 'dante',
24+
media: <Image src="public/images/avatar/small/nom.jpg" avatar />,
25+
header: 'Dante Schneider',
26+
headerMedia: '5:22:40 PM',
27+
content: 'The GB pixel is down, navigate the virtual interface!',
28+
},
29+
]
30+
31+
render() {
32+
return (
33+
<List
34+
selectable
35+
selectedIndex={this.state.selectedIndex}
36+
onSelectedIndexChange={(e, newProps) => {
37+
alert(
38+
`List is requested to change its selectedIndex state to "${newProps.selectedIndex}"`,
39+
)
40+
this.setState({ selectedIndex: newProps.selectedIndex })
41+
}}
42+
items={this.items}
43+
/>
44+
)
45+
}
46+
}
47+
48+
export default SelectableListControlledExample

docs/src/examples/components/List/Types/ListExampleSelection.knobs.tsx

Lines changed: 0 additions & 24 deletions
This file was deleted.

docs/src/examples/components/List/Types/index.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react'
1+
import * as React from 'react'
22
import ComponentExample from 'docs/src/components/ComponentDoc/ComponentExample'
33
import ExampleSection from 'docs/src/components/ComponentDoc/ExampleSection'
44

@@ -10,9 +10,14 @@ const Types = () => (
1010
examplePath="components/List/Types/ListExample"
1111
/>
1212
<ComponentExample
13-
title="Selection"
13+
title="Selectable list"
1414
description="A list can be formatted to indicate that its items can be selected."
15-
examplePath="components/List/Types/ListExampleSelection"
15+
examplePath="components/List/Types/ListExampleSelectable"
16+
/>
17+
<ComponentExample
18+
title="Controlled selectable list"
19+
description="List can handle selected index in controlled mode."
20+
examplePath="components/List/Types/ListExampleSelectableControlled"
1621
/>
1722
</ExampleSection>
1823
)

docs/src/prototypes/SearchPage/SearchPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class SearchPage extends React.Component<SearchPageState, any> {
9797
Results <strong>{results.length}</strong> of <strong>{DATA_RECORDS.length}</strong>
9898
</small>
9999
</p>
100-
<List selection items={results.map(dataRecordToListItem)} />
100+
<List selectable items={results.map(dataRecordToListItem)} />
101101
</div>
102102
)}
103103
</Segment>

src/components/List/List.tsx

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as PropTypes from 'prop-types'
66
import {
77
customPropTypes,
88
childrenExist,
9-
UIComponent,
9+
AutoControlledComponent,
1010
UIComponentProps,
1111
ChildrenComponentProps,
1212
commonPropTypes,
@@ -15,7 +15,7 @@ import ListItem from './ListItem'
1515
import { listBehavior } from '../../lib/accessibility'
1616
import { Accessibility, AccessibilityActionHandlers } from '../../lib/accessibility/types'
1717
import { ContainerFocusHandler } from '../../lib/accessibility/FocusHandling/FocusContainer'
18-
import { Extendable, ShorthandValue } from '../../../types/utils'
18+
import { Extendable, ShorthandValue, ComponentEventHandler } from '../../../types/utils'
1919

2020
export interface ListProps extends UIComponentProps, ChildrenComponentProps {
2121
/**
@@ -30,8 +30,21 @@ export interface ListProps extends UIComponentProps, ChildrenComponentProps {
3030
/** Shorthand array of props for ListItem. */
3131
items?: ShorthandValue[]
3232

33-
/** A selection list formats list items as possible choices. */
34-
selection?: boolean
33+
/** A selectable list formats list items as possible choices. */
34+
selectable?: boolean
35+
36+
/** Index of the currently selected item. */
37+
selectedIndex?: number
38+
39+
/** Initial selectedIndex value. */
40+
defaultSelectedIndex?: number
41+
42+
/**
43+
* Event for request to change 'selectedIndex' value.
44+
* @param {SyntheticEvent} event - React's original SyntheticEvent.
45+
* @param {object} data - All props and proposed value.
46+
*/
47+
onSelectedIndexChange?: ComponentEventHandler<ListProps>
3548

3649
/** Truncates content */
3750
truncateContent?: boolean
@@ -41,13 +54,14 @@ export interface ListProps extends UIComponentProps, ChildrenComponentProps {
4154
}
4255

4356
export interface ListState {
44-
selectedItemIndex: number
57+
focusedIndex: number
58+
selectedIndex?: number
4559
}
4660

4761
/**
4862
* A list displays a group of related content.
4963
*/
50-
class List extends UIComponent<Extendable<ListProps>, ListState> {
64+
class List extends AutoControlledComponent<Extendable<ListProps>, ListState> {
5165
static displayName = 'List'
5266

5367
static className = 'ui-list'
@@ -59,24 +73,28 @@ class List extends UIComponent<Extendable<ListProps>, ListState> {
5973
accessibility: PropTypes.func,
6074
debug: PropTypes.bool,
6175
items: customPropTypes.collectionShorthand,
62-
selection: PropTypes.bool,
76+
selectable: PropTypes.bool,
6377
truncateContent: PropTypes.bool,
6478
truncateHeader: PropTypes.bool,
79+
selectedIndex: PropTypes.number,
80+
defaultSelectedIndex: PropTypes.number,
81+
onSelectedIndexChange: PropTypes.func,
6582
}
6683

6784
static defaultProps = {
6885
as: 'ul',
6986
accessibility: listBehavior as Accessibility,
7087
}
7188

89+
static autoControlledProps = ['selectedIndex']
90+
getInitialAutoControlledState() {
91+
return { selectedIndex: -1, focusedIndex: 0 }
92+
}
93+
7294
static Item = ListItem
7395

7496
// List props that are passed to each individual Item props
75-
static itemProps = ['debug', 'selection', 'truncateContent', 'truncateHeader', 'variables']
76-
77-
public state = {
78-
selectedItemIndex: 0,
79-
}
97+
static itemProps = ['debug', 'selectable', 'truncateContent', 'truncateHeader', 'variables']
8098

8199
private focusHandler: ContainerFocusHandler = null
82100
private itemRefs = []
@@ -100,6 +118,22 @@ class List extends UIComponent<Extendable<ListProps>, ListState> {
100118
},
101119
}
102120

121+
constructor(props, context) {
122+
super(props, context)
123+
124+
this.focusHandler = new ContainerFocusHandler(
125+
() => this.props.items.length,
126+
index => {
127+
this.setState({ focusedIndex: index }, () => {
128+
const targetComponent = this.itemRefs[index] && this.itemRefs[index].current
129+
const targetDomNode = ReactDOM.findDOMNode(targetComponent) as any
130+
131+
targetDomNode && targetDomNode.focus()
132+
})
133+
},
134+
)
135+
}
136+
103137
renderComponent({ ElementType, classes, accessibility, rest }) {
104138
const { children } = this.props
105139

@@ -115,36 +149,32 @@ class List extends UIComponent<Extendable<ListProps>, ListState> {
115149
)
116150
}
117151

118-
componentDidMount() {
119-
this.focusHandler = new ContainerFocusHandler(
120-
() => this.props.items.length,
121-
index => {
122-
this.setState({ selectedItemIndex: index }, () => {
123-
const targetComponent = this.itemRefs[index] && this.itemRefs[index].current
124-
const targetDomNode = ReactDOM.findDOMNode(targetComponent) as any
125-
126-
targetDomNode && targetDomNode.focus()
127-
})
128-
},
129-
)
130-
}
131-
132152
renderItems() {
133153
const { items } = this.props
134-
const { selectedItemIndex } = this.state
154+
const { focusedIndex, selectedIndex } = this.state
155+
156+
this.focusHandler.syncFocusedIndex(focusedIndex)
135157

136158
this.itemRefs = []
137159

138160
return _.map(items, (item, idx) => {
139161
const maybeSelectableItemProps = {} as any
140162

141-
if (this.props.selection) {
163+
if (this.props.selectable) {
142164
const ref = React.createRef()
143165
this.itemRefs[idx] = ref
144166

145-
maybeSelectableItemProps.tabIndex = idx === selectedItemIndex ? 0 : -1
146167
maybeSelectableItemProps.ref = ref
147-
maybeSelectableItemProps.onFocus = () => this.focusHandler.syncFocusedItemIndex(idx)
168+
maybeSelectableItemProps.onFocus = () => this.setState({ focusedIndex: idx })
169+
maybeSelectableItemProps.onClick = e => {
170+
this.trySetState({ selectedIndex: idx })
171+
_.invoke(this.props, 'onSelectedIndexChange', e, {
172+
...this.props,
173+
...{ selectedIndex: idx },
174+
})
175+
}
176+
maybeSelectableItemProps.selected = idx === selectedIndex
177+
maybeSelectableItemProps.tabIndex = idx === focusedIndex ? 0 : -1
148178
}
149179

150180
const itemProps = {

0 commit comments

Comments
 (0)