Skip to content

Commit

Permalink
fix(menu): Add disabled list items to menu (#780)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Rename symmetric registerDocumentClickHandler/deregisterDocumentClickHandler adapter methods to registerBodyClickHandler/deregisterBodyClickHandler
  • Loading branch information
amsheehan authored Jun 7, 2017
1 parent 52856b0 commit ef44d3d
Show file tree
Hide file tree
Showing 11 changed files with 161 additions and 44 deletions.
8 changes: 4 additions & 4 deletions demos/select.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,13 @@
<span class="mdc-select__selected-text">Pick a food group</span>
<div class="mdc-simple-menu mdc-select__menu">
<ul class="mdc-list mdc-simple-menu__items">
<li class="mdc-list-item" role="option" aria-disabled="true">
<li class="mdc-list-item" role="option" aria-disabled="true">
Pick a food group
</li>
<li class="mdc-list-item" role="option" tabindex="0">
Bread, Cereal, Rice, and Pasta
</li>
<li class="mdc-list-item" role="option" tabindex="0">
<li class="mdc-list-item" role="option" aria-disabled="true" tabindex="0">
Vegetables
</li>
<li class="mdc-list-item" role="option" tabindex="0">
Expand Down Expand Up @@ -108,7 +108,7 @@ <h2 class="mdc-typography--title">Fully-Featured Component</h2>
<li class="mdc-list-item" role="option" id="grains" tabindex="0">
Bread, Cereal, Rice, and Pasta
</li>
<li class="mdc-list-item" role="option" id="vegetables" tabindex="0">
<li class="mdc-list-item" role="option" id="vegetables" aria-disabled="true" tabindex="0">
Vegetables
</li>
<li class="mdc-list-item" role="option" id="fruit" tabindex="0">
Expand Down Expand Up @@ -146,7 +146,7 @@ <h2 class="mdc-typography--title">CSS Only</h2>
<select class="mdc-select">
<option value="" default selected>Pick a food group</option>
<option value="grains">Bread, Cereal, Rice, and Pasta</option>
<option value="vegetables">Vegetables</option>
<option value="vegetables" disabled>Vegetables</option>
<option value="fruit">Fruit</option>
<option value="dairy">Milk, Yogurt, and Cheese</option>
<option value="meat">Meat, Poultry, Fish, Dry Beans, Eggs, and Nuts</option>
Expand Down
6 changes: 0 additions & 6 deletions demos/simple-menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,6 @@
<li class="mdc-list-item" role="menuitem">Settings</li>
</ul>
</div>
</section>

<section class="example">



</section>

<div class="demo-content">
Expand Down
4 changes: 0 additions & 4 deletions packages/mdc-list/mdc-list.scss
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,7 @@ $mdc-list-side-padding: 16px;
.mdc-list--two-line.mdc-list--dense & {
height: 60px;
}
// stylelint-enable plugin/selector-bem-pattern
}

// postcss-bem-linter: end //

// Override anchor tag styles for the use-case of a list being used for navigation
Expand Down Expand Up @@ -221,11 +219,9 @@ a.mdc-list-item {
}

// Reset padding on mdc-list since it's already accounted for by the list group.
// stylelint-disable plugin/selector-bem-pattern
.mdc-list {
padding: 0;
}
// stylelint-enable plugin/selector-bem-pattern
}

// postcss-bem-linter: end
23 changes: 21 additions & 2 deletions packages/mdc-menu/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,24 @@ classes:
| `mdc-simple-menu--open-from-bottom-right` | Open the menu from the bottom right. |


#### Disabled menu items

When used in components such as MDC Menu, `mdc-list-item`'s can be disabled.
To disable a list item, set `aria-disabled` to `"true"`, and set `tabindex` to `"-1"`.

```html
<div class="mdc-simple-menu" tabindex="-1">
<ul class="mdc-simple-menu__items mdc-list" role="menu" aria-hidden="true">
<li class="mdc-list-item" role="menuitem" tabindex="0">
A Menu Item
</li>
<li class="mdc-list-item" role="menuitem" tabindex="-1" aria-disabled="true">
Disabled Menu Item
</li>
</ul>
</div>
```

### Using the JS Component

