-
Notifications
You must be signed in to change notification settings - Fork 231
fix(list): maintain classes with state.listItemClassNames #776
Changes from all commits
02e6701
244b124
3a90d94
272d779
54eb6ea
b14a9a9
6d1166f
3cef521
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,6 +51,11 @@ export interface ListProps<T extends HTMLElement> extends React.HTMLProps<HTMLEl | |
children: ListItem<T> | ListItem<T>[] | React.ReactNode; | ||
}; | ||
|
||
|
||
interface ListState { | ||
listItemClassNames: {[listItemIndex: number]: string[]}, | ||
} | ||
|
||
function isReactText(element: any): element is React.ReactText { | ||
return typeof element === 'string' || typeof element === 'number'; | ||
} | ||
|
@@ -63,13 +68,18 @@ function isSelectedIndexType(selectedIndex: unknown): selectedIndex is MDCListIn | |
return typeof selectedIndex === 'number' && !isNaN(selectedIndex) || Array.isArray(selectedIndex); | ||
} | ||
|
||
export default class List<T extends HTMLElement = HTMLElement> extends React.Component<ListProps<T>, {}> { | ||
export default class List<T extends HTMLElement = HTMLElement> extends React.Component<ListProps<T>, ListState> { | ||
listItemCount = 0; | ||
foundation!: MDCListFoundation; | ||
hasInitializedListItem = false; | ||
hasInitializedListItemTabIndex = false; | ||
hasInitializedList = false; | ||
|
||
private listElement = React.createRef<HTMLElement>(); | ||
|
||
state: ListState = { | ||
listItemClassNames: {}, | ||
}; | ||
|
||
static defaultProps: Partial<ListProps<HTMLElement>> = { | ||
'className': '', | ||
'checkboxList': false, | ||
|
@@ -100,6 +110,11 @@ export default class List<T extends HTMLElement = HTMLElement> extends React.Com | |
this.props[ARIA_ORIENTATION] === VERTICAL | ||
); | ||
this.initializeListType(); | ||
|
||
// tabIndex for the list items can only be initialized after | ||
// the above logic has executed. Once this is true, we need to call forceUpdate. | ||
this.hasInitializedList = true; | ||
this.forceUpdate(); | ||
} | ||
|
||
componentDidUpdate(prevProps: ListProps<T>) { | ||
|
@@ -185,16 +200,30 @@ export default class List<T extends HTMLElement = HTMLElement> extends React.Com | |
listItem.setAttribute(attr, value); | ||
} | ||
}, | ||
/** | ||
* Pushes class name to state.listItemClassNames[listItemIndex] if it doesn't yet exist. | ||
*/ | ||
addClassForElementIndex: (index, className) => { | ||
const listItem = this.listElements[index]; | ||
if (listItem) { | ||
listItem.classList.add(className); | ||
const {listItemClassNames} = this.state; | ||
if (listItemClassNames[index] && listItemClassNames[index].indexOf(className) === -1) { | ||
listItemClassNames[index].push(className); | ||
} else { | ||
listItemClassNames[index] = [className]; | ||
} | ||
this.setState({listItemClassNames}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you test this with disabled list item?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good catch - updated and added tests |
||
}, | ||
/** | ||
* Finds the className within state.listItemClassNames[listItemIndex], and removes it | ||
* from the array. | ||
*/ | ||
removeClassForElementIndex: (index, className) => { | ||
const listItem = this.listElements[index]; | ||
if (listItem) { | ||
listItem.classList.remove(className); | ||
const {listItemClassNames} = this.state; | ||
if (listItemClassNames[index]) { | ||
const removalIndex = listItemClassNames[index].indexOf(className); | ||
if (removalIndex !== -1) { | ||
listItemClassNames[index].splice(removalIndex, 1); | ||
this.setState({listItemClassNames}); | ||
} | ||
} | ||
}, | ||
setTabIndexForListItemChildren: (listItemIndex, tabIndexValue) => { | ||
|
@@ -251,6 +280,44 @@ export default class List<T extends HTMLElement = HTMLElement> extends React.Com | |
return null; | ||
} | ||
|
||
/** | ||
* Initializes the tabIndex prop for the listItems. tabIndex is determined by: | ||
* 1. if selectedIndex is an array, and the index === selectedIndex[0] | ||
* 2. if selectedIndex is a number, and the the index === selectedIndex | ||
* 3. if there is no selectedIndex | ||
*/ | ||
private getListItemInitialTabIndex = (index: number) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To simplify this, I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @abhiomkar you're saying it doesn't matter if its checkbox or radio type list? It should only be set to the first selectedIndex? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's correct. See Accessibility section and examples for reference: https://github.com/material-components/material-components-web/tree/master/packages/mdc-list#accessibility There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm...ya that sounds a lot simpler than what i have...I will update. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ya that worked like a charm. Thanks! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: missing return type. And in other places. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is better for the TS compiler to infer the type here. Otherwise the return type is |
||
const {selectedIndex} = this.props; | ||
let tabIndex = {}; | ||
if (this.hasInitializedList && !this.hasInitializedListItemTabIndex) { | ||
const isSelectedIndexArray | ||
= Array.isArray(selectedIndex) && selectedIndex.length > 0 && index === selectedIndex[0]; | ||
const isSelected = selectedIndex === index; | ||
if (isSelectedIndexArray || isSelected || selectedIndex === -1) { | ||
tabIndex = {tabIndex: 0}; | ||
this.hasInitializedListItemTabIndex = true; | ||
} | ||
} | ||
|
||
return tabIndex; | ||
} | ||
|
||
/** | ||
* Method checks if the list item at `index` contains classes. If true, | ||
* method merges state.listItemClassNames[index] with listItem.props.className. | ||
* The return value is used as the listItem's className. | ||
*/ | ||
private getListItemClassNames = (index: number, listItem: React.ReactElement<ListItemProps<T>>) => { | ||
let {className = ''} = listItem.props; | ||
const {listItemClassNames} = this.state; | ||
if (listItemClassNames[index]) { | ||
listItemClassNames[index]; | ||
className = classnames(className, listItemClassNames[index]); | ||
} | ||
|
||
return className; | ||
} | ||
|
||
handleKeyDown = (e: React.KeyboardEvent<any>, index: number) => { | ||
e.persist(); // Persist the synthetic event to access its `key` | ||
this.foundation.handleKeydown( | ||
|
@@ -317,46 +384,33 @@ export default class List<T extends HTMLElement = HTMLElement> extends React.Com | |
|
||
renderChild = (child: React.ReactElement<ListItemProps<T>> | React.ReactChild) => { | ||
if (!isReactText(child) && isListItem(child)) { | ||
return this.renderListItem(child, this.listItemCount++); | ||
return this.renderListItem(child, child.props.disabled ? -1 : this.listItemCount++); | ||
} | ||
return child; | ||
}; | ||
|
||
renderListItem = (listItem: React.ReactElement<ListItemProps<T>>, index: number) => { | ||
const {checkboxList, radioList, selectedIndex} = this.props; | ||
let tabIndex = {}; | ||
const {checkboxList, radioList} = this.props; | ||
const tabIndex = this.getListItemInitialTabIndex(index); | ||
const className = this.getListItemClassNames(index, listItem); | ||
|
||
const { | ||
onKeyDown, | ||
onClick, | ||
onFocus, | ||
onBlur, | ||
/* eslint-disable no-unused-vars */ | ||
className: _classNames, | ||
/* eslint-enable no-unused-vars */ | ||
...otherProps | ||
} = listItem.props; | ||
|
||
if (!this.hasInitializedListItem) { | ||
const isSelectedIndexArray = Array.isArray(selectedIndex) && selectedIndex.length > 0; | ||
// if selectedIndex is populated then check if its a checkbox/radioList | ||
if (selectedIndex && (isSelectedIndexArray || selectedIndex > -1)) { | ||
const isCheckboxListSelected | ||
= checkboxList && Array.isArray(selectedIndex) && selectedIndex.indexOf(index) > -1; | ||
const isNonCheckboxListSelected = selectedIndex === index; | ||
if (isCheckboxListSelected || isNonCheckboxListSelected) { | ||
tabIndex = {tabIndex: 0}; | ||
this.hasInitializedListItem = true; | ||
} | ||
// set tabIndex=0 to first listItem if selectedIndex is not populated | ||
} else { | ||
tabIndex = {tabIndex: 0}; | ||
this.hasInitializedListItem = true; | ||
} | ||
} | ||
|
||
|
||
const props = { | ||
// otherProps must come first | ||
// otherProps must be first | ||
...otherProps, | ||
checkboxList, | ||
radioList, | ||
className, | ||
onKeyDown: (e: React.KeyboardEvent<T>) => { | ||
onKeyDown!(e); | ||
this.handleKeyDown(e, index); | ||
|
@@ -373,6 +427,11 @@ export default class List<T extends HTMLElement = HTMLElement> extends React.Com | |
onBlur!(e); | ||
this.handleBlur(e, index); | ||
}, | ||
onDestroy: () => { | ||
const {listItemClassNames} = this.state; | ||
delete listItemClassNames[index]; | ||
this.setState({listItemClassNames}); | ||
}, | ||
...tabIndex, | ||
}; | ||
return React.cloneElement(listItem, props); | ||
|
Uh oh!
There was an error while loading. Please reload this page.