Skip to content
This repository was archived by the owner on Jan 14, 2025. It is now read-only.

feat(list): Add component #339

Merged
merged 37 commits into from
Oct 24, 2018
Merged

feat(list): Add component #339

merged 37 commits into from
Oct 24, 2018

Conversation

bonniezhou
Copy link
Contributor

Fixes #189

@bonniezhou bonniezhou requested a review from moog16 October 16, 2018 01:11
Copy link

@moog16 moog16 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still reviewing list/index.test.js, but wanted to send you this ahead of time.

ListItem.propTypes = {
childrenTabIndex: PropTypes.number,
className: PropTypes.string,
primaryText: PropTypes.string,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think primaryText and secondaryText should be element, because they may want to wrap them in or something else.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually it might be oneOfType(element, string)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give me an example? does MDC Web allow for text to be other elements?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There might be a text formatter or date formatter out there that you would use as a component. IE:

<ListItem
  primaryText={<DateFormatter date={new Date()} />} />

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

ref: (listItem) => !this.listItems_[index] && this.listItems_.push(listItem),
...otherProps,
},
{className: this.getListItemClasses_(index)},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can wrap these in one object:

{
      updateClassList: this.updateListItemClassList,
      ref: (listItem) => !this.listItems_[index] && this.listItems_.push(listItem),
      className: this.getListItemClasses_(index)
      childrenTabIndex: this.state.listItemChildrenTabIndex[index],
      ...otherProps,
    },

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really know why but childrenTabIndex doesn't seem to be propagating if we do it this way

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it included in otherProps? If so, otherProps.childrenTabIndex will take precedence

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, works now!