MDC Simple Menu ships with a Component / Foundation combo which allows for frameworks to richly integrate the
Expand Down Expand Up @@ -247,6 +265,7 @@ The adapter for temporary drawers must provide the following functions, with cor
| `removeClass(className: string) => void` | Removes a class from the root element. |
| `hasClass(className: string) => boolean` | Returns boolean indicating whether element has a given class. |
| `hasNecessaryDom() => boolean` | Returns boolean indicating whether the necessary DOM is present (namely, the `mdc-simple-menu__items` container). |
| `getAttributeForEventTarget(target: EventTarget, attributeName: string) => string` | Returns the value of a given attribute on an event target. |
| `getInnerDimensions() => {width: number, height: number}` | Returns an object with the items container width and height |
| `hasAnchor: () => boolean` | Returns whether the menu has an anchor for positioning. |
| `getAnchorDimensions() => { width: number, height: number, top: number, right: number, bottom: number, left: number }` | Returns an object with the dimensions and position of the anchor (same semantics as `DOMRect`). |
Expand All @@ -256,8 +275,8 @@ The adapter for temporary drawers must provide the following functions, with cor
| `getNumberOfItems() => numbers` | Returns the number of _item_ elements inside the items container. In our vanilla component, we determine this by counting the number of list items whose `role` attribute corresponds to the correct child role of the role present on the menu list element. For example, if the list element has a role of `menu` this queries for all elements that have a role of `menuitem`. |
| `registerInteractionHandler(type: string, handler: EventListener) => void` | Adds an event listener `handler` for event type `type`. |
| `deregisterInteractionHandler(type: string, handler: EventListener) => void` | Removes an event listener `handler` for event type `type`. |
| `registerDocumentClickHandler(handler: EventListener) => void` | Adds an event listener `handler` for event type 'click'. |
| `deregisterDocumentClickHandler(handler: EventListener) => void` | Removes an event listener `handler` for event type 'click'. |
| `registerBodyClickHandler(handler: EventListener) => void` | Adds an event listener `handler` for event type 'click'. |
| `deregisterBodyClickHandler(handler: EventListener) => void` | Removes an event listener `handler` for event type 'click'. |
| `getYParamsForItemAtIndex(index: number) => {top: number, height: number}` | Returns an object with the offset top and offset height values for the _item_ element inside the items container at the provided index. Note that this is an index into the list of _item_ elements, and not necessarily every child element of the list. |
| `setTransitionDelayForItemAtIndex(index: number, value: string) => void` | Sets the transition delay on the element inside the items container at the provided index to the provided value. The same notice for `index` applies here as above. |
| `getIndexForEventTarget(target: EventTarget) => number` | Checks to see if the `target` of an event pertains to one of the menu items, and if so returns the index of that item. Returns -1 if the target is not one of the menu items. The same notice for `index` applies here as above. |
Expand Down
1 change: 1 addition & 0 deletions packages/mdc-menu/simple/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const strings = {
ITEMS_SELECTOR: '.mdc-simple-menu__items',
SELECTED_EVENT: 'MDCSimpleMenu:selected',
CANCEL_EVENT: 'MDCSimpleMenu:cancel',
ARIA_DISABLED_ATTR: 'aria-disabled',
};

