Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(list): Add single selection #2970

Merged
merged 47 commits into from
Jul 16, 2018
Merged

Conversation

williamernest
Copy link
Contributor

No description provided.

@codecov-io
Copy link

codecov-io commented Jun 20, 2018

Codecov Report

Merging #2970 into master will increase coverage by <.01%.
The diff coverage is 98.91%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #2970      +/-   ##
==========================================
+ Coverage    98.3%   98.31%   +<.01%     
==========================================
  Files         101      101              
  Lines        4368     4451      +83     
  Branches      564      585      +21     
==========================================
+ Hits         4294     4376      +82     
- Misses         74       75       +1
Impacted Files Coverage Δ
packages/mdc-list/constants.js 100% <ø> (ø) ⬆️
packages/mdc-list/foundation.js 100% <100%> (ø) ⬆️
packages/mdc-list/index.js 98.78% <97.72%> (-1.22%) ⬇️

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 c1fa93a...f1bce52. Read the comment docs.

@@ -6176,6 +6176,7 @@
"version": "0.0.9",
"bundled": true,
"dev": true,
"optional": true,
Copy link
Contributor

Choose a reason for hiding this comment

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

Was this file supposed to change?

@@ -212,7 +254,11 @@ Method Signature | Description
`getListItemCount() => Number` | Returns the total number of list items (elements with `mdc-list-item` class) that are direct children of the `root_` element.
`getFocusedElementIndex() => Number` | Returns the `index` value of the currently focused element.
`getListItemIndex(ele: Element) => Number` | Returns the `index` value of the provided `ele` element.
`setAttributeForElementIndex(ndx: Number, attr: String, value: String) => void` | Sets the `attr` attribute to the value of `value` for the list item at `ndx`.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not in love with the variable name ndx but it's already in here so I'm not gonna request a change.

