-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Conversation
Codecov Report
@@ 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
Continue to review full report at Codecov.
|
package-lock.json
Outdated
@@ -6176,6 +6176,7 @@ | |||
"version": "0.0.9", | |||
"bundled": true, | |||
"dev": true, | |||
"optional": true, |
There was a problem hiding this comment.
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?
packages/mdc-list/README.md
Outdated
@@ -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`. |
There was a problem hiding this comment.
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.
packages/mdc-list/foundation.js
Outdated
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) { |
There was a problem hiding this comment.
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?
packages/mdc-list/foundation.js
Outdated
} | ||
|
||
if (ndx >= 0) { | ||
if (this.adapter_.getListItemCount() > ndx) { |
There was a problem hiding this comment.
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
packages/mdc-list/foundation.js
Outdated
} 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)) { |
There was a problem hiding this comment.
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)
packages/mdc-list/foundation.js
Outdated
|
||
currentIndex = this.adapter_.getListItemIndex(listItem); | ||
|
||
if (currentIndex < 0) { |
There was a problem hiding this comment.
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.
packages/mdc-list/index.js
Outdated
} | ||
|
||
this.foundation_.setSingleSelection(value); | ||
const selectedElement = this.root_.querySelector('.mdc-list-item[aria-selected="true"]'); |
There was a problem hiding this comment.
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?
packages/mdc-list/index.js
Outdated
|
||
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"])')) |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clever!!!
packages/mdc-list/README.md
Outdated
#### 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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
packages/mdc-list/adapter.js
Outdated
@@ -41,11 +41,48 @@ class MDCListAdapter { | |||
/** @param {Element} node */ | |||
getListItemIndex(node) {} | |||
|
|||
/** | |||
* @param {Number} index |
There was a problem hiding this comment.
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.
packages/mdc-list/constants.js
Outdated
FOCUSABLE_CHILD_ELEMENTS: 'button:not(:disabled), a', | ||
ITEMS_SELECTOR: '.mdc-list-item', | ||
ITEMS_SELECTOR: '.mdc-list-item:not(.mdc-list-item--disabled)', |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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_); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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_); |
There was a problem hiding this comment.
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.
Hey @williamernest I opened PR #3029 to export |
There was a problem hiding this 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 :)
packages/mdc-list/README.md
Outdated
|
||
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so that the user...
packages/mdc-list/README.md
Outdated
(`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), |
There was a problem hiding this comment.
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'?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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> | ||
``` |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
packages/mdc-list/constants.js
Outdated
@@ -18,14 +18,16 @@ | |||
/** @enum {string} */ | |||
const cssClasses = { | |||
LIST_ITEM_CLASS: 'mdc-list-item', | |||
LIST_SELECTED_CLASS: 'mdc-list-item--selected', |
There was a problem hiding this comment.
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
packages/mdc-list/constants.js
Outdated
FOCUSABLE_CHILD_ELEMENTS: 'button:not(:disabled), a', | ||
ITEMS_SELECTOR: '.mdc-list-item', | ||
ITEMS_SELECTOR: '.mdc-list-item:not(.mdc-list-item--disabled)', |
There was a problem hiding this comment.
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
test/unit/mdc-list/mdc-list.test.js
Outdated
document.body.appendChild(root); | ||
const selectedNode = root.querySelectorAll('.mdc-list-item')[1]; | ||
component.getDefaultFoundation().adapter_.removeClassForElementIndex(1, 'foo'); | ||
assert.isFalse(selectedNode.classList.contains('foo')); |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
This comment was marked as resolved.
Sorry, something went wrong.
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.
This comment was marked as outdated.
Sorry, something went wrong.
There was a problem hiding this comment.
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', () => { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this 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', () => { |
There was a problem hiding this comment.
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', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here - titles reversed
No description provided.