export const numbers = {
Expand Down
28 changes: 20 additions & 8 deletions packages/mdc-menu/simple/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default class MDCSimpleMenuFoundation extends MDCFoundation {
removeClass: (/* className: string */) => {},
hasClass: (/* className: string */) => {},
hasNecessaryDom: () => /* boolean */ false,
getAttributeForEventTarget: (/* target: EventTarget, attributeName: string */) => {},
getInnerDimensions: () => /* { width: number, height: number } */ ({}),
hasAnchor: () => /* boolean */ false,
getAnchorDimensions: () =>
Expand All @@ -47,8 +48,8 @@ export default class MDCSimpleMenuFoundation extends MDCFoundation {
getNumberOfItems: () => /* number */ 0,
registerInteractionHandler: (/* type: string, handler: EventListener */) => {},
deregisterInteractionHandler: (/* type: string, handler: EventListener */) => {},
registerDocumentClickHandler: (/* handler: EventListener */) => {},
deregisterDocumentClickHandler: (/* handler: EventListener */) => {},
registerBodyClickHandler: (/* handler: EventListener */) => {},
deregisterBodyClickHandler: (/* handler: EventListener */) => {},
getYParamsForItemAtIndex: (/* index: number */) => /* {top: number, height: number} */ ({}),
setTransitionDelayForItemAtIndex: (/* index: number, value: string */) => {},
getIndexForEventTarget: (/* target: EventTarget */) => /* number */ 0,
Expand All @@ -72,9 +73,9 @@ export default class MDCSimpleMenuFoundation extends MDCFoundation {
this.clickHandler_ = (evt) => this.handlePossibleSelected_(evt);
this.keydownHandler_ = (evt) => this.handleKeyboardDown_(evt);
this.keyupHandler_ = (evt) => this.handleKeyboardUp_(evt);
this.documentClickHandler_ = () => {
this.documentClickHandler_ = (evt) => {
this.adapter_.notifyCancel();
this.close();
this.close(evt);
};
this.isOpen_ = false;
this.startScaleX_ = 0;
Expand Down Expand Up @@ -114,7 +115,7 @@ export default class MDCSimpleMenuFoundation extends MDCFoundation {
this.adapter_.deregisterInteractionHandler('click', this.clickHandler_);
this.adapter_.deregisterInteractionHandler('keyup', this.keyupHandler_);
this.adapter_.deregisterInteractionHandler('keydown', this.keydownHandler_);
this.adapter_.deregisterDocumentClickHandler(this.documentClickHandler_);
this.adapter_.deregisterBodyClickHandler(this.documentClickHandler_);
}

// Calculate transition delays for individual menu items, so that they fade in one at a time.
Expand Down Expand Up @@ -300,6 +301,9 @@ export default class MDCSimpleMenuFoundation extends MDCFoundation {
}

handlePossibleSelected_(evt) {
if (this.adapter_.getAttributeForEventTarget(evt.target, strings.ARIA_DISABLED_ATTR) === 'true') {
return;
}
const targetIndex = this.adapter_.getIndexForEventTarget(evt.target);
if (targetIndex < 0) {
return;
Expand Down Expand Up @@ -372,14 +376,22 @@ export default class MDCSimpleMenuFoundation extends MDCFoundation {
this.animateMenu_();
this.adapter_.addClass(MDCSimpleMenuFoundation.cssClasses.OPEN);
this.focusOnOpen_(focusIndex);
this.adapter_.registerDocumentClickHandler(this.documentClickHandler_);
this.adapter_.registerBodyClickHandler(this.documentClickHandler_);
});
this.isOpen_ = true;
}

// Close the menu.
close() {
this.adapter_.deregisterDocumentClickHandler(this.documentClickHandler_);
close(evt = null) {
const targetIsDisabled = evt ?
this.adapter_.getAttributeForEventTarget(evt.target, strings.ARIA_DISABLED_ATTR) === 'true' :
false;

if (targetIsDisabled) {
return;
}

this.adapter_.deregisterBodyClickHandler(this.documentClickHandler_);
this.adapter_.addClass(MDCSimpleMenuFoundation.cssClasses.ANIMATING);
requestAnimationFrame(() => {
this.removeTransitionDelays_();
Expand Down
5 changes: 3 additions & 2 deletions packages/mdc-menu/simple/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class MDCSimpleMenu extends MDCComponent {
removeClass: (className) => this.root_.classList.remove(className),
hasClass: (className) => this.root_.classList.contains(className),
hasNecessaryDom: () => Boolean(this.itemsContainer_),
getAttributeForEventTarget: (target, attributeName) => target.getAttribute(attributeName),
getInnerDimensions: () => {
const {itemsContainer_: itemsContainer} = this;
return {width: itemsContainer.offsetWidth, height: itemsContainer.offsetHeight};
Expand All @@ -83,8 +84,8 @@ export class MDCSimpleMenu extends MDCComponent {
getNumberOfItems: () => this.items.length,
registerInteractionHandler: (type, handler) => this.root_.addEventListener(type, handler),
deregisterInteractionHandler: (type, handler) => this.root_.removeEventListener(type, handler),
registerDocumentClickHandler: (handler) => document.addEventListener('click', handler),
deregisterDocumentClickHandler: (handler) => document.removeEventListener('click', handler),
registerBodyClickHandler: (handler) => document.body.addEventListener('click', handler),
deregisterBodyClickHandler: (handler) => document.body.removeEventListener('click', handler),
getYParamsForItemAtIndex: (index) => {
const {offsetTop: top, offsetHeight: height} = this.items[index];
return {top, height};
Expand Down
17 changes: 17 additions & 0 deletions packages/mdc-menu/simple/mdc-simple-menu.scss
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ $mdc-simple-menu-item-fade-duration: .3s;
outline: none;
color: inherit;
text-decoration: none;
user-select: none;

@include mdc-typography(subheading2);

Expand Down Expand Up @@ -190,6 +191,22 @@ $mdc-simple-menu-item-fade-duration: .3s;
opacity: .18;
}

.mdc-list-item[aria-disabled="true"] {
cursor: default;

@include mdc-theme-prop(color, text-disabled-on-light);

@include mdc-theme-dark(".mdc-select") {
@include mdc-theme-prop(color, text-disabled-on-dark);
}
}

.mdc-list-item[aria-disabled="true"] {
&:focus::before,
&:active::before {
opacity: 0;
}
}
/* stylelint-enable plugin/selector-bem-pattern */
}

Expand Down
33 changes: 33 additions & 0 deletions packages/mdc-select/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,39 @@ style dependencies for both the mdc-list and mdc-menu for this component to func
</div>
```

#### Disabled options

When used in components such as MDC Select, `mdc-list-item`'s can be disabled.
To disable a list item, set `aria-disabled` to `"true"`, and set `tabindex` to `"-1"`.

```html
<div class="mdc-select" role="listbox">
<span class="mdc-select__selected-text">Pick a food group</span>
<div class="mdc-simple-menu mdc-select__menu">
<ul class="mdc-list mdc-simple-menu__items">
<li class="mdc-list-item" role="option" id="grains" tabindex="0">
Bread, Cereal, Rice, and Pasta
</li>
<li class="mdc-list-item" role="option" id="vegetables" tabindex="-1" aria-disabled="true">
Vegetables (Disabled)
</li>
<li class="mdc-list-item" role="option" id="fruit" tabindex="0">
Fruit
</li>
<li class="mdc-list-item" role="option" id="dairy" tabindex="0">
Milk, Yogurt, and Cheese
</li>
<li class="mdc-list-item" role="option" id="meat" tabindex="0">
Meat, Poultry, Fish, Dry Beans, Eggs, and Nuts
</li>
<li class="mdc-list-item" role="option" id="fats" tabindex="0">
Fats, Oils, and Sweets
</li>
</ul>
</div>
</div>
```

### Using the Pure CSS Select

The `mdc-select` CSS class also works on browser's native `<select>` elements, allowing for a
Expand Down
35 changes: 22 additions & 13 deletions test/unit/mdc-menu/mdc-simple-menu.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ test('adapter#hasNecessaryDom returns true if the DOM includes a drawer', () =>
assert.isOk(component.getDefaultFoundation().adapter_.hasNecessaryDom());
});

test('adapter#getAttributeForEventTarget returns the value of an attribute for a given event target', () => {
const {root, component} = setupTest();
const attrName = 'aria-disabled';
const attrVal = 'true';
const target = root.querySelectorAll('[role="menuitem"]')[1];

target.setAttribute(attrName, attrVal);

assert.equal(component.getDefaultFoundation().adapter_.getAttributeForEventTarget(target, attrName), attrVal);
});

test('adapter#hasNecessaryDom returns false if the DOM does not include the items container', () => {
const {root, component} = setupTest();
const items = root.querySelector(strings.ITEMS_SELECTOR);
Expand Down Expand Up @@ -160,25 +171,23 @@ test('adapter#deregisterInteractionHandler proxies to removeEventListener', () =
td.verify(handler(td.matchers.anything()), {times: 0});
});

test('adapter#registerDocumentClickHandler proxies to addEventListener', () => {
const {root, component} = setupTest();
test('adapter#registerBodyClickHandler proxies to addEventListener', () => {
const {component} = setupTest();
const handler = td.func('interactionHandler');
document.body.appendChild(root);
component.getDefaultFoundation().adapter_.registerDocumentClickHandler(handler);
domEvents.emit(document, 'click');

component.getDefaultFoundation().adapter_.registerBodyClickHandler(handler);
domEvents.emit(document.body, 'click');
td.verify(handler(td.matchers.anything()));
document.body.removeChild(root);
});

test('adapter#deregisterDocumentClickHandler proxies to removeEventListener', () => {
const {root, component} = setupTest();
test('adapter#deregisterBodyClickHandler proxies to removeEventListener', () => {
const {component} = setupTest();
const handler = td.func('interactionHandler');
document.body.appendChild(root);
root.addEventListener('click', handler);
component.getDefaultFoundation().adapter_.deregisterInteractionHandler(handler);
domEvents.emit(document, 'click');

document.body.addEventListener('click', handler);
component.getDefaultFoundation().adapter_.deregisterBodyClickHandler(handler);
domEvents.emit(document.body, 'click');
td.verify(handler(td.matchers.anything()), {times: 0});
document.body.removeChild(root);
});

test('adapter#getYParamsForItemAtIndex returns the height and top of the item at the provided index', () => {
Expand Down
Loading

0 comments on commit ef44d3d

Please sign in to comment.