if (ndx === this.selectedIndex_) {
this.adapter_.setAttributeForElementIndex(this.selectedIndex_, strings.ARIA_SELECTED, false);
this.adapter_.removeClassForElementIndex(this.selectedIndex_, cssClasses.LIST_SELECTED_CLASS);
if (this.selectedIndex_ > 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does this only set negative tabindex for items 1...n instead of 0...n?

}

if (ndx >= 0) {
if (this.adapter_.getListItemCount() > ndx) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Collapse these into one if statement

} else if (this.isSingleSelectionList_ && (isEnter || isSpace)) {
this.preventDefaultEvent_(evt);
// Check if the space key was pressed on the list item or a child element.
if (evt.target.classList.contains(cssClasses.LIST_ITEM_CLASS)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be an adapter method (hasClass or something similar)


currentIndex = this.adapter_.getListItemIndex(listItem);

if (currentIndex < 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This if statement can be removed since it always evaluates to true.

}

this.foundation_.setSingleSelection(value);
const selectedElement = this.root_.querySelector('.mdc-list-item[aria-selected="true"]');
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you provide some explanation about why this uses the aria-selected attribute instead of the mdc-list-item--selected class?


this.foundation_.setSingleSelection(value);
const selectedElement = this.root_.querySelector('.mdc-list-item[aria-selected="true"]');
[].slice.call(this.root_.querySelectorAll('.mdc-list-item:not([aria-selected="true"])'))
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you provide some explanation about why this uses the aria-selected attribute instead of the mdc-list-item--selected class?

if (!matches) { // IE uses a different name for the same functionality
matches = Element.prototype.msMatchesSelector;
}
return matches.call(ele, strings.FOCUSABLE_CHILD_ELEMENTS);
Copy link
Contributor

Choose a reason for hiding this comment

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

Clever!!!

@patrickrodee patrickrodee self-assigned this Jun 20, 2018
#### Pre-selected list item

When rendering the list with a pre-selected list item, the list item that needs to be selected should contain
the `mdc-list-item--selected` class before creating the list.
Copy link
Contributor

@patrickrodee patrickrodee Jun 21, 2018

Choose a reason for hiding this comment

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

Should the pre-selected list item also have the aria-selected="true" attribute?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, it gets added automatically when the list component is created. We add the --selected class because we use that as our selector.

Copy link
Contributor

Choose a reason for hiding this comment

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

Gotcha. Can you show me the line number where that occurs?

Copy link
Contributor Author

@williamernest williamernest Jun 21, 2018

Choose a reason for hiding this comment

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

const selectedElement = this.root_.querySelector('.mdc-list-item--selected');

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see where aria-selected="true" gets added though. Could you point me to that?

Copy link
Contributor

Choose a reason for hiding this comment

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

Cool, that makes sense. I still don't see how the default selected list item gets the aria-selected attribute though. As far as I can tell, it never gets the aria-selected attribute.

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 the selectedElement is found it sets the selectedIndex to the selectedElement index. The selectedIndex proxies to the foundation setSelectedIndex, which sets the attributes on the list element.

this.selectedIndex = this.listElements_.indexOf(selectedElement);

this.foundation_.setSelectedIndex(index);

this.adapter_.setAttributeForElementIndex(this.selectedIndex_, strings.ARIA_SELECTED, true);

Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome, thanks for the patience and guiding me through. Since that means the aria-selected attribute is only set by the MDC component index.js implementation during initialization, let's add a note to include aria-selected="true" for the pre-selected list item. That will help users who wrap our components and our users in general.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

? aria-selected is set by the foundation

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 you wrap the component and want to use the single selection list, you have to call setSelectedIndex on the foundation so that it can initialize the initial selected value. setSelectedIndex also sets aria-selected on the list element that is selected.

@@ -41,11 +41,48 @@ class MDCListAdapter {
/** @param {Element} node */
getListItemIndex(node) {}

/**
* @param {Number} index
Copy link
Contributor

Choose a reason for hiding this comment

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

These basic types (number, string, boolean, etc) should be lowercase.

FOCUSABLE_CHILD_ELEMENTS: 'button:not(:disabled), a',
ITEMS_SELECTOR: '.mdc-list-item',
ITEMS_SELECTOR: '.mdc-list-item:not(.mdc-list-item--disabled)',
Copy link
Contributor

Choose a reason for hiding this comment

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

Why did you add the :not(.mdc-list-item--disabled) selector?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Disabled items should not be receive focus, so the items selector should ignore them.

Copy link
Contributor

Choose a reason for hiding this comment

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

Changing the key to ENABLED_ITEMS_SELECTOR is more semantically correct

/** @param {boolean} isSingleSelectionList */
set singleSelection(isSingleSelectionList) {
if (isSingleSelectionList) {
this.root_.addEventListener('click', this.handleClick_);
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be a foundation method.

Copy link
Contributor Author

@williamernest williamernest Jun 28, 2018

Choose a reason for hiding this comment

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

Aren't we moving toward adding event listeners in the index.js ? See initialSyncWithDOM above.

Copy link
Contributor

Choose a reason for hiding this comment

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

True, we're exploring it. I guess we can try it here and see how it goes.

Will handleClick cause problems if it's triggered by a non-single selection list item? If so, there should be a check inside the handleClick method that ensures it won't cause problems if it's called.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

handleClick has logic to determine if you did not click on a list item (it looks for an ancestor mdc-list-item) and if doesn't find one it returns early.

/** @param {boolean} isSingleSelectionList */
set singleSelection(isSingleSelectionList) {
if (isSingleSelectionList) {
this.root_.addEventListener('click', this.handleClick_);
Copy link
Contributor

Choose a reason for hiding this comment

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

True, we're exploring it. I guess we can try it here and see how it goes.

Will handleClick cause problems if it's triggered by a non-single selection list item? If so, there should be a check inside the handleClick method that ensures it won't cause problems if it's called.

@trimox
Copy link
Contributor

trimox commented Jul 4, 2018

Hey @williamernest I opened PR #3029 to export MDCListFoundation. Would you prefer to add those exports here? If so, I'll close #3029.

@williamernest
Copy link
Contributor Author

@trimox Thanks. I updated them here so we can go ahead and close #3029

Copy link
Contributor

@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.

Left some comments on tests, which you can ignore except for one :)


The default component requires that every list item receives a `tabindex` value so that it can receive focus
(`li` elements cannot receive focus at all without a `tabindex` value). Any element not already containing a
`tabindex` attribute will receive `tabindex=-1`. The first list item should have `tabindex="0"` so that it the
Copy link
Contributor

Choose a reason for hiding this comment

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

so that the user...

(`li` elements cannot receive focus at all without a `tabindex` value). Any element not already containing a
`tabindex` attribute will receive `tabindex=-1`. The first list item should have `tabindex="0"` so that it the
user can find the first element using the `tab` key, but subsequent `tab` keys strokes will cause focus to
skip over the entire list. If the list items contain sub-elements that are focusable (`button` or `a` elements),
Copy link
Contributor

Choose a reason for hiding this comment

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

is it also true that inputs will receive a tabindex='-1'?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, we only query for button and a elements to toggle tabIndex. We removed the checkbox demos so we could focus on the base cases (button and a elements) and then add the other features later.

Copy link
Contributor

Choose a reason for hiding this comment

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

oh sorry - i meant to remove this comment.

<li class="mdc-list-item mdc-list-item--selected" aria-selected="true" tabindex="0">Single-line item</li>
<li class="mdc-list-item" tabindex="-1">Single-line item</li>
</ul>
```
Copy link
Contributor

Choose a reason for hiding this comment

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

I think adding the JS here showing to call setSelectedIndex will complete the documentation....if I'm reading that correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The documentation shows the JS in above examples. This example is discussing how the html should be rendered when creating your own component.

@@ -18,14 +18,16 @@
/** @enum {string} */
const cssClasses = {
LIST_ITEM_CLASS: 'mdc-list-item',
LIST_SELECTED_CLASS: 'mdc-list-item--selected',
Copy link
Contributor

Choose a reason for hiding this comment

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

LIST_ITEM_SELECTED_CLASS is more consistent with the above class

FOCUSABLE_CHILD_ELEMENTS: 'button:not(:disabled), a',
ITEMS_SELECTOR: '.mdc-list-item',
ITEMS_SELECTOR: '.mdc-list-item:not(.mdc-list-item--disabled)',
Copy link
Contributor

Choose a reason for hiding this comment

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

Changing the key to ENABLED_ITEMS_SELECTOR is more semantically correct

document.body.appendChild(root);
const selectedNode = root.querySelectorAll('.mdc-list-item')[1];
component.getDefaultFoundation().adapter_.removeClassForElementIndex(1, 'foo');
assert.isFalse(selectedNode.classList.contains('foo'));
Copy link
Contributor

Choose a reason for hiding this comment

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

when does the element get the foo class? I ran this and it looks like if you remove line 113 it still passes

foundation.handleKeydown(event);

td.verify(preventDefault(), {times: 1});
td.verify(mockAdapter.setAttributeForElementIndex(0, strings.ARIA_SELECTED, true));
Copy link
Contributor

Choose a reason for hiding this comment

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

do you want a {times: 1} here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

{times: 1} is the default behavior.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm pretty sure that is not correct. The default behavior is any non-zero number of invocations is valid.

foundation.handleKeydown(event);

td.verify(preventDefault(), {times: 2});
td.verify(mockAdapter.removeAttributeForElementIndex(0, strings.ARIA_SELECTED));

This comment was marked as resolved.

const {foundation, mockAdapter} = setupTest();
const preventDefault = td.func('preventDefault');
const target = {classList: ['mdc-list-item']};
const event = {key: 'Space', target, preventDefault};

This comment was marked as outdated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It doesn't doesn't increase coverage since that statement is already covered by the key parameter.

td.verify(mockFoundation.setSingleSelection(true), {times: 1});
});

test('selectedIndex calls setSelectedIndex on foundation', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

setSelectedIndex is a pretty complex function, but only has this one test. Might want to add 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.

setSelectedIndex has nearly 100% coverage through the click/key handler tests.

Copy link
Contributor

@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.

2 comments about test titles. I think they are off. BUT everything else looks good pending those changes.

td.verify(preventDefault(), {times: 1});
});

test('#handleKeydown enter key causes preventDefault to be called on the event', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you have the titles of this and the next tests reversed with regards to if singleSelection=true

td.verify(preventDefault(), {times: 0});
});

test('#handleKeydown enter key does not causes preventDefault to be called if singleSelection=true', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

same here - titles reversed

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

Successfully merging this pull request may close these issues.

None yet

6 participants