...otherProps
} = graphic.props;
const props = {
tabIndex: tabIndex != undefined ? this.props.childrenTabIndex : -1,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is strange...shouldn't they be able to override tabIndex if they pass it through the graphic element? I don't see this logic in MDC Web list. Is this something that needs to be here?

Also !==

And if it is undefined doesn't the defaultProp take its place?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is kinda weird...basically there are elements whose tabIndex we want the parent list item to be able to toggle between 0 and -1 (like button/icon/input/link), and elements whose tabIndex should always be -1 (like span/div/leading icons).

In MDC Web we just set button and a elements to be togglable, and all other elements to have tabIndex always -1. I think MDC Web is missing some functionality, for example icons should be togglable too.

So I wanted to make it up to the user to set the child elements to be togglable, while the rest default to be untogglable (tabIndex always -1). I think the idea is to completely control tabIndex within the list, meaning the user cannot override it. This is for accessibility. I redid it after splitting out the graphic/text/meta out into subcomponents, let me know what you think.

renderText(primaryText, secondaryText) {
if (secondaryText) {
return (
<span className='mdc-list-item__text'>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should make the follow separate elements that we provide:

mdc-list-item__text <ListItemText />
mdc-list-item__primary-text <ListItemPrimaryText />
mdc-list-item__secondary-text <ListItemSecondaryTexst />
mdc-list-item__graphic <ListItemGraphic />
mdc-list-item__meta <ListItemMeta />

OR we allow the user to do this both ways. So we can keep props inline, and provide the above elements, but then we would need to render children too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I broke them out into 3 subcomponents because have 3 separate text components seemed overkill

}

componentDidMount() {
for (let i = 0; i < this.listItems_.length; i++) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this a forEach. It reads better.

}

getListItemIndexOfElement_ = (element) => {
for (let i = 0; i < this.listItems_.length; i++) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're using a for here, but what if we add the polyfill for findIndex? I remember you doing this somewwhere else too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What polyfill?

removeAttributeForElementIndex: (index, attr) => {
const listItemAttributes = this.state.listItemAttributes;
attr = attr === 'tabindex' ? 'tabIndex' : attr;
listItemAttributes[index][attr] = null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you actually have to remove it from the object?

toggleCheckbox: (index) => {
const listItem = this.listItems_[index];
if (listItem) {
listItem.toggleCheckbox();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still need to implement toggleCheckbox()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think I'll file an issue and work on it separately

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (index >= 0) {
this.foundation_.handleKeydown(e, true /* isRootListItem is true if index >= 0 */, index);
}
this.props.onKeyDown(e);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the props.onEvent methods above. I think we should allow the developer to setup the dom before we do anything if needed. I cant think of why they would need to, but that's the pattern I've been following. What are your thoughts?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same with click, focus, and blur

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM


getListItemClasses_(index) {
const {listItemClassList} = this.state;
return listItemClassList[index] ? classnames(Array.from(listItemClassList[index])) : '';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you also want to get the listItem.className and merge it here.

@moog16 moog16 self-assigned this Oct 17, 2018
@codecov-io
Copy link

codecov-io commented Oct 18, 2018

Codecov Report

Merging #339 into master will increase coverage by 0.19%.
The diff coverage is 99%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #339      +/-   ##
==========================================
+ Coverage   97.29%   97.49%   +0.19%     
==========================================
  Files          40       45       +5     
  Lines        1514     1714     +200     
  Branches      164      191      +27     
==========================================
+ Hits         1473     1671     +198     
- Misses         41       43       +2
Impacted Files Coverage Δ
packages/list/ListItemText.js 100% <100%> (ø)
packages/list/ListItemMeta.js 100% <100%> (ø)
packages/list/ListItemGraphic.js 100% <100%> (ø)
packages/list/ListItem.js 96% <96%> (ø)
packages/list/index.js 99.29% <99.29%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update f33a046...585f340. Read the comment docs.

Copy link

@moog16 moog16 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testing some things in the screenshot tests...I will share if I find anything interesting.

const graphicProps = {
className: this.classes,
role: 'presentation',
tabIndex: tabbableOnListItemFocus ? this.props.tabIndex : -1,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just use tabIndex here if they pass a tabIndex in from props? Why do we need this special prop? This goes for all the other components that have this prop.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talked offline: this is to determine whether the graphic tabIndex should switch from -1 to 0 when the list item is focused or if its tabIndex should always be -1.


const graphicProps = {
className: this.classes,
role: 'presentation',
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user passes in a different role, does this go with their configuration? We shouldn't overwrite.

Copy link

@moog16 moog16 Oct 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this works:

<ListItemGraphic graphic={<MaterialIcon icon='folder'/>} role='menu'/>

But this looks more intuitive:

<ListItemGraphic graphic={<MaterialIcon icon='folder' role='menu'/>}/>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, removed override.

export default class ListItemMeta extends Component {
get classes() {
const {className, meta} = this.props;
return classnames('mdc-list-item__meta', className, meta.className);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if meta.className is undefined, it will print undefined in the rendered class

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just kidding...tested this. Its fine

tabbableOnListItemFocus: PropTypes.bool,
className: PropTypes.string,
tabIndex: PropTypes.number,
meta: PropTypes.element,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should also be oneOfType(element, string)

import PropTypes from 'prop-types';
import classnames from 'classnames';

export default class ListItemText extends Component {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking we could even break this into . Not sure what you think. We'd also then need to provide another component that wraps the 2 (the element with .mdc-list-item__text). This might be simpler, so I'm all for it and just wanted to get your thoughts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talked offline, will keep as is unless there proves to be a need otherwise

};

componentDidMount() {
for (var key in this.listItems_) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use let -- its just so in right now

const {listItemAttributes} = this.state;
listItemAttributes[key] = {
'aria-selected': false,
'tabIndex': this.listItemIndices_[key] === 0 ? 0 : -1,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if someone manually changes the tabIndex to something other than 0 or -1...in this case it'll default to -1

...otherProps
} = listItem.props;

const keyOrIndex = key || index;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you have 1 or the other? I think the key should always just be the index. If you do this, you should be able to remove listItemIndicies.

setAttributeForElementIndex: (index, attr, value) => {
const {listItemAttributes} = this.state;
attr = attr === 'tabindex' ? 'tabIndex' : attr;
const key = this.getListItemKeyFromIndex_(index);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the key should always be the index. Instead of this, cant you just do this.listItems_[key]?


getListItemIndexOfTarget_ = (eventTarget) => {
// Find the first ancestor that is a list item.
const listItemElement = eventTarget.closest('.mdc-list-item');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😩

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it helps, the mdc-dom package's ponyfill module has this function

@@ -27,6 +27,11 @@ import classnames from 'classnames';
export default class ListItem extends Component {
listItemElement_ = React.createRef();

componentDidMount() {
const {init} = this.props;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just add a defaultProp for init as an empty method. You don't need to guard against it on line 32.

init();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we set a default prop here the actual prop doesn't override it....

className | String | Classes to be applied to the list item element
childrenTabIndex | Number | Tab index to be applied to all children of the list item
init | Function() => void | Callback to initialize the list item on mount
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its actually Callback executed when list item mounts

@@ -5,7 +5,7 @@ import {shallow, mount} from 'enzyme';
import List from '../../../packages/list';
import {ListItem} from '../../../packages/list';

suite.only('List');
suite('List');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a test for removing a list item and adding a list item.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return this.getIndexOfListItemElement_(target);
}

onKeyDown = (e) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move onKeyDown, onClick, onFocus, and onBlur into the renderListItem, and pass the index. We can then remove getListItemIndexOfTarget_.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants